๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๋ฐฑ์—”๋“œ/spring&springboot

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„_Spring Boot ์ˆ™๋ จ์ฃผ์ฐจ] ์‚ฌ์šฉ์ž ๊ด€๋ฆฌํ•˜๊ธฐ(Spring Security, ์ ‘๊ทผ ๋ถˆ๊ฐ€ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ)

by alswlfl 2026. 6. 29.

Spring Security ํ”„๋ ˆ์ž„์›Œํฌ

Spring Security ํ”„๋ ˆ์ž„์›Œํฌ๋Š” Spring ์„œ๋ฒ„์—์„œ ํ•„์š”ํ•œ ์ธ์ฆ ๋ฐ ์ธ๊ฐ€๋ฅผ ์œ„ํ•ด ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์คŒ์œผ๋กœ์จ ๊ฐœ๋ฐœ์˜ ์ˆ˜๊ณ ๋ฅผ ๋œ์–ด์คŒ

  • ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ framework ์ถ”๊ฐ€ํ•ด์ค˜์•ผํ•จ → Security(build.gradle ๋‚ด)
implementation 'org.springframework.boot:spring-boot-starter-security'
  • Spring Security๋Š” Filter ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•จ
  • Spring Security ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ์„ ์œ„ํ•ด ๊ธฐ๋ณธ์ ์ธ Security ์„ค์ •ํ•ด์ค˜์•ผํ•จ
  • Security๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Session ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•จ(+Default ๋กœ๊ทธ์ธ๋„ ์ œ๊ณตํ•จ) 

Security ์„ค์ •(config > WebSecurityConfig ํด๋ž˜์Šค ํŒŒ์ผ ์ƒ์„ฑ)

package com.sparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // Spring Security ์ง€์›์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
public class WebSecurityConfig {

    // Security ์‚ฌ์šฉํ•  ๋•Œ, ์–ด๋–ค URL์„ ์ธ๊ฐ€ํ• ์ง€์— ๋Œ€ํ•œ ์„ค์ •๊ณผ ์–ด๋–ค ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ณ  ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒ ๋‹ค๋Š” ์˜ต์…˜ ์„ค์ •
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF ์„ค์ •
        http.csrf((csrf) -> csrf.disable()); // 'CSRF๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒ ๋‹ค'๋ผ๊ณ  ์„ค์ •

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        // 'resources ํ•˜์œ„ ํŒŒ์ผ(js, css)๋กœ ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ ๋ชจ๋“  ์ ‘๊ทผ ํ—ˆ์šฉํ•˜๊ฒ ๋‹ค'๋ผ๋Š” ์˜๋ฏธ
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources ์ ‘๊ทผ ํ—ˆ์šฉ ์„ค์ •
                        // .requestMatchers("/api/user/**").permitAll() // /api/user/ ํ˜•ํƒœ๋กœ ์˜ค๋Š” URL์€ ๋ชจ๋“  ์ ‘๊ทผ ํ—ˆ์šฉ
                        .anyRequest().authenticated() // ๊ทธ ์™ธ ๋ชจ๋“  ์š”์ฒญ ์ธ์ฆ์ฒ˜๋ฆฌ
                // ์—ฌ๋Ÿฌ ์˜ต์…˜ ์กด์žฌํ•จ
        );

        // ๋กœ๊ทธ์ธ ์‚ฌ์šฉ -> Spring Security์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์กด์žฌํ•จ
        // ๋˜ํ•œ, ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ œ๊ณตํ•ด์คŒ(username์€ ๊ธฐ๋ณธ์ ์œผ๋กœ 'user')
        http.formLogin(Customizer.withDefaults());

        return http.build();
    }
}

 

