Spring๐ŸŒธ

Spring Security - JWT๋ฅผ ์ด์šฉํ•œ ์ž๊ฒฉ ์ฆ๋ช… ๋ฐ ๊ฒ€์ฆ ๊ตฌํ˜„

Jeein0313 2023. 6. 7. 14:24

์ด์ „ ํฌ์ŠคํŒ…์—์„œ ํšŒ์› ๊ฐ€์ž… ์‹œ ๋“ฑ๋กํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ, ํŒจ์Šค์›Œ๋“œ๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์ธ ์ธ์ฆ์„ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋ฉด response header(Authorization, Refresh)๋ฅผ ํ†ตํ•ด JWT๋ฅผ ์ „๋‹ฌ๋ฐ›์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ JWT๋ฅผ ์ด์šฉํ•ด ์ž๊ฒฉ ์ฆ๋ช…์ด ํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ request ์ „์†ก ์‹œ, request header๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ๋ฐ›์€ JWT๋ฅผ ์„œ๋ฒ„ ์ธก์—์„œ ๊ฒ€์ฆํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด ๋ณด์ž.

 

 

 

1๏ธโƒฃ JWT ๊ฒ€์ฆ ํ•„ํ„ฐ ๊ตฌํ˜„

 

JWT๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ์ „์šฉ Security Filter๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ.

package com.codestates.todoapp.auth.filter;

import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;

//ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ „์†ก๋œ request header์— ํฌํ•จ๋œ JWT์— ๋Œ€ํ•ด ๊ฒ€์ฆ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” JwtVerificationFilter
public class JwtVerificationFilter extends OncePerRequestFilter { //request ๋‹น ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋Š” Security Filter๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
    private final JwtTokenizer jwtTokenizer; //JWT๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  Claims๋ฅผ ์–ป๋Š” ๋ฐ ์‚ฌ์šฉ๋จ.

    private final CustomAuthorityUtils authorityUtils;//JWT ๊ฒ€์ฆ์— ์„ฑ๊ณตํ•˜๋ฉด Authentication ๊ฐ์ฒด๋ฅผ ์ฑ„์œจ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋จ.

    public JwtVerificationFilter(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Map<String, Object> claims = verifyJws(request);
        setAuthenticationToContext(claims);

        //JWT์˜ ์„œ๋ช… ๊ฒ€์ฆ์— ์„ฑ๊ณตํ•˜๊ณ , Security Context์— Authentication์„ ์ €์žฅํ•œ ๋’ค์—๋Š” ๋‹ค์Œ Security Filter๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
        filterChain.doFilter(request, response);
    }

    //ํŠน์ • ์กฐ๊ฑด์— ๋ถ€ํ•ฉํ•˜๋ฉด(true์ด๋ฉด) ํ•ด๋‹น Filter์˜ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ  ๋‹ค์Œ Filter๋กœ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ํ•ด์ค€๋‹ค.
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException{
        //JWT๊ฐ€ Authorization header์— ํฌํ•จ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด JWT ์ž๊ฒฉ ์ฆ๋ช…์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์š”์ฒญ์ด๋ผ๊ณ  ํŒ๋‹จํ•˜๊ณ  ๋‹ค์Œ Filter๋กœ ์ฒ˜๋ฆฌ๋ฅผ ๋„˜๊ธด๋‹ค.
        String authorization = request.getHeader("Authorization");
        return authorization == null || !authorization.startsWith("Bearer");
    }

    //JWT๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” private ๋ฉ”์„œ๋“œ
    private Map<String, Object> verifyJws(HttpServletRequest request) {
        //request์˜ header์—์„œ JWT๋ฅผ ์–ป๊ณ  ์žˆ๋‹ค.
        String jws = request.getHeader("Authorization").replace("Bearer ", "");

        //JWT ์„œ๋ช…์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ Secret Key๋ฅผ ์–ป๋Š”๋‹ค.
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
        //JWT์—์„œ Claims๋ฅผ ํŒŒ์‹ฑ -> ์ •์ƒ์ ์œผ๋กœ ํŒŒ์‹ฑ๋˜๋ฉด ์„œ๋ช… ๊ฒ€์ฆ ์—ญ์‹œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์„ฑ๊ณตํ•œ ๊ฒƒ.
        Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();

        return claims;
    }

