Spring Secuirty 39 React - Spring Security JWT 를 이용한 JWT 토큰 유효성 검증
포스트
취소

Spring Secuirty 39 React - Spring Security JWT 를 이용한 JWT 토큰 유효성 검증

소스 전체

https://gitlab.com/kimdongy1000/public_project_amadeus/-/tree/main?ref_type=heads

해당 소스는 민감한 정보를 제외한 순수 코드입니다 사용하실려면 application.yml 에 자신이 필요한 정보를 기입하시면 사용 가능합니다 해당 글을 적는부분과 소스의 올라간 부분은 상당히 많이 다릅니다

networkApiCall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export async function networkApiCall(api , method , param){

    const headers = new Headers();
    const project_name = f1().project_name;
   
    headers.append("Content-Type" , "application/json");
   

    const access_token = localStorage.getItem(`${project_name}_ACCESS_TOKEN`);
    if(access_token) headers.append("Authorization" , `Bearer ${access_token}`);

    let options = {

        headers : headers,
        url : f1().api_base_url + api ,
        method : method
    }

    if(!("get"== method.toLowerCase("get")) && param) options.body = JSON.stringify(param);

    return await fetch(options.url , options).then( (response) => {

       

        if(response.status === 401){

            const project_name = f1().project_name;
            localStorage.removeItem(`${project_name}_ACCESS_TOKEN`)
            window.location.href = '/login';
           
           
        }else {

            return response.json();
        }
       
       
   
    }).then( (response2) => {


        if(response2.HttpStatus === 500){        
            throw new Error(response2.errorMsg);
        }
               
        return response2
    })
}

제가 사용하는 React 에서 공통 api 호출입니다 여기서 localStorage JWT 를 가져와서 해당 토큰이 존재하면 헤더에 Authorization 에 Bearer 에 저장하고 항상 요청시 날아가게 만들었습니다

1
2
3
const access_token = localStorage.getItem(`${project_name}_ACCESS_TOKEN`);
if(access_token) headers.append("Authorization" , `Bearer ${access_token}`);

그 부분이 이 부분에 해당하게 됩니다

CustomJwtAuthenticationFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class CustomJwtAuthenticationFilter extends OncePerRequestFilter {

    private CustomJWTParser jwtParser;

    public CustomJwtAuthenticationFilter(CustomJWTParser jwtParser) {
        this.jwtParser = jwtParser;
    }

    @Autowired
    private Gson gson;



    private static final String[] ALLOW_URL = {
                "/user/register", "/user/login"
                , "/oauth2/google/oauth2_login_address"
                , "/oauth2_google_login"
                , "/oauth2/naver/oauth2_login_address"
                , "/oauth2_naver_login"
    };

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

        String authorizationHeader = request.getHeader("Authorization");
        final String url = request.getRequestURI();
        String method = request.getMethod();

        List<String> allow_url_matcher = Arrays.stream(ALLOW_URL).filter(x -> url.equals(x)).collect(Collectors.toList());


        if (allow_url_matcher.size() > 0 || "OPTIONS".equals(method.toUpperCase())) {

            filterChain.doFilter(request, response);   

        } else {

            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {

                String accessToken = authorizationHeader.substring(7); // "Bearer " 다음의 값만 추출

                try {

                    /*

                        1. JWT 토큰을 분석해서 유효성 검증
                        2. 해당 토큰으로 만들어진 것을 바탕으로 시큐리티 contextHolder 에 setAuthentication 에 저장을 하게 됩니다
                        3. 그리고 인증이 완료 되었으면 다음 filter 로 이동을 합니다 

                    */

                    Authentication authenticationToken = jwtParser.jwtParse(accessToken); 
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken); 
                    filterChain.doFilter(request, response);

                } catch (Exception e) {

                    /*
                        토큰 검증 중에 에러가 발생하면 토큰 일치 여부와 관계 없이 401 에러를 뿜고 return 합니다
                    */


                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("UnAuthorized User");
                }
            } else {

                /*
                    토큰이 존재 하지 않아도 마찬가지로 401 에러를 return 하게 됩니다 
                */

                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.getWriter().write("UnAuthorized User");

            }
        }
    }
}