CSRF(Cross-Site Request Forgery, ์‚ฌ์ดํŠธ ๊ฐ„ ์š”์ฒญ ์œ„์กฐ)
๊ณต๊ฒฉ์ž๊ฐ€ ์ธ์ฆ๋œ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅ๋œ ์ฟ ํ‚ค์˜ ์„ธ์…˜ ์ •๋ณด๋ฅผ ํ™œ์šฉํ•ด ์›น ์„œ๋ฒ„์— ์‚ฌ์šฉ์ž๊ฐ€ ์˜๋„ํ•˜์ง€ ์•Š์€ ์š”์ฒญ์„ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ
- CSRF ์„ค์ •์ด ๋˜์–ด์žˆ๋Š” ๊ฒฝ์šฐ html์—์„œ CSRF ํ† ํฐ ๊ฐ’์„ ๋„˜๊ฒจ์ฃผ์–ด์•ผ ์š”์ฒญ์„ ์ˆ˜์‹  ๊ฐ€๋Šฅํ•จ
- ์ฟ ํ‚ค ๊ธฐ๋ฐ˜์˜ ์ทจ์•ฝ์ ์„ ์ด์šฉํ•œ ๊ณต๊ฒฉ์ด๊ธฐ ๋•Œ๋ฌธ์— REST ๋ฐฉ์‹์˜ API์—์„œ๋Š” disable์ด ๊ฐ€๋Šฅํ•จ
* ํ•ด๋‹น ๊ฐ•์˜์—์„œ๋Š” POST ์š”์ฒญ๋งˆ๋‹ค ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๋Œ€์‹  CSRF protection์„ disableํ•จ
http.csrf((csrf) -> csrf.disable())

Spring Security ์ดํ•ดํ•˜๊ธฐ

  • Spring์—์„œ ๋ชจ๋“  ํ˜ธ์ถœ์€ DispatcherServlet์„ ํ†ต๊ณผํ•˜๊ฒŒ ๋˜๊ณ  ์ดํ›„์— ๊ฐ ์š”์ฒญ์„ ๋‹ด๋‹นํ•˜๋Š” Controller๋กœ ๋ถ„๋ฐฐํ•จ
  • ์ด๋•Œ, ๊ฐ ์š”์ฒญ์— ๋Œ€ํ•ด ๊ณตํ†ต์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผํ•  ํ•„์š”๊ฐ€ ์žˆ์„ ๋•Œ DispatcherServlet ์ด์ „์— ๋‹จ๊ณ„๊ฐ€ ํ•„์š”ํ•˜๋ฉฐ ์ด๊ฒƒ์ด Filter์ž„

Spring Security ๋™์ž‘ ๊ณผ์ •

  • Spring Security๋„ ์ธ์ฆ ๋ฐ ์ธ๊ฐ€๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด Filter๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, Spring Security๋Š” FilterChainProxy๋ฅผ ํ†ตํ•ด์„œ ์ƒ์„ธ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์Œ

Form Login ๊ธฐ๋ฐ˜ ์ธ์ฆ

  • Form Login ๊ธฐ๋ฐ˜ ์ธ์ฆ์€ ์ธ์ฆ์ด ํ•„์š”ํ•œ URL ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ, ์ธ์ฆ์ด ๋˜์ง€ ์•Š์œผ๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•จ

UsernamePasswordAuthenticationFilter

Form Login ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์‚ฌ์šฉํ•  ๋•Œ, ์š”์ฒญ๋œ username๊ณผ password๋ฅผ ์ธ์ฆ ์ฒ˜๋ฆฌํ•˜๋Š” Filter

UsernamePasswordAuthenticationFilter

  1. ์‚ฌ์šฉ์ž๊ฐ€ username๊ณผ password ์ œ์ถœ
  2. UsernamePasswordAuthenticationFilter๋Š” ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด Authentication ์ข…๋ฅ˜ ์ค‘ ํ•˜๋‚˜์ธ UsernamePasswordAuthenticationToken์„ ๋งŒ๋“ฆ
  3. ๋งŒ๋“ค์–ด์ง„ ํ† ํฐ์„ AuthenticationManager์—๊ฒŒ ๋„˜๊ธด ํ›„, ์ธ์ฆ์„ ์‹œ๋„ํ•จ
  4. ์„ฑ๊ณตํ•˜๋ฉด, SecurityContextHolder์— ๊ทธ Authentication(UsernamePasswordAuthenticationToken)์„ ์„ธํŒ…ํ•˜๋ฉด์„œ ์ธ์ฆ ์ฒ˜๋ฆฌ ์™„๋ฃŒ๋จ
  5. ์‹คํŒจํ•˜๋ฉด, SecurityContextHolder๋ฅผ ๋น„์›€

