From d94c40e66e84e9a8c331e3adbd6adab59b83b959 Mon Sep 17 00:00:00 2001 From: yang Date: Fri, 30 Jun 2023 02:33:38 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20AccessToken=20Redis=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/security/jwt/JwtTokenProvider.java | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/main/java/chilling/encore/global/config/security/jwt/JwtTokenProvider.java b/src/main/java/chilling/encore/global/config/security/jwt/JwtTokenProvider.java index 6eebd86..210df53 100644 --- a/src/main/java/chilling/encore/global/config/security/jwt/JwtTokenProvider.java +++ b/src/main/java/chilling/encore/global/config/security/jwt/JwtTokenProvider.java @@ -2,6 +2,7 @@ import chilling.encore.domain.User; import chilling.encore.global.config.redis.RedisRepository; +import chilling.encore.global.config.security.exception.SecurityException.RemovedAccessTokenException; import chilling.encore.repository.springDataJpa.UserRepository; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; @@ -10,6 +11,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -24,6 +26,9 @@ import java.util.NoSuchElementException; import java.util.stream.Collectors; +import static chilling.encore.global.config.security.jwt.JwtConstants.JwtExcpetionCode.REMOVE_ACCESS_TOKEN; +import static chilling.encore.global.config.security.jwt.JwtConstants.JwtExcpetionMessage.REMOVED_ACCESS_TOKEN; + @Slf4j @Component @RequiredArgsConstructor @@ -31,6 +36,9 @@ public class JwtTokenProvider implements InitializingBean { private final RedisRepository redisRepository; private static final String AUTHORITIES_KEY = "auth"; private static final String USER_IDX = "userIdx"; + private static final String ACCESS = "Access"; + private static final String REFRESH = "Refresh"; + private static final String GRANT = "Bearer"; @Value("${jwt.secret}") private String secret; @@ -66,7 +74,7 @@ public TokenInfoResponse createToken(Authentication authentication) { updateRefreshToken(userIdx, refreshToken); - return TokenInfoResponse.from("Bearer", accessToken, refreshToken, accessTokenValidityTime); + return TokenInfoResponse.from(GRANT, accessToken, refreshToken, accessTokenValidityTime); } public TokenInfoResponse oauth2CreateToken(Authentication authentication) { @@ -84,14 +92,14 @@ public TokenInfoResponse oauth2CreateToken(Authentication authentication) { updateRefreshToken(userIdx, refreshToken); - return TokenInfoResponse.from("Bearer", accessToken, refreshToken, accessTokenValidityTime); + return TokenInfoResponse.from(GRANT, accessToken, refreshToken, accessTokenValidityTime); } private String createRefreshToken(String authorities, Date current, Long userIdx) { Date refreshTokenValidity = new Date(current.getTime() + this.refreshTokenValidityTime); String refreshToken = Jwts.builder() - .setSubject("Refresh") + .setSubject(REFRESH) .claim(AUTHORITIES_KEY, authorities) .claim(USER_IDX, userIdx) .setIssuedAt(current) @@ -105,7 +113,7 @@ private String createAccessToken(String authorities, Date current, Long userIdx) Date accessTokenValidity = new Date(current.getTime() + this.accessTokenValidityTime); String accessToken = Jwts.builder() - .setSubject("Access") + .setSubject(ACCESS) .claim(AUTHORITIES_KEY, authorities) .claim(USER_IDX, userIdx) .setIssuedAt(current) @@ -113,6 +121,8 @@ private String createAccessToken(String authorities, Date current, Long userIdx) .setExpiration(accessTokenValidity) .compact(); + log.info("accessToken 저장"); + redisRepository.setValues(ACCESS + userIdx.toString(), accessToken, Duration.ofSeconds(accessTokenValidityTime)); return accessToken; } @@ -157,25 +167,23 @@ private Claims parseClaims(String token) { public boolean validateToken(String token) { try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + checkMultiLogin(token); return true; } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { - log.info("잘못된 JWT 서명입니다."); throw e; } catch (ExpiredJwtException e) { - log.info("만료된 JWT 토큰입니다."); throw e; } catch (UnsupportedJwtException e) { - log.info("지원되지 않는 JWT 토큰입니다."); throw e; } catch (IllegalArgumentException e) { - log.info("JWT 토큰이 잘못되었습니다."); throw e; } } + + public boolean validateRefreshToken(String token) { - if (!getSubject(token).equals("Refresh")) + if (!getSubject(token).equals(REFRESH)) throw new IllegalArgumentException("Refresh Token이 아닙니다."); try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); @@ -208,6 +216,14 @@ public boolean checkBlackList(String token) { return true; } + private void checkMultiLogin(String token) { + Jws claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + String userIdx = claims.getBody().get(USER_IDX).toString(); + log.info("checkMultiple Login"); + if (redisRepository.getValues(ACCESS + userIdx).isEmpty()) + throw new RemovedAccessTokenException(REMOVED_ACCESS_TOKEN.getMessage(), REMOVE_ACCESS_TOKEN.getCode(), HttpStatus.FORBIDDEN); + } + public String getSubject(String token) { Claims claims = parseClaims(token); From eb94b6580f79c5a1e53b9622978841054db53c04 Mon Sep 17 00:00:00 2001 From: yang Date: Fri, 30 Jun 2023 02:34:18 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20AccessToken=20Redis=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/security/exception/SecurityException.java | 6 ++++++ .../encore/global/config/security/jwt/JwtConstants.java | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/chilling/encore/global/config/security/exception/SecurityException.java b/src/main/java/chilling/encore/global/config/security/exception/SecurityException.java index 5aa4fd5..7ab0046 100644 --- a/src/main/java/chilling/encore/global/config/security/exception/SecurityException.java +++ b/src/main/java/chilling/encore/global/config/security/exception/SecurityException.java @@ -8,6 +8,12 @@ protected SecurityException(String message, String errorCode, HttpStatus httpSta super(message, errorCode, httpStatus); } + public static class RemovedAccessTokenException extends SecurityException { + public RemovedAccessTokenException(String message, String errorCode, HttpStatus httpStatus) { + super(message, errorCode, httpStatus); + } + } + public static class InvalidJwtFormatException extends SecurityException { public InvalidJwtFormatException(String message, String errorCode, HttpStatus httpStatus) { super(message, errorCode, httpStatus); diff --git a/src/main/java/chilling/encore/global/config/security/jwt/JwtConstants.java b/src/main/java/chilling/encore/global/config/security/jwt/JwtConstants.java index 9734280..b6efd0b 100644 --- a/src/main/java/chilling/encore/global/config/security/jwt/JwtConstants.java +++ b/src/main/java/chilling/encore/global/config/security/jwt/JwtConstants.java @@ -12,7 +12,8 @@ public enum JwtExcpetionMessage { JWT_NOT_SUPPORTED("[ERROR] 지원되지 않는 토큰 입니다"), WRONG_TOKEN("[ERROR] 잘못된 토큰 입니다"), NON_AUTHORITY("[ERROR] 권한이 없습니다"), - UNKHOWN_EXCEPTION("[ERROR] 알 수 없는 예외 발생!!"); + UNKHOWN_EXCEPTION("[ERROR] 알 수 없는 예외 발생!!"), + REMOVED_ACCESS_TOKEN("[ERROR] 다른 컴퓨터에서 로그인 되었습니다."); private final String message; } @@ -24,7 +25,8 @@ public enum JwtExcpetionCode { JWT_NOT_SUPPORTED("jwt003"), WRONG_TOKEN("jwt004"), UNKHOWN_EXCEPTION("jwt005"), - NON_AUTHORITY("jwt006"); + NON_AUTHORITY("jwt006"), + REMOVE_ACCESS_TOKEN("jwt007"); private final String code; } From 5d29f847225af1f25338bad8dbc9d238130ac2f7 Mon Sep 17 00:00:00 2001 From: yang Date: Fri, 30 Jun 2023 02:34:29 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20AccessToken=20Redis=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/jwt/filter/JwtExceptionFilter.java | 14 +++++++++----- .../config/security/jwt/filter/JwtFilter.java | 3 +++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtExceptionFilter.java b/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtExceptionFilter.java index 9126c74..785b3a8 100644 --- a/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtExceptionFilter.java +++ b/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtExceptionFilter.java @@ -16,6 +16,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.nio.charset.StandardCharsets; @Component @RequiredArgsConstructor @@ -29,23 +30,26 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); } catch (ExpiredJwtException e) { String errorResponseJson = getErrorResponseJson(request, response, e); - response.getOutputStream().write(errorResponseJson.getBytes("UTF-8")); + response.getOutputStream().write(errorResponseJson.getBytes(StandardCharsets.UTF_8)); } catch (InvalidJwtFormatException e) { String errorResponseJson = getErrorResponseJson(request, response, e); - response.getOutputStream().write(errorResponseJson.getBytes("UTF-8")); + response.getOutputStream().write(errorResponseJson.getBytes(StandardCharsets.UTF_8)); } catch (NonSupportedJwtException e) { String errorResponseJson = getErrorResponseJson(request, response, e); - response.getOutputStream().write(errorResponseJson.getBytes("UTF-8")); + response.getOutputStream().write(errorResponseJson.getBytes(StandardCharsets.UTF_8)); } catch (WrongTokenException e) { String errorResponseJson = getErrorResponseJson(request, response, e); - response.getOutputStream().write(errorResponseJson.getBytes("UTF-8")); + response.getOutputStream().write(errorResponseJson.getBytes(StandardCharsets.UTF_8)); } catch (UnKnownException e) { String errorResponseJson = getErrorResponseJson(request, response, e); - response.getOutputStream().write(errorResponseJson.getBytes("UTF-8")); + response.getOutputStream().write(errorResponseJson.getBytes(StandardCharsets.UTF_8)); + } catch (RemovedAccessTokenException e) { + String errorResponseJson = getErrorResponseJson(request, response, e); + response.getOutputStream().write(errorResponseJson.getBytes(StandardCharsets.UTF_8)); } } diff --git a/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtFilter.java b/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtFilter.java index 68fc32e..9bef826 100644 --- a/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtFilter.java +++ b/src/main/java/chilling/encore/global/config/security/jwt/filter/JwtFilter.java @@ -46,6 +46,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI); } + } catch (SecurityException.RemovedAccessTokenException e) { + log.error("exception : {}", e.getMessage()); + throw e; } catch (SecurityException | MalformedJwtException e) { log.error("exception : {}", e.getMessage()); throw new SecurityException.InvalidJwtFormatException(JwtExcpetionMessage.INVALID_FORMAT.getMessage(), JwtExcpetionCode.INVALID_FORMAT.getCode(), HttpStatus.FORBIDDEN);