    //Authentication ๊ฐ์ฒด๋ฅผ SecurityContext์— ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ priavte ๋ฉ”์„œ๋“œ
    private void setAuthenticationToContext(Map<String, Object> claims){
        String username = (String) claims.get("username");
        List<GrantedAuthority> authorities = authorityUtils.createAuthorities((List) claims.get("roles"));
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(authentication);

    }
}

 

 

 

2๏ธโƒฃ SecurityConfiguration ์„ค์ • ์—…๋ฐ์ดํŠธ

์„ธ์…˜ ์ •์ฑ… ์„ค์ •์„ ์ถ”๊ฐ€, JwtVerificationFilter ์ถ”๊ฐ€

package com.codestates.todoapp.config;

import com.codestates.todoapp.auth.filter.JwtAuthenticationFilter;
import com.codestates.todoapp.auth.filter.JwtVerificationFilter;
import com.codestates.todoapp.auth.handler.MemberAuthenticationFailureHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationSuccessHandler;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfiguration {
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;


    public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .headers().frameOptions().sameOrigin()
                .and()
                .csrf().disable()
                .cors(Customizer.withDefaults())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //์„ธ์…˜์„ ์ƒ์„ฑํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•œ๋‹ค.
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .apply(new CustomFilterConfigurer())//์ปค์Šคํ„ฐ๋งˆ์ด์ง•๋œ Configuration ์ถ”๊ฐ€
                .and()
                .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll());
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
        UrlBasedCorsConfigurationSource  source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    //JwtAuthenticationFilter๋ฅผ ๋“ฑ๋กํ•˜๋Š” ์—ญํ• 
    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity>{
        @Override
        public void configure(HttpSecurity builder) throws Exception{
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
            jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
            jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
            jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());

            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);


            builder
                    .addFilter(jwtAuthenticationFilter) //JwtAuthenticationFilter๋ฅผ Spring Security Filter Chain์— ์ถ”๊ฐ€ํ•œ๋‹ค.
                    .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); //JwtAuthenticationFilter์—์„œ ๋กœ๊ทธ์ธ ์ธ์ฆ์„ ์„ฑ๊ณตํ•œ ํ›„ ๋ฐœ๊ธ‰๋ฐ›์€ JWT๊ฐ€ ํด๋ผ์ด์–ธํŠธ์˜ request header์— ํฌํ•จ๋˜์–ด ์žˆ์„ ๊ฒฝ์šฐ์—๋งŒ ๋™์ž‘ํ•œ๋‹ค.

        }
    }

}

 

 

๋กœ๊ทธ์ธ ์ธ์ฆ ํ›„, JWT๋„ ์ž˜ ๋ฐœ๊ธ‰๋˜๊ณ  JWT๋ฅผ ์ด์šฉํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์˜ ์ž๊ฒฉ ์ฆ๋ช…์— ๋Œ€ํ•œ ๊ฒ€์ฆ๊นŒ์ง€ ์ž˜ ๋˜๋Š” ๊ฒƒ ํ™•์ธํ–ˆ๋‹ค.

 

๊ทธ๋Ÿฐ๋ฐ Spring Security ์ชฝ์—์„œ ์„œ๋ฒ„ ์ธก ๋ฆฌ์†Œ์Šค์— ์ ์ ˆํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ์„ค์ •์„ ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์•„๋ฌด๋ฆฌ JWT๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์˜ ์ž๊ฒฉ ์ฆ๋ช…์ด ํ™•์ธ๋œ๋‹ค๊ณ  ํ•˜๋”๋ผ๋„ ๊ทธ ์˜๋ฏธ๊ฐ€ ํ‡ด์ƒ‰๋œ๋‹ค.

 

JWT๋ฅผ ์ด์šฉํ•œ ์ž๊ฒฉ ์ฆ๋ช…์ด๋ผ๋Š” ์˜๋ฏธ์—๋Š” ํŠน์ • ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ์ ์ ˆํ•œ ๊ถŒํ•œ์„ ๊ฐ€์กŒ๋Š”์ง€๋ฅผ ํŒ๋‹จํ•ด์•ผ ํ•œ๋‹ค๋Š” ์˜๋ฏธ๋„ ํฌํ•จํ•˜๊ณ  ์žˆ๋‹ค.

 