SecurityContextHolder

SecurityContextHolder

  • SecurityContextHolder ์•ˆ์— SecurityContext๊ฐ€ ์กด์žฌํ•˜๊ณ , ๊ทธ ์•ˆ์— Authentication์ด ๋‹ด๊น€
  • SecurityContext๋Š” SecurityContextHolder๋ฅผ ํ†ตํ•ด์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•จ

 

Authentication

Authentication์€ ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ SecurityContext์—์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Œ

  • principal: ์‚ฌ์šฉ์ž๋ฅผ ์‹๋ณ„ํ•จ
    - Username/Password ๋ฐฉ์‹์œผ๋กœ ์ธ์ฆ์„ ํ•  ๋•Œ ์ผ๋ฐ˜์ ์œผ๋กœ UserDetails ์ธ์Šคํ„ด์Šค์ž„
  • credentials: ์ฃผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ, ๋Œ€๋ถ€๋ถ„ ์‚ฌ์šฉ์ž ์ธ์ฆ์— ์‚ฌ์šฉํ•œ ํ›„ ๋น„์›€
  • authorities: ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ถ€์—ฌํ•œ ๊ถŒํ•œ์„ GrantedAuthority๋กœ ์ถ”์ƒํ™”ํ•ด ์‚ฌ์šฉํ•จ
<UserDetails>
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
	UserRoleEnum role = user.getRole();
    String authority = role.getAuthority();
    
    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
    Collection<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(simpleGrantedAuthority);
    
    return authorities;
}

Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

 

UserDetailsService๋Š” username/password ์ธ์ฆ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ๋•Œ ์‚ฌ์šฉ์ž๋ฅผ ์กฐํšŒํ•˜๊ณ  ๊ฒ€์ฆํ•œ ํ›„ UserDetails๋ฅผ ๋ฐ˜ํ™˜ํ•จ. Customํ•˜์—ฌ Bean์œผ๋กœ ๋“ฑ๋ก ํ›„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

UserDetails๋Š” UsernamePasswordAuthenticationToken ํƒ€์ž…์˜ Authentication๋ฅผ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉ๋˜๋ฉฐ ํ•ด๋‹น ์ธ์ฆ๊ฐ์ฒด๋Š” SecurityContextHolder์— ์„ธํŒ…๋จ. Customํ•ด ์‚ฌ์šฉ๊ฐ€๋Šฅํ•จ


Spring Security ๋กœ๊ทธ์ธ(Session ๋ฐฉ์‹)

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์‚ฌ์šฉ ์ „/ํ›„

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์‚ฌ์šฉ ์ „๊ณผ ํ›„

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์‚ฌ์šฉ ํ›„, Client์˜ ์š”์ฒญ์€ ๋ชจ๋‘ Spring Security๋ฅผ ๊ฑฐ์น˜๊ฒŒ๋จ
  • Spring Security ์—ญํ•  → ์ธ์ฆ/์ธ๊ฐ€
    • ์„ฑ๊ณต ์‹œ: Controller๋กœ Client ์š”์ฒญ ์ „๋‹ฌ → Client ์š”์ฒญ + ์‚ฌ์šฉ์ž ์ •๋ณด(UserDetails)
    • ์‹คํŒจ ์‹œ: Controller๋กœ Client ์š”์ฒญ ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ(Client์—๊ฒŒ Error Response ๋ณด๋ƒ„)

๋กœ๊ทธ์ธ ๊ณผ์ •

