Home [Spring Security JWT] JWT 검증 필터
Post
Cancel

[Spring Security JWT] JWT 검증 필터

이 게시글은 유튜브 개발자 유미님의 무료강의 스프링 시큐리티 JWT 11 : JWT 검증 필터 을 수강하며 공부한 내용을 담고있습니다.
이 게시글은 공부한 내용을 제가 보기 편하게 정리한 내용이므로 정확한 정보는 위 영상을 시청해주세요.

이 게시글은 기본적인 Spring 기본 실습을 해봤다는 가정하에 기초적인 설명은 생략되어 있습니다.
또한 실습을 따라가며 기본적인 JWT 사용방법을 학습하기에 상세한 원리와 설명은 없는 경우가 있습니다.

개요

앞서 JWT 를 발급하고 검증하는 메서드를 만들고, 발급하는 로직을 작성했다.
발급받은 JWT 를 복사해서 Header 에 추가한 뒤 / 경로로 접근을 시도하면 정상적으로 접근이 된다.
여기는 별다른 JWT 검증을 하지 않기 때문이다(JWT 없어도 접근이됨). 하지만 /admin 경로는 기본적으로 JWT 값을 검증 후 통과해야 접근할 수 있기 때문에 403 응답을 받게된다.
JWT 검증 필터를 추가 후 /admin 경로에 접근을 시도해본다.

JWT Filter

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
public class JWTFilter extends OncePerRequestFilter {
    JWTUtil jwtUtil;

    public JWTFilter(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // JWT 값을 검증할 것이므로 request 에서 JWT 값을 꺼낸다.
        String authorization = request.getHeader("Authorization"); // Bearer + JWT값 을 Header 에서 꺼냄.

        // 꺼낸 값을 검증한다.
        // bearer 가 붙지 않으면 JWT 가 아니거나 잘못된 값이고, null 이면 JWT 를 발급받지 못했다는의미.
        if(authorization == null || !authorization.startsWith("Bearer ")) {
            System.out.println("token null");
            // 연결된 다음 필터에게 request, response 값을 넘겨주고,
            filterChain.doFilter(request, response);
            // 메서드 종료
            return ;
        }

        // Bearer + JWT값 을 공백을 기준으로 쪼갠 후 JWT값만 취한다.
        String token = authorization.split(" ")[1];

        // JWT 에 담긴 username 과 role 값을 검증하기 전에 JWT 유효기간이 남았는지 확인한다.
        try {
            // Token 만료시 ExpiredJwtException 예외가 발생한다.
            jwtUtil.isExpired(token);
        } catch(ExpiredJwtException e) {
            System.out.println("token 이 만료되었다.");
            // token 이 만료되었으면 다름에 수행될 filter 에게 매개변수를 넘기고 종료한다.
            filterChain.doFilter(request,response);
            return;
        }
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);
        UserEntity userEntity = new UserEntity();
        userEntity.setUsername(username);
        userEntity.setPassword("temp"); // 아무 값이나 담는다.
        userEntity.setRole(role);

        CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
        Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}
  1. JWTFilter 클래스를 생성한다.
  2. 요청에 대해 한번만 동작하는 Once Per Request Filter 를 상속 받는다.
  3. doFilterInternal 추상메서드를 Override 한다.
  4. request 의 헤더에서 JWT 값을 추출한다.
  5. JWT 값이 없거나 Bearer 로 시작하지 않으면 JWT 가 없는걸로 간주하고 끝낸다.
  6. JWT 값에서 Bearer 를 제거한다.
  7. JWT 의 유효기간이 남았는지 검증한다.
  8. JWT 에 있는 username 과 role 값으로 UserEntity 객체를 만들고 그 값으로 CustomUserDetails 객체를 만든다.
  9. CustomUserDetails 객체로 Authentication 객체를 만든다.
  10. 생성한 Authentication 객체를 SecurityContextHolder 에 등록한다.
  11. 메서드를 종료한다.(다음 필터로 넘어간다.)

filterChain.doFilter

filterChain.doFilter(request, response); 는 다음 필터로 넘어가는 역할을 한다.

인증은 어떻게 수행이 되지?

jwtUtil.isExpired() 에서 수행이된다.
JWTUtil 클래스를 다시보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class JWTUtil {
    
    // 생략

    public String getUsername(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
    }

    public String getRole(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
    }
    public Boolean isExpired(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

}

getUsername(), getRole(), isExpired() 을 보면 공통적으로 Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token) 가 있다.
해당 부분에서 token 이 변조되지 않았는지 검증을 한다.

SecurityContextHolder 에 등록하는 이유

사용자가 특정 요청을 수행하고 응답해줄 때 까지 다시 인증하지 않도록 하기 위함이다.
SecurityContextHolder 에 정보를 등록하면 response 를 할 때 까지 재인증을 하지 않아도 된다.

‘등록’을 한다는 개념때문에 왜 stateless 지? 했다.
state 를 저장을 하는 시간이 request 부터 response 까지 이기 때문이다.
예를 들어 stateful 한 방식인 session 은 request 에 대한 response 를 한 뒤에도 서버는 client 의 state 를 알고 있다.
하지만 SecurityContextHolder 에 등록된 정보는 client 에게 response 하면 사라진다.
SecurityContextHolder 에 등록하는 이유는 위에서 말했듯이 JWT 를 여러번 인증할 필요없게 해준다.
JWT 를 또 인증해야할 일이 있을 때, 넌 아까 인증했던 녀석이구나 인증할 필요 없어~ 해주는 역할이다.
request -> JWT 인증 -> 특정 로직 -> JWT 인증 -> 특정 로직2 -> JWT 인증 -> 특정 로직3 -> response 이라는게 있다고 상상해보자. 이럴 때 SecurityContextHolder 에 등록되어있으면 JWT 인증을 스킵할 수 있다.
(일부 오개념이 있는데, 지금 수준에서는 이렇게 쉽게 이해하고 넘어간다..)

JWT Filter 등록

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    // 생략
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        //생략

        // 6. 필터 등록
        // addFilterBefore(등록할 필터, 기준이 되는 필터)
        http.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

LoginFilter 를 등록했을 때처럼 SecurityConfig 의 filterChain 메소드에 JWTFilter를 등록하면 된다.
등록 위치는 LoginFilter 앞에 등록할 것이므로 addFilterBefore() 를 사용한다.

테스트

POST localhost:8080/login 경로로 요청을 보내 JWT 를 발급 받은 뒤 JWT 를 복사한다.
Header 에 JWT 값을 넣고 JWT 가 만료되기 전에 GET localhost:8080/admin 경로로 요청을 보낸다.
그럼 200 응답을 받을 수 있다.

만약 제대로 작성한 것 같은데 403 응답을 받는다면, LoginFilter 의 createJwt() 메서드에서 만료시간을 60*60*10L 보다 길게 설정하자.

localhost:8080/admin 를 GET 으로 보내는 이유는 AdminController 에서 @GetMapping(“/admin”) 으로 해놨기 때문이다.



이전 포스팅 : JWT 발급 메서드 추가
다음 포스팅 : 사용자 정보와 CORS 설정

This post is licensed under CC BY 4.0 by the author.

[Spring Security JWT] JWT 발급 메서드 추가

[Spring Security JWT] 사용자 정보와 CORS 설정