ํ˜„์žฌ๊นŒ์ง€ SecurityConfiguration์—๋Š” ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ ์—ฌ๋ถ€์— ์ƒ๊ด€์—†์ด ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •๋œ ์ƒํƒœ์ด๋‹ค.

 

 

 

3๏ธโƒฃ MemberController๋ฅผ ํ†ตํ•ด ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ๋ถ€์—ฌ

package com.codestates.todoapp.config;

import com.codestates.todoapp.auth.filter.JwtAuthenticationFilter;
import com.codestates.todoapp.auth.filter.JwtVerificationFilter;
import com.codestates.todoapp.auth.handler.MemberAuthenticationFailureHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationSuccessHandler;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfiguration {
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;


    public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .headers().frameOptions().sameOrigin()
                .and()
                .csrf().disable()
                .cors(Customizer.withDefaults())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //์„ธ์…˜์„ ์ƒ์„ฑํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•œ๋‹ค.
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .apply(new CustomFilterConfigurer())//์ปค์Šคํ„ฐ๋งˆ์ด์ง•๋œ Configuration ์ถ”๊ฐ€
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers(HttpMethod.POST, "/members").permitAll()
                        .antMatchers(HttpMethod.PATCH, "/members/**").hasRole("USER")
                        .antMatchers(HttpMethod.GET, "/members").hasRole("ADMIN")
                        .antMatchers(HttpMethod.GET, "/members/**").hasAnyRole("USER", "ADMIN")
                        .antMatchers(HttpMethod.DELETE, "/members").hasRole("ADMIN")
                        .antMatchers(HttpMethod.DELETE, "/members/**").hasAnyRole("USER", "ADMIN")
                        .anyRequest().permitAll()
                );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
        UrlBasedCorsConfigurationSource  source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    //JwtAuthenticationFilter๋ฅผ ๋“ฑ๋กํ•˜๋Š” ์—ญํ• 
    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity>{
        @Override
        public void configure(HttpSecurity builder) throws Exception{
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
            jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
            jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
            jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());

            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);


            builder
                    .addFilter(jwtAuthenticationFilter) //JwtAuthenticationFilter๋ฅผ Spring Security Filter Chain์— ์ถ”๊ฐ€ํ•œ๋‹ค.
                    .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); //JwtAuthenticationFilter์—์„œ ๋กœ๊ทธ์ธ ์ธ์ฆ์„ ์„ฑ๊ณตํ•œ ํ›„ ๋ฐœ๊ธ‰๋ฐ›์€ JWT๊ฐ€ ํด๋ผ์ด์–ธํŠธ์˜ request header์— ํฌํ•จ๋˜์–ด ์žˆ์„ ๊ฒฝ์šฐ์—๋งŒ ๋™์ž‘ํ•œ๋‹ค.

        }
    }

}

 

 

 

์ด์ œ ์กฐ๊ธˆ ๋” ๊น”๋”ํ•œ ๋งˆ๋ฌด๋ฆฌ๋ฅผ ์œ„ํ•ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ ๊ด€๋ จ๋œ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด ๋ณด์ž.

 

 

 

4๏ธโƒฃ JwtVerificationFilter์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€

JwtVerificationFilter์˜ ๊ฒฝ์šฐ, ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌ๋ฐ›์€ JWT์˜ Claims๋ฅผ ์–ป๋Š” ๊ณผ์ •์—์„œ ๋‚ด๋ถ€์ ์œผ๋กœ JWT์— ๋Œ€ํ•œ ์„œ๋ช…(Signature)์„ ๊ฒ€์ฆํ•œ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ํ˜„์žฌ JwtVerificationFilter์—์„œ๋Š” JWT์— ๋Œ€ํ•œ ์„œ๋ช… ๊ฒ€์ฆ์— ์‹คํŒจํ•  ๊ฒฝ์šฐ throw ๋˜๋Š” SignatureException์— ๋Œ€ํ•ด์„œ ์–ด๋–ค ์ฒ˜๋ฆฌ๋„ ํ•˜์ง€ ์•Š๊ณ  ์žˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  JWT๊ฐ€ ๋งŒ๋ฃŒ๋  ๊ฒฝ์šฐ, ๋ฐœ์ƒํ•˜๋Š” ExpiredJwtException์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋„ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๊ณ  ์žˆ๋‹ค.

 