๋กœ๊ทธ์ธ ๊ณผ์ •

  • Authentication Manager๊ฐ€ ์ธ์ฆ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋•Œ, UserDetailsService๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํ•ด๋‹นํ•˜๋Š” username์œผ๋กœ ํšŒ์› ์ •๋ณด๋ฅผ ์กฐํšŒํ•จ(์ด ๋ถ€๋ถ„์„ ์ปค์Šคํ…€ํ•ด์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
  • ์กฐํšŒํ•œ ์œ ์ € ์ •๋ณด๋กœ UserDetails ๋งŒ๋“  ๋‹ค์Œ์— Authentication Manger์—๊ฒŒ ์ „๋‹ฌํ•˜๋ฉด, ์ธ์ฆ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•จ

1. Client

  • ๋กœ๊ทธ์ธ ์‹œ๋„
  • ๋กœ๊ทธ์ธ ์‹œ๋„ํ•  username, password ์ •๋ณด๋ฅผ HTTP body๋กœ ์ „๋‹ฌ(POST ์š”์ฒญ)
  • ๋กœ๊ทธ์ธ ์‹œ๋„ URL์€ WebSecurityConfig ํด๋ž˜์Šค์—์„œ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ
package com.sparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // Spring Security ์ง€์›์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
public class WebSecurityConfig {

    // Security ์‚ฌ์šฉํ•  ๋•Œ, ์–ด๋–ค URL์„ ์ธ๊ฐ€ํ• ์ง€์— ๋Œ€ํ•œ ์„ค์ •๊ณผ ์–ด๋–ค ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ณ  ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒ ๋‹ค๋Š” ์˜ต์…˜ ์„ค์ •
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF ์„ค์ •
        http.csrf((csrf) -> csrf.disable()); // 'CSRF๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒ ๋‹ค'๋ผ๊ณ  ์„ค์ •

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        // 'resources ํ•˜์œ„ ํŒŒ์ผ(js, css)๋กœ ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ ๋ชจ๋“  ์ ‘๊ทผ ํ—ˆ์šฉํ•˜๊ฒ ๋‹ค'๋ผ๋Š” ์˜๋ฏธ
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources ์ ‘๊ทผ ํ—ˆ์šฉ ์„ค์ •
                        .requestMatchers("/api/user/**").permitAll() // /api/user/ ํ˜•ํƒœ๋กœ ์˜ค๋Š” URL์€ ๋ชจ๋“  ์ ‘๊ทผ ํ—ˆ์šฉ
                        .anyRequest().authenticated() // ๊ทธ ์™ธ ๋ชจ๋“  ์š”์ฒญ ์ธ์ฆ์ฒ˜๋ฆฌ
                // ์—ฌ๋Ÿฌ ์˜ต์…˜ ์กด์žฌํ•จ
        );

        // Form Login ์„ค์ • ์‚ฌ์šฉ
        http.formLogin((formLogin) ->
                formLogin
                        // ๋กœ๊ทธ์ธ View ์ œ๊ณต (GET /api/user/login-page)
                        .loginPage("/api/user/login-page")
                        // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ (POST /api/user/login)
                        // ์ด์ „์— Controller ๋‚ด์— ๋งŒ๋“  API๊ฐ€ ์•„๋‹Œ, Spring Security ์ž์ฒด์—์„œ ๋กœ๊ทธ์ธํ•  ๋•Œ์˜ url ์ง€์ •
                        // ์ฆ‰ Controller ์•ž๋‹จ์—์„œ ์ฒ˜๋ฆฌ
                        .loginProcessingUrl("/api/user/login")
                        // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ํ›„ ์„ฑ๊ณต ์‹œ URL
                        .defaultSuccessUrl("/")
                        // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ํ›„ ์‹คํŒจ ์‹œ URL
                        .failureUrl("/api/user/login-page?error")
                        .permitAll()
        );

        return http.build();
    }
}

 

UserDetailsService์™€ UserDetails ๊ตฌํ˜„

  • UserDetailsServiceImpl๊ณผ UserDetailsImpl์„ ์ปค์Šคํ…€ํ•˜๊ฒŒ ๋˜๋ฉด, 'Security์˜ default ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒ ๋‹ค'๋ผ๋Š” ์„ค์ •์ด ๋จ