이 커스텀Filter 로 JWT 를 파싱해서 다음 필터로 넘길지 아니면 401 에러를 발생시킬지 결정하는 부분입니다

CustomJWTParser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Component
public class CustomJWTParser {

    @Autowired
    private JWK jwk;

    @Value("${spring.security.access_tokenExpireTime}")
    private Long access_tokenExpireTime;

    public Authentication jwtParse(String accessToken) throws ParseException, JOSEException {

        /*
        
            1. JWT_TOKEN 을 통해서 SignedJWT 객체를 먼저 만들어냅니다
            2. 앞전에 만든 JWK(JAVA_WEB_KEY) 를 통해서 RSAVerifier 객체를 만들고 이때 객체는 RSA 의 pubKey 로 만들어줍니다
            3. RSA 같은 비대칭키는 비밀키로 암호화 하고 공개키로 복호화를 합니다

        */

        SignedJWT signedJWT = SignedJWT.parse(accessToken); 
        RSASSAVerifier rsassaVerifier = new RSASSAVerifier((RSAKey) jwk.toPublicJWK());  

        /*verify 함수를 통해서 서명이 먼저 일치하는지 판단 */
        boolean verify = signedJWT.verify(rsassaVerifier); 


        if(!verify){
            throw new BadCredentialsException("잘못된 서명입니다.");
        }

        /*
            서명이 일치하면 일단 JWTClaimsSet 분리를 시작하고 토큰의 유효시간을 검증합니다 
        */

        JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();  

        Date jwt_expire_time =  jwtClaimsSet.getExpirationTime();
        Date today = new Date();
        Date today_add_expire_time = new Date(today.getTime());

        /*
            서명이 일치하면 이제는 토큰의 만료시간을 체크합니다 토큰이 만료시간이 끝아면 Exception 을 던지고 끝이납니다
            사실 이때는 BadCredentialsException 보다는 다른 RunTimeException 을 추천합니다 
        */

        if(jwt_expire_time.getTime() <= today_add_expire_time.getTime()){ // 
            throw new BadCredentialsException("시간이 만료된 토큰입니다");
        }

        /*

        만료시간 유효성까지 검증이 완료되면 새로운 UserDetails 만들어서 Authentication 을 만들고 이를 return 을 하게 됩니다
        그러면 시큐리티 컨텍스트 안에는 JWT 로 만들어진 UserDetails 정보가 만들어서 들어가게 됩니다 

        */

        String username = (String)jwtClaimsSet.getClaim("username");
        List<String> authority = (List<String>) jwtClaimsSet.getClaim("authority");


        List<GrantedAuthority> array_authority = authority.stream().map(x -> new SimpleGrantedAuthority(x)).collect(Collectors.toList());

        UserDetails userDetails = new User(username , UUID.randomUUID().toString() , array_authority);
        Authentication authenticationToken = new UsernamePasswordAuthenticationToken(userDetails , null , array_authority);

        return authenticationToken;
    }
}

이 부분은 filter 에서 넘겨받은 access_token 으로 시큐리티컨텍스트를 만드는 과정입입니다

SecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Autowired
private CustomJWTParser jwtParser;


@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{

    httpSecurity.authorizeRequests().antMatchers(   "/user/register" ,
                                                    "/user/login" ,
                                                    "/oauth2/google/oauth2_login_address" ,
                                                    "/oauth2_google_login" ,
                                                    "/oauth2/naver/oauth2_login_address" ,
                                                    "/oauth2_naver_login"
                                                ).permitAll();
    httpSecurity.authorizeRequests().anyRequest().authenticated();

    httpSecurity.csrf().disable();

    httpSecurity.formLogin().disable();
    httpSecurity.httpBasic().disable();

    httpSecurity.addFilterAfter(new CustomJwtAuthenticationFilter(jwtParser) , CorsFilter.class);


    return httpSecurity.build();
}

그리고 Filter 를 CorsFilter 다음으로 설정합니다 그러면 우리는 이 과정으로 인해서 Front 에서 넘어오는 발급된 JWT 토큰을 가지고 새로운 Authentication 을 만들어서 인증을 완료하게 된것입니다