이 게시글은 유튜브 개발자 유미님의 무료강의 스프링 시큐리티 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);
}
}
- JWTFilter 클래스를 생성한다.
- 요청에 대해 한번만 동작하는 Once Per Request Filter 를 상속 받는다.
- doFilterInternal 추상메서드를 Override 한다.
- request 의 헤더에서 JWT 값을 추출한다.
- JWT 값이 없거나 Bearer 로 시작하지 않으면 JWT 가 없는걸로 간주하고 끝낸다.
- JWT 값에서 Bearer 를 제거한다.
- JWT 의 유효기간이 남았는지 검증한다.
- JWT 에 있는 username 과 role 값으로 UserEntity 객체를 만들고 그 값으로 CustomUserDetails 객체를 만든다.
- CustomUserDetails 객체로 Authentication 객체를 만든다.
- 생성한 Authentication 객체를 SecurityContextHolder 에 등록한다.
- 메서드를 종료한다.(다음 필터로 넘어간다.)
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 설정