package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // ํ•ด๋‹น ์œ ์ € ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ด
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        return new UserDetailsImpl(user); // ์ •๋ณด๋กœ ๊ฐ์ฒด ์ƒ์„ฑํ•ด ๋ฐ˜ํ™˜
    }
}

 

Authentication์— ์ €์žฅํ•œ User ์ •๋ณด ํ™œ์šฉํ•˜๊ธฐ

package com.sparta.springauth.controller;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.security.UserDetailsImpl;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        // Authentication์˜ Principle
        User user = userDetails.getUser();
        System.out.println("user.getUsername() = "+user.getUsername());

        return "redirect:/";
    }
}

Spring Security JWT ๋กœ๊ทธ์ธ

JWT ์ธ์ฆ ์ฒ˜๋ฆฌ Filter

  • ๊ธฐ์กด์— Controller๊ณผ Service ๋‹จ์—์„œ ์ฒ˜๋ฆฌํ•œ JWT ๊ด€๋ จ ๋กœ์ง์ธ login ๋ถ€๋ถ„ ์‚ญ์ œ → ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ์™€ ์ธ์ฆ/์ธ๊ฐ€ ์ฒ˜๋ฆฌ ๋ถ„๋ฆฌํ•˜๋Š” ๋ชฉ์ 
  • UsernamePasswordAuthenticationFilter ์ƒ์† ๋ฐ›์•„ ์‚ฌ์šฉ
    • UsernamePasswordAuthenticationFilter๋Š” ๋ฐ›์•„์˜จ username๊ณผ password๋ฅผ ์ธ์ฆ ๊ฐ์ฒด์ธ UsernamePasswordAuthenticationToken์œผ๋กœ ๋งŒ๋“  ํ›„, AuthenticationManager๋ฅผ ํ†ตํ•ด ํ™•์ธํ•˜๋Š” ์ž‘์—… ์ˆ˜ํ–‰ํ•จ
  • Session ๋ฐฉ์‹์ด ์•„๋‹Œ JWT ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์œ„ ์ž‘์—…์„ ์ง์ ‘ Customํ•ด์„œ ์‚ฌ์šฉ
package com.sparta.springauth.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.springauth.dto.LoginRequestDto;
import com.sparta.springauth.entity.UserRoleEnum;
import com.sparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

@Slf4j(topic = "๋กœ๊ทธ์ธ ๋ฐ JWT ์ƒ์„ฑ")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        // WebSecurityConfig์—์„œ loginProcessingUrl ๋ถ€๋ถ„์— ์„ค์ •ํ•ด ๋†“์€ ๊ฐ’์„
        // Customํ•˜๊ธฐ ์œ„ํ•ด ์ƒ์„ฑ์ž ํ˜ธ์ถœ ์‹œ ๊ฐ™์ด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
        setFilterProcessesUrl("/api/user/login");
    }

    // ๋กœ๊ทธ์ธ ์‹œ๋„ํ•˜๋Š” ๋ฉ”์„œ๋“œ
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("๋กœ๊ทธ์ธ ์‹œ๋„");
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            // AuthenticationManager๋Š” authenticate ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Œ
            // authenticate ๋ฉ”์„œ๋“œ๋Š” ๊ฒ€์ฆ(์ธ์ฆ ์ฒ˜๋ฆฌ)ํ•˜๋Š” ๋ฉ”์„œ๋“œ
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    // ์„ฑ๊ณตํ–ˆ์„ ๋•Œ ์ˆ˜ํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ
    // Authentication ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์˜ด(UserDetails)
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("๋กœ๊ทธ์ธ ์„ฑ๊ณต ๋ฐ JWT ์ƒ์„ฑ");
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(username, role); // ํ† ํฐ ์ƒ์„ฑ
        jwtUtil.addJwtToCookie(token, response); // JWT ํ† ํฐ์„ ์ฟ ํ‚ค์— ๋‹ด๊ธฐ
    }

    // ์‹คํŒจํ–ˆ์„ ๋•Œ ์ˆ˜ํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("๋กœ๊ทธ์ธ ์‹คํŒจ");
        response.setStatus(401); // ์ธ์ฆ์ด ๋˜์ง€ ์•Š์•˜์Œ ์ƒํƒœ์ฝ”๋“œ ์ „๋‹ฌ
    }
}