JWT ๊ฒ€์ฆ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” Exception์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง์„ JwtVerificationFilter์— ์ถ”๊ฐ€ํ•ด๋ณด๋„๋ก ํ•˜์ž.

package com.codestates.todoapp.auth.filter;
import io.jsonwebtoken.security.SignatureException;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;

//ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ „์†ก๋œ request header์— ํฌํ•จ๋œ JWT์— ๋Œ€ํ•ด ๊ฒ€์ฆ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” JwtVerificationFilter
public class JwtVerificationFilter extends OncePerRequestFilter { //request ๋‹น ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋Š” Security Filter๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
    private final JwtTokenizer jwtTokenizer; //JWT๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  Claims๋ฅผ ์–ป๋Š” ๋ฐ ์‚ฌ์šฉ๋จ.

    private final CustomAuthorityUtils authorityUtils;//JWT ๊ฒ€์ฆ์— ์„ฑ๊ณตํ•˜๋ฉด Authentication ๊ฐ์ฒด๋ฅผ ์ฑ„์œจ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋จ.

    public JwtVerificationFilter(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

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

        //์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์ถ”๊ฐ€๋จ. ์ถ”๊ฐ€๋œ ์• ํŠธ๋ฆฌ๋ทฐํŠธ๋Š” AthenticationEntryPoint์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
        try {
            Map<String, Object> claims = verifyJws(request);
            setAuthenticationToContext(claims);
        } catch (SignatureException se) {
            request.setAttribute("exception", se);
        } catch (ExpiredJwtException ee) {
            request.setAttribute("exception", ee);
        } catch (Exception e) {
            request.setAttribute("exception", e);
        }

        //JWT์˜ ์„œ๋ช… ๊ฒ€์ฆ์— ์„ฑ๊ณตํ•˜๊ณ , Security Context์— Authentication์„ ์ €์žฅํ•œ ๋’ค์—๋Š” ๋‹ค์Œ Security Filter๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
        filterChain.doFilter(request, response);
    }

    //ํŠน์ • ์กฐ๊ฑด์— ๋ถ€ํ•ฉํ•˜๋ฉด(true์ด๋ฉด) ํ•ด๋‹น Filter์˜ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ  ๋‹ค์Œ Filter๋กœ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ํ•ด์ค€๋‹ค.
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        //JWT๊ฐ€ Authorization header์— ํฌํ•จ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด JWT ์ž๊ฒฉ ์ฆ๋ช…์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์š”์ฒญ์ด๋ผ๊ณ  ํŒ๋‹จํ•˜๊ณ  ๋‹ค์Œ Filter๋กœ ์ฒ˜๋ฆฌ๋ฅผ ๋„˜๊ธด๋‹ค.
        String authorization = request.getHeader("Authorization");
        return authorization == null || !authorization.startsWith("Bearer");
    }

    //JWT๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” private ๋ฉ”์„œ๋“œ
    private Map<String, Object> verifyJws(HttpServletRequest request) {
        //request์˜ header์—์„œ JWT๋ฅผ ์–ป๊ณ  ์žˆ๋‹ค.
        String jws = request.getHeader("Authorization").replace("Bearer ", "");

        //JWT ์„œ๋ช…์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ Secret Key๋ฅผ ์–ป๋Š”๋‹ค.
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
        //JWT์—์„œ Claims๋ฅผ ํŒŒ์‹ฑ -> ์ •์ƒ์ ์œผ๋กœ ํŒŒ์‹ฑ๋˜๋ฉด ์„œ๋ช… ๊ฒ€์ฆ ์—ญ์‹œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์„ฑ๊ณตํ•œ ๊ฒƒ.
        Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();

        return claims;
    }

    //Authentication ๊ฐ์ฒด๋ฅผ SecurityContext์— ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ priavte ๋ฉ”์„œ๋“œ
    private void setAuthenticationToContext(Map<String, Object> claims) {
        String username = (String) claims.get("username");
        List<GrantedAuthority> authorities = authorityUtils.createAuthorities((List) claims.get("roles"));
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(authentication);

    }

}

์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง์€ ๊ฐ„๋‹จํ•˜๋‹ค.

