- 만료된 토큰 확인후 재발급 및 리프레쉬 토큰 적용 -
public class JwtAuthorizationFilter extends OncePerRequestFilter
// JWT 토큰 substring
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = jwtUtil.getAccessTokenFromRequest(req);
String refreshTokenValue = jwtUtil.getRefreshTokenFromRequest(req);
tokenValue = jwtUtil.substringToken(tokenValue);
refreshTokenValue = jwtUtil.substringToken(refreshTokenValue);
// jwtAccess토큰 오류검증
if (!jwtUtil.validateToken(tokenValue)) {
return;
}
// jwt 토큰들 둘다 만료되면 상태코드 반환및 재로그인 요청
if (jwtUtil.isExpiredToken(tokenValue) && jwtUtil.isExpiredToken(refreshTokenValue)) {
return;
}
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
try {
// 토큰 재발급 api가 아닐경우만 인증객체 생성시도
if(!req.getRequestURI().equals("/user/reissue")) {
setAuthentication(info.getSubject());
}
기존 인가필터 코드에서 리프레쉬 토큰에 대한 검증이 추가됨, 또한 url 검사를 통해 재발급 요청 url일 경우 인증객체 생성은 하지않음
+ login API를 컨트롤러 단에서 받아 리프레쉬 코드를 저장해보려 시도했지만 시큐리티가 가져간 로그인 api는 컨트롤러 단까지 오지 않았음
public class JwtAuthorizationFilter extends OncePerRequestFilter
// 내부의 dofilter를 해도 최종적으로 컨트롤러 단까지는 가지 않는다는 부분을 알았다
@GetMapping("/login")
public ResponseEntity<CommonResponse<Void>> login(@AuthenticationPrincipal UserDetailsImpl userDetails, HttpServletRequest req) {
authService.login(userDetails, req);
@Transactional
public void login(UserDetailsImpl userDetails, HttpServletRequest req) {
TestUser user = userRepository.findByUsername(userDetails.getUsername());
String refreshToken = jwtUtil.getRefreshTokenFromRequest(req);
user.setRefreshToken(refreshToken);
}
그래서 인증필터 (로그인 요청시 시큐리티가 동작하는)에서 처음 토큰만들때 리프레쉬 토큰 정보도 그냥 user에
저장시킨후 db에 @Transactional로 반영하려 했지만 필터단에서는 뭔가 안되는지 오류
(Service단에서의 코드를 적용하면 되는것을 보니 bean 등록문제였던것..?)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
//UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String refreshToken = jwtUtil.createRefreshToken(username);
jwtUtil.addRefreshJwtToCookie(refreshToken, response);
String token = jwtUtil.createAccessToken(username);
jwtUtil.addAccessJwtToCookie(token, response);
refreshTokenSave(refreshToken, username); // 오류!
}
해결 : service단을 가져와서 동작시킴
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
...
authService.login(username, refreshToken); // 정상동작!
}
service 안에서 refreshToken을 username에 해당하는 객체에 저장하고 db 업데이트 하는동작을 하니 정상적으로 리프레쉬 토큰이 db에 들어갔다
- 수정된 부분 -
> UserDetailsServiceImpl에서 repository 받아서 동작
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final TestUserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TestUser user = userRepository.findByUsername(username);
return new UserDetailsImpl(user);
}
> Jwt Util에서 리프레쉬 토큰 관련 추가
public class JwtUtil {
...
public static final String REFRESHTOKEN_HEADER = "RefreshToken";
private final long REFRESH_TOKEN_TIME = 60 * 30 * 1000L;
...
public String createRefreshToken(String username) {
return createToken(username, REFRESH_TOKEN_TIME);
}
public String createAccessToken(String username) {
return createToken(username, TOKEN_TIME);
}
...
public void addRefreshJwtToCookie(String token, HttpServletResponse res) {
addJwtToCookie(token, res, REFRESHTOKEN_HEADER);
}
public void addAccessJwtToCookie(String token, HttpServletResponse res){
addJwtToCookie(token, res, AUTHORIZATION_HEADER);
}
public void setIsCheckToken(String token,String refreshToken, CheckValidToken check) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
} catch (SecurityException | MalformedJwtException | SignatureException e) {
check.setValidToken(false);
} catch (ExpiredJwtException e) {
check.setExpiredToken(true);
} catch (UnsupportedJwtException e) {
check.setValidToken(false);
} catch (IllegalArgumentException e) {
check.setValidToken(false);
}
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken);
} catch (SecurityException | MalformedJwtException | SignatureException e) {
check.setValidRefreshToken(false);
} catch (ExpiredJwtException e) {
check.setExpiredRefreshToken(true);
} catch (UnsupportedJwtException e) {
check.setValidRefreshToken(false);
} catch (IllegalArgumentException e) {
check.setValidRefreshToken(false);
}
}
public String getAccessTokenFromRequest(HttpServletRequest req) {
return getTokenFromRequest(req, AUTHORIZATION_HEADER);
}
public String getRefreshTokenFromRequest(HttpServletRequest req) {
return getTokenFromRequest(req, REFRESHTOKEN_HEADER);
}
}
- 기존 Jwt 메서드들에서 리프레쉬 토큰도 적용할수 있도록 매개변수 추가, 매개변수에 따른 메서드들 정의
> JwtAuthro.. Filter에서 url에 따라 검증 분기하도록 수정, 토큰 예외에 대한 boolean 값 CheckValidToken 클래스
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final AuthService authService;
....
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
if(!req.getRequestURI().equals("/user/login")) {
CheckValidToken isCheckToken = new CheckValidToken();
String tokenValue = jwtUtil.getAccessTokenFromRequest(req);
String refreshTokenValue = jwtUtil.getRefreshTokenFromRequest(req);
if (StringUtils.hasText(tokenValue) && StringUtils.hasText(refreshTokenValue)) {
// JWT 토큰 substring
tokenValue = jwtUtil.substringToken(tokenValue);
refreshTokenValue = jwtUtil.substringToken(refreshTokenValue);
jwtUtil.setIsCheckToken(tokenValue, refreshTokenValue, isCheckToken);
// jwtAccess토큰 오류검증
if (!isCheckToken.isValidToken()) {
return;
}
// jwt 토큰들 둘다 만료되면 상태코드 반환및 재로그인 요청
if (isCheckToken.isExpiredToken() && isCheckToken.isExpiredRefreshToken()) {
return;
}
try {
// 재발급 api가 아닐경우만 인증객체 생성시도
if (!req.getRequestURI().equals("/user/reissue")) {
// 현재 만료토큰에 대한 예외처리 메시지가 없어서 만료된 상태로 일반 API 요청하면 아무반응없음
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
setAuthentication(info.getSubject());
} else {
authService.tokenReissuance(refreshTokenValue, res);
}
} catch (Exception e) {
return;
}
}
}
}
public class CheckValidToken {
private boolean isValidToken;
private boolean isExpiredToken;
private boolean isValidRefreshToken;
private boolean isExpiredRefreshToken;
...
}
> SecyrityConfig에서 authService 주입
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationConfiguration authenticationConfiguration;
private final AuthService authService;
}
> authService (컨트롤러 단에서가 아닌 필터단에서 사용되는중)
@Service
@RequiredArgsConstructor
public class AuthService {
private final JwtUtil jwtUtil;
private final TestUserRepository userRepository;
private final PasswordConfig passwordConfig;
@Transactional
public void login(String username, String refreshToken) {
TestUser user = userRepository.findByUsername(username);
user.setRefreshToken(refreshToken);
}
public void tokenReissuance(String refreshToken, HttpServletResponse res) {
String username = jwtUtil.getUserInfoFromToken(refreshToken).getSubject();
TestUser user = userRepository.findByUsername(username);
// 클라이언트(현재로그인중, 리프레쉬토큰 만료x, 토큰만료 상태) 에서 보내온 refresh토큰과 db에 저장된
// 현재 로그인중인 username에 해당하는 refresh토큰 비교후 같으면 새로운 토큰발급
String userTokenValue = user.getRefreshToken().substring(7);
if(userTokenValue.equals(refreshToken)) {
String newToken = jwtUtil.createAccessToken(username);
jwtUtil.addAccessJwtToCookie(newToken, res);
}
}
// 임시 테스트용, 유일하게 컨트롤러 단에서 사용중
public void initTable() {
String password = "testPassword";
password = passwordConfig.passwordEncoder().encode(password);
TestUser user = new TestUser("testUserId", password);
userRepository.save(user);
}
}
- 느낀점 -
코딩하면서 기존의 Controller - Service - Repository 구조에 너무 매몰되어서 필터단에서는 로그인 검증 및 토큰생성만 하고 토큰 저장, db에 반영하는 부분등은 무조건 Controller단으로 바로 보내서 작업해야 하는줄 알고 많이 해매었던거 같다
만료된 토큰 -> 재발급 필요 -> 재발급 요청 = 컨트롤러 단에서 해보자 이 생각이었는데 이미 만료된 토큰이다보니 필터검증에서 걸러버리고 검증과정을 조건문을 걸어 다 무시하고 보낸다해도 filterChain.doFilter(req, res); 의 시큐리티
필터 체인과정중 토큰 만료에 대한 검증이 있는지 컨트롤러 단까지 가기전에 에러처리가 되어버려서 답이 없게 느껴졌다.
// 이 필터에서 제일 고생많이한듯..
public class JwtAuthorizationFilter extends OncePerRequestFilter { }
// 만악의 근원
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res
, FilterChain filterChain) throws ServletException, IOException {
...
filterChain.doFilter(req, res); << 이놈 자체에서도 또 만료토큰 잡아버릴줄은 생각도 못했네..
}
결과적으로 SecurityConfig 에서 각 필터들에 Service단을 주입해줌으로써 내가 원하는 기능(토큰 재발급, 로그인시 db에 사용자 정보 저장)을 필터과정중에 바로 해줌으로써 해결..
'TIL > Web Back' 카테고리의 다른 글
[Sparta] 내일배움캠프 TIL 30일차 (0) | 2024.06.11 |
---|---|
[Sparta] teamProject 3일차 TIL (1) | 2024.06.07 |
[Sparta] teamProject 1일차 TIL (0) | 2024.06.05 |
[Sparta] 내일배움캠프 TIL 31일차 (0) | 2024.06.04 |
[Sparta] 내일배움캠프 TIL 30일차 (0) | 2024.06.03 |