JWT ์ธ๊ฐ€ ์ฒ˜๋ฆฌ Filter

package com.sparta.springauth.jwt;

import com.sparta.springauth.security.UserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j(topic = "JWT ๊ฒ€์ฆ ๋ฐ ์ธ๊ฐ€")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getTokenFromRequest(req); // ์ฟ ํ‚ค์—์„œ JWT ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ

        if (StringUtils.hasText(tokenValue)) {
            // JWT ํ† ํฐ substring
            tokenValue = jwtUtil.substringToken(tokenValue); // Bearer ๋–ผ๊ธฐ
            log.info(tokenValue);

            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                return;
            }

            Claims info = jwtUtil.getUserInfoFromToken(tokenValue); // JWT ๋‚ด ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        filterChain.doFilter(req, res);
    }

    // ์ธ์ฆ ์ฒ˜๋ฆฌ
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    // ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ
    private Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

ํ•„ํ„ฐ ๋“ฑ๋ก

package com.sparta.springauth.config;

import com.sparta.springauth.jwt.JwtAuthenticationFilter;
import com.sparta.springauth.jwt.JwtAuthorizationFilter;
import com.sparta.springauth.jwt.JwtUtil;
import com.sparta.springauth.security.UserDetailsServiceImpl;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // Spring Security ์ง€์›์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;

    public WebSecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService, AuthenticationConfiguration authenticationConfiguration) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
        this.authenticationConfiguration = authenticationConfiguration;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF ์„ค์ •
        http.csrf((csrf) -> csrf.disable());

        // ๊ธฐ๋ณธ ์„ค์ •์ธ Session ๋ฐฉ์‹์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  JWT ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources ์ ‘๊ทผ ํ—ˆ์šฉ ์„ค์ •
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'๋กœ ์‹œ์ž‘ํ•˜๋Š” ์š”์ฒญ ๋ชจ๋‘ ์ ‘๊ทผ ํ—ˆ๊ฐ€
                        .anyRequest().authenticated() // ๊ทธ ์™ธ ๋ชจ๋“  ์š”์ฒญ ์ธ์ฆ์ฒ˜๋ฆฌ
        );

        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/user/login-page").permitAll()
        );

        // ํ•„ํ„ฐ ๊ด€๋ฆฌ
        // ์ธ๊ฐ€ Filter ๋จผ์ € ์ˆ˜ํ–‰
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        // UsernamePasswordAuthenticationFilter ์ˆ˜ํ–‰ ์ „์— jwtAuthenticationFilter ๋จผ์ € ์ˆ˜ํ–‰
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

JWT ์„ฑ๊ณต


์ ‘๊ทผ ๋ถˆ๊ฐ€ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

์ ‘๊ทผ ๋ถˆ๊ฐ€ ํŽ˜์ด์ง€๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ํ•ด๋‹น ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•œ๋‹ค๊ฑฐ๋‚˜ ์•„๋‹ˆ๋ฉด ํŠน์ •ํ•œ API์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ → ์ฆ‰, ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์— ๋”ฐ๋ผ ์ œ์–ด ์„ค์ •

Spring Security์—์„œ ๊ถŒํ•œ(Authority) ์„ค์ • ๋ฐฉ๋ฒ•