try-catch๋ฌธ์œผ๋กœ ํŠน์ • ์˜ˆ์™ธ ํƒ€์ž…์˜ Exception์ด catch ๋˜๋ฉด ํ•ด๋‹น Exception์„ catch ํ•œ ํ›„์— Exception์„ ๋‹ค์‹œ throw ํ•œ๋‹ค๋“ ์ง€ ํ•˜๋Š” ์ฒ˜๋ฆฌ๊ฐ€ ์•„๋‹Œ, ๋‹จ์ˆœํžˆ request.setAttribute()๋ฅผ ์„ค์ •ํ•˜๋Š” ์ผ๋ฐ–์— ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

์ด๋Ÿฐ์‹์œผ๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋ฉด, ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ SecurityContext์— Authentication ๊ฐ์ฒด(ํด๋ผ์ด์–ธํŠธ ์ •๋ณด)๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค.

SecurityContext์— ํด๋ผ์ด์–ธํŠธ์˜ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ๋‹ค์Œ Security Filter ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋‹ค๋ณด๋ฉด ๊ฒฐ๊ตญ์—๋Š” Filter ๋‚ด๋ถ€์—์„œ AuthenticationException์ด ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜๊ณ , ์ด AuthenticationException์€ ๋ฐ”๋กœ ์•„๋ž˜์— ์„ค๋ช…ํ•˜๋Š” AuthenticationEntryPoint๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋œ๋‹ค.

 

 

 

5๏ธโƒฃ AuthenticationEntryPoint ๊ตฌํ˜„

AuthenticationEntryPoint๋Š” SignatureException, ExpiredException ๋“ฑ Exception ๋ฐœ์ƒ์œผ๋กœ ์ธํ•ด SecurityContext์— Authentication์ด ์ €์žฅ๋˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ๋“ฑ AuthenticationException์ด ๋ฐœ์ƒํ•  ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ ๊ฐ™์€ ์—ญํ• ์„ ํ•œ๋‹ค.

package com.codestates.todoapp.auth.handler;

import com.codestates.todoapp.auth.utils.ErrorResponder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//AuthenticationException์ด ๋ฐœ์ƒํ•  ๋•Œ์˜ ํ•ธ๋“ค๋Ÿฌ ์—ญํ• ์„ ํ•œ๋‹ค.
@Slf4j
@Component
public class MemberAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Exception exception = (Exception) request.getAttribute("exception");
        ErrorResponder.sendErrorResponse(response, HttpStatus.UNAUTHORIZED);
        logExceptionMessage(authException, exception);
    }

    //์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ
    private void logExceptionMessage(AuthenticationException authException, Exception exception) {
        String message = exception != null ? exception.getMessage() : authException.getMessage();
        log.warn("Unauthorized error happend: {}", message);
    }
}

 

 

 

6๏ธโƒฃ ErrorResponder(ErrorResponse๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค)

 

ErrorResponse๋ฅผ ์ถœ๋ ฅ ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

package com.codestates.todoapp.auth.utils;

import com.codestates.todoapp.response.ErrorResponse;
import com.google.gson.Gson;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//ErrorResponse๋ฅผ ์ถœ๋ ฅ ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•˜๋Š” ํด๋ž˜์Šค.
public class ErrorResponder {
    public static void sendErrorResponse(HttpServletResponse response, HttpStatus status) throws IOException {
        Gson gson = new Gson();
        ErrorResponse errorResponse = ErrorResponse.of(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(status.value());
        response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
    }
}

 

 

 

7๏ธโƒฃ AccessDeniedHandler ๊ตฌํ˜„

 

์ธ์ฆ์—๋Š” ์„ฑ๊ณตํ–ˆ์œผ๋‚˜ ํ•ด๋‹น ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์—†์œผ๋ฉด ํ˜ธ์ถœ๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ

package com.codestates.todoapp.auth.handler;

import com.codestates.todoapp.auth.utils.ErrorResponder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


//ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ๊ถŒํ•œ์ด ์—†์„ ๊ฒฝ์šฐ ํ˜ธ์ถœ๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ
@Slf4j
@Component
public class MemberAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ErrorResponder.sendErrorResponse(response, HttpStatus.FORBIDDEN);
        log.warn("Forbidden error happend: {}", accessDeniedException.getMessage());
    }
}

 

 

 

8๏ธโƒฃ SecurityCofiguration์— AuthenticationEntryPoint ๋ฐ AccessDeniedHandler ์ถ”๊ฐ€

package com.codestates.todoapp.config;

