From 439b77893437aad9b228fc55f0b28a60912cbf80 Mon Sep 17 00:00:00 2001 From: Fortune00 Date: Sun, 8 May 2022 20:02:05 +0900 Subject: [PATCH] [feat][#6] Implement refreshToken in UserController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Access Token이 만료되면, 클라이언트는 refresh를 요청하고 새로운 토큰을 받는다 - JwtAuthFilter : 만료되지 않은 업데이트 이전의 토큰을 가진 요청은 400 Status를 반환 --- .../gp/cnusambe/config/WebSecurityConfig.java | 4 ++- .../controller/user/UserController.java | 30 +++++++++++++++++++ .../payload/response/LoginResponse.java | 4 +-- .../cnusambe/security/jwt/JwtAuthFilter.java | 26 +++++++++++----- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/main/java/gp/cnusambe/config/WebSecurityConfig.java b/src/main/java/gp/cnusambe/config/WebSecurityConfig.java index 2c5fccc..f8b2ef9 100644 --- a/src/main/java/gp/cnusambe/config/WebSecurityConfig.java +++ b/src/main/java/gp/cnusambe/config/WebSecurityConfig.java @@ -5,6 +5,7 @@ import gp.cnusambe.security.jwt.JwtAuthFilter; import gp.cnusambe.security.jwt.JwtTokenProvider; import gp.cnusambe.service.user.UserDetailsServiceImpl; +import gp.cnusambe.util.RedisUtil; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -26,6 +27,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsServiceImpl userDetailsServiceImpl; private final JwtAuthEntryPoint unauthorizedHandler; private final JwtAccessDeniedHandler accessDeniedHandler; + private final RedisUtil redisUtil; @Bean public PasswordEncoder passwordEncoder() { @@ -40,7 +42,7 @@ public AuthenticationManager authenticationManagerBean() throws Exception { @Bean public JwtAuthFilter authTokenFilter() { - return new JwtAuthFilter(jwtTokenProvider, userDetailsServiceImpl); + return new JwtAuthFilter(jwtTokenProvider, userDetailsServiceImpl, redisUtil); } @Override diff --git a/src/main/java/gp/cnusambe/controller/user/UserController.java b/src/main/java/gp/cnusambe/controller/user/UserController.java index 1fb0766..154e3f9 100644 --- a/src/main/java/gp/cnusambe/controller/user/UserController.java +++ b/src/main/java/gp/cnusambe/controller/user/UserController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; +import java.util.UUID; @RequiredArgsConstructor @RestController @@ -65,6 +66,28 @@ public ResponseEntity login(@RequestBody LoginRequest loginReques return new ResponseEntity<>(jwtResponse, HttpStatus.OK); } + @PostMapping("/refresh") + public ResponseEntity refreshToken(@RequestBody LogoutOrRefreshRequest request) { + String uuid = request.getUuid(); + String userId = request.getUserId(); + String oldAccessToken = request.getAccessToken(); + String oldRefreshToken = redisUtil.getData(uuid).orElseThrow(RefreshTokenException::new); + + if (!jwtTokenProvider.validateJwtToken(oldAccessToken)) { + throw new RefreshTokenException(); + } + + if(!userId.equals(jwtTokenProvider.getUserIdFromJwtToken(oldRefreshToken)) && !userId.equals(jwtTokenProvider.getUserIdFromJwtToken(oldAccessToken))) { + throw new RefreshTokenException(); + } + + UserDetailsImpl userDetailsImpl = (UserDetailsImpl) userDetailsServiceImp.loadUserByUsername(userId); + LoginResponse jwtResponse = generateAndSaveToken(userDetailsImpl); + deleteToken(uuid, oldAccessToken); + + return new ResponseEntity<>(jwtResponse, HttpStatus.OK); + } + private LoginResponse generateAndSaveToken(UserDetailsImpl userDetailsImpl) { String userId = userDetailsImpl.getUserId(); String uuid = UUID.randomUUID().toString(); @@ -75,4 +98,11 @@ private LoginResponse generateAndSaveToken(UserDetailsImpl userDetailsImpl) { return new LoginResponse(userId, accessToken, uuid); } + + private void deleteToken(String uuid, String oldAccessToken) { + if (redisUtil.getData(uuid).isPresent()) { + redisUtil.deleteData(uuid); + } + redisUtil.setDataExpire(oldAccessToken, oldAccessToken, (int)JwtTokenProvider.TOKEN_EXPIRATION_SECONDS); + } } \ No newline at end of file diff --git a/src/main/java/gp/cnusambe/payload/response/LoginResponse.java b/src/main/java/gp/cnusambe/payload/response/LoginResponse.java index 6926e1d..9034156 100644 --- a/src/main/java/gp/cnusambe/payload/response/LoginResponse.java +++ b/src/main/java/gp/cnusambe/payload/response/LoginResponse.java @@ -6,10 +6,8 @@ import lombok.Getter; @Getter -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class LoginResponse { - @JsonProperty("token_type") - private final String TOKEN_TYPE = "Bearer"; + private final String tokenType = "Bearer"; private String userId; private String accessToken; private String uuid; diff --git a/src/main/java/gp/cnusambe/security/jwt/JwtAuthFilter.java b/src/main/java/gp/cnusambe/security/jwt/JwtAuthFilter.java index e9e443b..4529b50 100644 --- a/src/main/java/gp/cnusambe/security/jwt/JwtAuthFilter.java +++ b/src/main/java/gp/cnusambe/security/jwt/JwtAuthFilter.java @@ -1,6 +1,7 @@ package gp.cnusambe.security.jwt; import gp.cnusambe.service.user.UserDetailsServiceImpl; +import gp.cnusambe.util.RedisUtil; import io.jsonwebtoken.ExpiredJwtException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,29 +26,40 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final UserDetailsServiceImpl userDetailsServiceImpl; + private final RedisUtil redisUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { Optional jwt = parseJwt(request); + if (jwt.isPresent() && jwtTokenProvider.validateJwtToken(jwt.get())) { - String username = jwtTokenProvider.getUserIdFromJwtToken(jwt.get()); - UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(username); - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); + Optional isLockedAccessToken = redisUtil.getData(jwt.get()); + + if (isLockedAccessToken.isEmpty()) { + String username = jwtTokenProvider.getUserIdFromJwtToken(jwt.get()); + UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } else { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + } else { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } catch (ExpiredJwtException e) { log.error("ExpiredJwtException : {}", e.getMessage()); } catch (Exception e) { log.error("Cannot set user authentication : {}", e.getMessage()); } + filterChain.doFilter(request, response); } private Optional parseJwt(HttpServletRequest request) { - String headerAuth = request.getHeader("Authorization"); - if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + String headerAuth = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith(BEARER_PREFIX)) { return Optional.of(headerAuth.substring(7)); } return Optional.empty();