๊ถŒํ•œ ์„ค์ • ๋ฐฉ๋ฒ•

  • ํšŒ์› ์ƒ์„ธ์ •๋ณด(UserDetailImpl)๋ฅผ ํ†ตํ•œ "๊ถŒํ•œ(Authority)" ์„ค์ • ๊ฐ€๋Šฅ
  • ๊ถŒํ•œ์€ 1๊ฐœ ์ด์ƒ ์„ค์ • ๊ฐ€๋Šฅ
  • "๊ถŒํ•œ ์ด๋ฆ„" ๊ทœ์น™
    • "ROLE_"๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•จ
      ex) "ADMIN" ๊ถŒํ•œ ๋ถ€์—ฌ → "ROLE_ADMIN"
            "USER" ๊ถŒํ•œ ๋ถ€์—ฌ → "ROLE_USER"
package com.sparta.springauth.entity;

public enum UserRoleEnum {
    USER(Authority.USER),  // ์‚ฌ์šฉ์ž ๊ถŒํ•œ
    ADMIN(Authority.ADMIN);  // ๊ด€๋ฆฌ์ž ๊ถŒํ•œ

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() {
        return this.authority;
    }

    public static class Authority {
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
}
// ๊ถŒํ•œ ์„ค์ • ์‹œ ์‚ฌ์šฉ
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    UserRoleEnum role = user.getRole();
    String authority = role.getAuthority();

    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
    Collection<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(simpleGrantedAuthority);

    return authorities;
}
  • ์‚ฌ์šฉ์ž์— ์ €์žฅ๋˜์–ด์žˆ๋Š” role์˜ authority ๊ฐ’์„ ์‚ฌ์šฉํ•ด ๋™์ ์œผ๋กœ ์ €์žฅ

API ๋ณ„ ๊ถŒํ•œ ์ œ์–ด ๋ฐฉ๋ฒ•

์ œ์–ดํ•˜๊ณ  ์‹ถ์€ API Controller ์ชฝ์—์„œ @Secured ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ

  • @Secured("๊ถŒํ•œ ์ด๋ฆ„") ์„ ์–ธ
  • ๊ถŒํ•œ 1๊ฐœ ์ด์ƒ ์„ค์ • ๊ฐ€๋Šฅ
@Secured(UserRoleEnum.Authority.ADMIN) // ๊ด€๋ฆฌ์ž์šฉ
@GetMapping("/products/secured")
public String getProductsByAdmin(@AuthenticationPrincipal UserDetailsImpl userDetails) {
    System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
    for (GrantedAuthority authority : userDetails.getAuthorities()) {
        System.out.println("authority.getAuthority() = " + authority.getAuthority());
    }

    return "redirect:/";
}
  • @Secured ์–ด๋…ธํ…Œ์ด์…˜ ํ™œ์„ฑํ™” ๋ฐฉ๋ฒ• → @EnableMethodSecurity(securedEnabled = true)
@EnableMethodSecurity(securedEnabled = true) // @Secured ์–ด๋…ธํ…Œ์ด์…˜ ํ™œ์„ฑํ™”๋ฅผ ์œ„ํ•ด ์„ค์ •
public class WebSecurityConfig {
	...
}

๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ์•„๋‹Œ ๊ฒฝ์šฐ /api/products/secured ์ž…๋ ฅ ์‹œ ํŠ•๊ฒจ๋‚˜๊ฐ

  • ์ ‘๊ทผ ๋ถˆ๊ฐ€ ํŽ˜์ด์ง€ ์ถ”๊ฐ€ ํ›„ ํ•ธ๋“ค๋ง ์„ค์ •
...

@Configuration
@EnableWebSecurity // Spring Security ์ง€์›์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
@EnableMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
	...

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		...

        // ์ ‘๊ทผ ๋ถˆ๊ฐ€ ํŽ˜์ด์ง€
        http.exceptionHandling((exceptionHandling) ->
                exceptionHandling
                        .accessDeniedPage("/forbidden.html")); //์ ‘๊ทผ ๋ถˆ๊ฐ€ ํŽ˜์ด์ง€ URL ์„ค์ •

        return http.build();
    }
}

์ ‘๊ทผ ๋ถˆ๊ฐ€ ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ ํ™”๋ฉด