import com.codestates.todoapp.auth.filter.JwtAuthenticationFilter;
import com.codestates.todoapp.auth.filter.JwtVerificationFilter;
import com.codestates.todoapp.auth.handler.MemberAccessDeniedHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationEntryPoint;
import com.codestates.todoapp.auth.handler.MemberAuthenticationFailureHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationSuccessHandler;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import com.codestates.todoapp.member.entity.Member;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfiguration {
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;


    public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .headers().frameOptions().sameOrigin()
                .and()
                .csrf().disable()
                .cors(Customizer.withDefaults())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //์„ธ์…˜์„ ์ƒ์„ฑํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•œ๋‹ค.
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .exceptionHandling()
                .authenticationEntryPoint(new MemberAuthenticationEntryPoint()) //์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
                .accessDeniedHandler(new MemberAccessDeniedHandler()) //์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
                .and()
                .apply(new CustomFilterConfigurer())//์ปค์Šคํ„ฐ๋งˆ์ด์ง•๋œ Configuration ์ถ”๊ฐ€
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers(HttpMethod.POST, "/members").permitAll()
                        .antMatchers(HttpMethod.PATCH, "/members/**").hasRole("USER")
                        .antMatchers(HttpMethod.GET, "/members").hasRole("ADMIN")
                        .antMatchers(HttpMethod.GET, "/members/**").hasAnyRole("USER", "ADMIN")
                        .antMatchers(HttpMethod.DELETE, "/members").hasRole("ADMIN")
                        .antMatchers(HttpMethod.DELETE, "/members/**").hasAnyRole("USER", "ADMIN")
                        .anyRequest().permitAll()
                );


        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
        UrlBasedCorsConfigurationSource  source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    //JwtAuthenticationFilter๋ฅผ ๋“ฑ๋กํ•˜๋Š” ์—ญํ• 
    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity>{
        @Override
        public void configure(HttpSecurity builder) throws Exception{
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
            jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
            jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
            jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());

            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);


            builder
                    .addFilter(jwtAuthenticationFilter) //JwtAuthenticationFilter๋ฅผ Spring Security Filter Chain์— ์ถ”๊ฐ€ํ•œ๋‹ค.
                    .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); //JwtAuthenticationFilter์—์„œ ๋กœ๊ทธ์ธ ์ธ์ฆ์„ ์„ฑ๊ณตํ•œ ํ›„ ๋ฐœ๊ธ‰๋ฐ›์€ JWT๊ฐ€ ํด๋ผ์ด์–ธํŠธ์˜ request header์— ํฌํ•จ๋˜์–ด ์žˆ์„ ๊ฒฝ์šฐ์—๋งŒ ๋™์ž‘ํ•œ๋‹ค.

        }
    }

}

 

 

์ด์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•œ ํ›„, ์œ ํšจํ•˜์ง€ ์•Š์€ JWT ํ† ํฐ ๋˜๋Š” ๋งŒ๋ฃŒ๋œ JWT ๋ฅผ Authorization header์— ์ถ”๊ฐ€ํ•ด์„œ request๋ฅผ ์ „์†กํ•˜๋ฉด ์•„๋ž˜์˜ response๋ฅผ ์ „์†กํ•œ๋‹ค.

 

๋˜ํ•œ ๊ถŒํ•œ์ด ์—†๋Š” ๋ฆฌ์†Œ์Šค์— request๋ฅผ ์ „์†ก ์‹œ, ์•„๋ž˜์™€ ๊ฐ™์€ response๋ฅผ ์ „์†กํ•œ๋‹ค.

 

 

์ด๊ฒƒ์œผ๋กœ JWT์— ๋Œ€ํ•œ ๊ธฐ๋Šฅ์ด ์™„์„ฑ๋˜์—ˆ๋‹ค.

 

 

 

ใ…‹ใ…‹ใ…‹ ํ•œ๋ฒˆ์œผ๋กœ๋Š” ์–ด๋ฆผ๋„ ์—†์ง€!!  ์ง„ํ–‰ ์ค‘์ธ ํ”„๋กœ์ ํŠธ์— ๋‹ค์‹œ ์ ์šฉํ•ด ๋ณด๋ฉด์„œ ์ „์ฒด ํ๋ฆ„์„ ์ตํ˜€์•ผ๊ฒ ๋‹ค๐Ÿ˜