240304 기록, jwt적용(3)
LoginFilter
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
import com.example.userservice.dto.MyUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
// UsernamePasswordAuthenticationFilter는 원래 HttpSecurity#formLogin에서 진행을 했는데
// jwt를 위해 formLogin을 disable 시켰기 때문에 개발자가 구현해줘야 한다
private final AuthenticationManager authenticationManager;
// JWTUtil 클래스를 통해서 로그인이 성공했을 때 토큰 발급
// SecurityConfig에 가서 LoginFilter 생성자에 JWTUtil 파라미터도 추가해주어야 한다
private final JWTUtil jwtUtil;
@Override
// 미승인 Authentication 생성하는 메소드
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{
// 요청을 가로채서 username과 password를 추출
String username = obtainUsername(request);
String password = obtainPassword(request);
System.out.println("username : " + username);
// 스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 한다
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
// username, password, role
// token에 담은 검증을 AuthenticationManager로 전달
return authenticationManager.authenticate(authToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
String username = myUserDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
String token = jwtUtil.createJwt(username, role, 60*60*10L);
response.addHeader("Authorization", "Bearer " + token);
// 위의 HTTP인증방식은 RFC 7235 정의에 의해서 공식적으로 형식을 지정해줌
// Authorization: 타입 인증토큰
// Authorization: Bearer 인증토큰string
// "Bearer "의 경우 공백을 끝에 붙여줘야 함
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(401);
}
}
1
2
3
4
LoginFilter loginFilter = new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil);
loginFilter.setFilterProcessesUrl("/api/users/loginProc");
http
.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
- 로그인 성공 시 토큰을 생성하기 위해 로직이 구현되어 있는 JWTUtil을 의존성 주입해 준다
- 시큐리티에서 로그인필터 등록 시 객체를 생성하며 등록을 했는데, JWTUtil을 필드에 선언하였으므로, 그곳에 매개변수로 JWTUtil을 추가해준다.
- 로그인 성공 시 Authentication을 통해 username, role 을 가져와 토큰을 생성한다.
- 생성된 토큰을 response header에 넣어준다. 넣을 때 형식에 주의한다.
- response.addHeader(“Authorization”, “Bearer “ + token);
- Bearer의 뒤에는 공백이 있다.
- 로그인 실패 시 response의 상태를 401 에러로 세팅한다.
테스트
- 포스트맨을 이용하여 로그인 진행
- 로그인 성공 시 응답 헤더 탭으로 가보면
- Authorization이 있고
- Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIyQGFiYy5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWF0IjoxNzA5NTM1Nzg4LCJpc3MiOiJqd0tpbSIsImV4cCI6MTcwOTUzNTgyNH0.bhcxG4A0YCplb6-42p4oCUNvEc6LXFPRZ26LSRlr8t0
- 라는 토큰이 발행됨
- 이제 다른 서비스 요청 시 이 토큰의 정보를 담아 요청하면 됨
- 아직은 토큰 검증 로직이 없어서 진행이 되지 않음
트러블슈팅
- 포스트맨으로 로그인을 진행하였는데 에러발생
- ECDSA signing keys must be PrivateKey instances
- 원인을 찾아보니 토큰 생성 시 암호화 알고리즘을 .signWith(key, SignatureAlgorithm.ES256) 로 했었다.
- .signWith(key, SignatureAlgorithm.HS256)로 수정.
JWT 검증 필터 구현
- 스프링 시큐리티 filter chain에 요청에 담긴 JWT를 검증하기 위한 커스텀 필터
- 해당 필터를 통해 요청 헤더 Authorization에 담긴 JWT를 검증하고 강제로 SecurityContextHolder에 세션을 임시로 생성
- 해당 세션은 STATELESS 상태로 관리되므로 해당 요청이 끝나면 소멸된다
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
80
81
82
83
84
85
86
87
88
import com.example.userservice.dto.MyUserDetails;
import com.example.userservice.model.User;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
@Component
public class JWTFilter extends OncePerRequestFilter {
// OncePerRequestFilter 는 요청에 대해 한번만 동작한다
private final JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// request의 해더에 담아진 토큰 정보를 해석하여 유저 정보를 가져와야 한다
// 이를 JWTUtil이 진행한다
// 토큰 검증 구현
// request에서 Authorization 헤더를 찾는다
String authorization = request.getHeader("Authorization");
// request 헤더에서 가져온 authorization이 null인지 혹은 Bearer로 시작하는지 확인
if(authorization == null || !authorization.startsWith("Bearer ")){
System.out.println("token null");
// filterChain으로 엮여있는 다음 필터에 doFilter를 통해 request와 response를 넘겨준다
filterChain.doFilter(request, response);
// 조건에 해당되면 메소드 종료 (필수)
return;
}
// 토큰을 분리하여 소멸 시간 검증
String token = authorization.split(" ")[1];
// Bearer 와 뒤의 토큰 정보를 분리하고, 토큰 정보를 받아옴
// JWTUtil에 구현한 유효기간 검사
if(jwtUtil.isExpired(token)){
System.out.println("token expired");
// filterChain으로 엮여있는 다음 필터에 doFilter를 통해 request와 response를 넘겨준다
filterChain.doFilter(request, response);
// 조건에 해당되면 메소드 종료 (필수)
return;
}
// 여기까지 왔으면, 토큰이 있고, 양식도 제대로 되어있고 유효기간도 남아있다는 것
// 토큰을 기반으로 일시적인 세션을 만들어서 저장한다.
// 토큰에서 username과 role을 획득한다
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
// User Entity에 값을 설정하여 생성
User user = new User();
user.setEmail(username);
user.setPassword("tempPassword");
user.setRole(role);
// 패스워드의 경우 토큰에 담겨있지 않았는데, 비밀번호도 초기화를 해줘야 한다
// 비밀번호를 DB에서 매번 요청이 올 때마다 조회를 하면 비효율적이므로 임시로 만들어 초기화
// MyUserDetails 객체를 만든다.
MyUserDetails myUserDetails = new MyUserDetails(user);
// 스프링 시큐리티에 인증 토큰 생성
Authentication authentication = new UsernamePasswordAuthenticationToken(myUserDetails, null, myUserDetails.getAuthorities());
// 세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
// 검증 필터를 만들었다면 SecurityConfig에 가서 필터를 등록해주자
}
}
- SecurityConfig에서 LoginFilter 앞에 필터 등록
1
2
3
4
5
6
7
8
// 토큰 검증 필터 등록
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
LoginFilter loginFilter = new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil);
loginFilter.setFilterProcessesUrl("/api/users/loginProc");
http
.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
트러블슈팅
- JWTUtil에서 .parseClaimsJwt(token) 로 인해서 에러 발생
- Signed Claims JWSs are not supported.
- .parseClaimsJws(token) 로 변경하여 해결