๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

์Šค๋งˆ์ผ๊ฒŒ์ดํŠธ ์„œ๋ฒ„๊ฐœ๋ฐœ์บ ํ”„ 4๊ธฐ

[์„œ๋ฒ„๊ฐœ๋ฐœ์บ ํ”„] ์ธ์ฆ ์„œ๋ฒ„ - Spring Security + JWT

 ์„œ๋ฒ„๊ฐœ๋ฐœ์บ ํ”„์˜ ๋‘ ๋ฒˆ์งธ ๊ฐœ์ธ ๊ณผ์ œ๋กœ ์ธ์ฆ ์„œ๋ฒ„ ๊ตฌ์ถ•์ด ์ฃผ์–ด์กŒ๋‹ค. ์ธ์ฆ ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋ฉฐ ์‚ฌ์šฉํ–ˆ๋˜ ๊ธฐ์ˆ ๊ณผ ์ด์Šˆ๋“ค์„ ์ •๋ฆฌํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค. ์Šคํ”„๋ง์—์„œ๋Š” ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ํ†ตํ•ด ๋ฆฌ์†Œ์Šค์˜ ์‚ฌ์šฉ์„ ์‰ฝ๊ฒŒ ์ปจํŠธ๋กค ํ•  ์ˆ˜ ์žˆ๋„๋ก Spring Security๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์ ์šฉํ•˜๋ฉฐ ๋ฐฑ๊ธฐ์„ ๋‹˜์˜ ์œ ํŠœ๋ธŒ ๊ฐ•์ขŒ์™€ happydaddy๋‹˜์˜ ํฌ์ŠคํŒ…์ด ํฐ ๋„์›€์ด ๋˜์—ˆ๋‹ค.

 ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์Šคํ”„๋ง์˜ Dispatcher Servlet ์•ž๋‹จ์— ํ•„ํ„ฐ๋ฅผ ๋“ฑ๋ก์‹œ์ผœ ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑˆ๋‹ค. ์ด ํ›„ ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ์— ๋Œ€ํ•ด ๊ถŒํ•œ์ด ์—†์„ ๊ฒฝ์šฐ ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์‹œํ‚จ๋‹ค. ๋‚˜๋Š” API ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ณ , ํ† ํฐ ๊ธฐ๋ฐ˜์œผ๋กœ ํ†ต์‹  ํ•  ๊ณ„ํš์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด์— ๋งž๋Š” ์‹œํ๋ฆฌํ‹ฐ ์„ค์ •๊ณผ ๋”๋ถˆ์–ด JWT๋ฅผ ์œ„ํ•œ ํ•„ํ„ฐ๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค.

 

Spring Security

 ์‹œํ๋ฆฌํ‹ฐ ์„ค์ •์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ๋‹ค์–‘ํ•œ ํ•„ํ„ฐ๋ฅผ ํ†ตํ•ด ์ธ์ฆ์„ ์ง„ํ–‰ํ•˜๋Š”๋ฐ, ๊ทธ ์ค‘ UsernamePasswordAuthenticationFilter๊ฐ€ ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์‹œํ‚ค๋Š” ์—ญํ• ์„ ํ•˜๋Š” ํ•„ํ„ฐ์ด๋‹ค. ๋”ฐ๋ผ์„œ ํ•ด๋‹น ํ•„ํ„ฐ์˜ ์•ž์— JWT๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ํ•„ํ„ฐ๋ฅผ ๋“ฑ๋กํ•ด์•ผํ–ˆ๋‹ค.

@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtUtil jwtUtil;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .csrf().disable()
                .cors()
                .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                    .antMatchers(
                            "/users/signin",
                            "/users/signup/**",
                            "/users/password/**",
                            "/exception/**"
                    ).permitAll()
                    .antMatchers("/admin/**").hasAuthority("ADMIN")
                    .anyRequest().authenticated()
                .and()
                    .exceptionHandling()
                    .accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()
                    .addFilterBefore(
                            new JwtAuthenticationFilter(jwtUtil),
                            UsernamePasswordAuthenticationFilter.class
                    );
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

 ๋˜ํ•œ ์šฐ๋ฆฌ๊ฐ€ ๋“ฑ๋กํ•œ JWT ํ•„ํ„ฐ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ, ํ•„ํ„ฐ์˜ ์ˆœ์„œ์ƒ Spring์˜ DispatcherServlet๊นŒ์ง€ ์˜ˆ์™ธ๊ฐ€ ๋„๋‹ฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ์œ„ํ•œ Handler๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค.
ํ—ค๋”์— ํ† ํฐ์ด ๋‹ด๊ฒจ์žˆ์ง€ ์•Š๊ฑฐ๋‚˜, ํ† ํฐ์ด ๋งŒ๋ฃŒ ๋˜๋Š” ์กฐ์ž‘๋œ ๊ฒฝ์šฐ ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ถ€๋ถ„์—์„œ ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋๋‚˜๊ฒŒ ๋˜๋ฏ€๋กœ, ์ด๋ฅผ ์žก์•„๋‚ด๊ธฐ ์œ„ํ•ด์„œ๋Š” SpringSecurity์—์„œ ์ œ๊ณตํ•˜๋Š” AuthenticationEntryPoint๋ฅผ ์ƒ์†๋ฐ›์•„ ์žฌ์ •์˜ ํ•ด์•ผํ–ˆ๋‹ค. ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ /exception/entrypoint๋กœ ํฌ์›Œ๋”ฉํ•˜๋„๋ก ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) 
    				throws IOException, ServletException {
                    
        response.sendRedirect("/exception/entrypoint");
        
    }
    
}

 

 ๋ฐ˜๋ฉด ํ—ค๋”์— ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์ด ๋‹ด๊ฒจ์žˆ์œผ๋‚˜, ์š”์ฒญ์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์—†๋Š” ๊ฒฝ์šฐ SpringSecurity์—์„œ ์ œ๊ณตํ•˜๋Š” AccessDeniedHandler๋ฅผ ์ƒ์†๋ฐ›์€ ํ›„, /exception/accessdenied๋กœ ํฌ์›Œ๋”ฉํ•˜๋„๋ก ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, 
    			AccessDeniedException accessDeniedException) throws IOException, ServletException {
        
        response.sendRedirect("/exception/accessdenied");
        
    }
}

 

JWT Authentication Filter

 JWT ์ธ์ฆ ํ•„ํ„ฐ๋Š” OncePerRequestFilter๋ฅผ ์ƒ์†๋ฐ›์•„ ์ž‘์„ฑํ–ˆ๋‹ค. ํ•ด๋‹น ํ•„ํ„ฐ์—์„œ ํ† ํฐ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ์ง„ํ–‰๋œ๋‹ค.

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
    					throws ServletException, IOException {
        
        String token = jwtUtil.getToken(request);

        if(token != null && jwtUtil.isValidToken(token)) {
            Authentication authentication = jwtUtil.getAuthentication(token);
            SecurityContext context = SecurityContextHolder.getContext();
            context.setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}

 

 ํ† ํฐ์ด ์œ ํšจํ•œ ํ† ํฐ์œผ๋กœ ํŒ๋ช…๋‚˜๋ฉด, ํ† ํฐ์—์„œ ์‚ฌ์šฉ์ž์˜ email๊ณผ role์„ ๊บผ๋‚ด์™€์„œ Authentication์„ ๋งŒ๋“  ํ›„, SecurityContext์— ํ•ด๋‹น Authentication์„ ์ „๋‹ฌํ•œ๋‹ค. ์ด Authentication๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ํ† ํฐ์„ ํ†ตํ•ด ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฆฌ์†Œ์Šค๊ฐ€ ์ •ํ•ด์ง„๋‹ค.

@Component
public class JwtUtil {

    public Authentication getAuthentication(String token) {
        Claims claims = getClaims(token);
        String email = claims.getSubject();
        int role = (int) claims.get("role");

        return new UsernamePasswordAuthenticationToken(
            email, 
            null, 
            Collections.singletonList(new SimpleGrantedAuthority(this.roles.get(role)))
        );
    }

    ...

}