From 4e27f1a7940916f4c360043abb04eb010e8b8c39 Mon Sep 17 00:00:00 2001 From: choidongkuen Date: Thu, 4 Jan 2024 20:49:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20jwtAuthenticationFilter=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20Service=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JwtAuthenticationFilter.java | 71 +++++++++++ .../core/security/service/JwtService.java | 111 ++++++++++++++++++ .../core/security/service/RedisService.java | 43 +++++++ .../security/service/SecurityService.java | 34 ++++++ 4 files changed, 259 insertions(+) create mode 100644 src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/net/teumteum/core/security/service/JwtService.java create mode 100644 src/main/java/net/teumteum/core/security/service/RedisService.java create mode 100644 src/main/java/net/teumteum/core/security/service/SecurityService.java diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..e515cc83 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,71 @@ +package net.teumteum.core.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.teumteum.core.property.JwtProperty; +import net.teumteum.core.security.UserAuthentication; +import net.teumteum.core.security.service.AuthService; +import net.teumteum.core.security.service.JwtService; +import net.teumteum.core.security.service.RedisService; +import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserRepository; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final RedisService redisService; + private final AuthService authService; + + private final JwtProperty jwtProperty; + private final UserRepository userRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException + { + /* Cors Preflight Request */ + if(request.getMethod().equals("OPTIONS")) { + return; + } + /** + * 전달 받은 Access token 부터 Authentication 인증 객체 Security Context에 저장 + */ + try { + String token = this.resolveTokenFromRequest(request); + // access token 이 있고 유효하다면 + if (StringUtils.hasText(token) && this.jwtService.validateToken(token)) { + User user = this.authService.findUserByToken(token).get(); + UserAuthentication authentication = new UserAuthentication(user); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + // access token 이 만료된 경우 or access token 정보로 식별 안되는 경우 + // 예외가 발생하기만 해도 ExceptionTranslationFilter 호출 + } catch (InsufficientAuthenticationException e) { + log.info("JwtAuthentication UnauthorizedUserException!"); + } + filterChain.doFilter(request, response); + } + + private String resolveTokenFromRequest(HttpServletRequest request) { + String token = request.getHeader(jwtProperty.getAccess().getHeader()); + if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) { + return token.substring(jwtProperty.getBearer().length()).trim(); + } + return null; + } +} diff --git a/src/main/java/net/teumteum/core/security/service/JwtService.java b/src/main/java/net/teumteum/core/security/service/JwtService.java new file mode 100644 index 00000000..d8cf09e9 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/service/JwtService.java @@ -0,0 +1,111 @@ +package net.teumteum.core.security.service; + +import io.jsonwebtoken.*; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.teumteum.core.property.JwtProperty; +import net.teumteum.core.security.dto.TokenResponse; +import net.teumteum.user.domain.User; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +/* JWT 관련 모든 작업을 위한 Service */ +@Slf4j +@Service +@RequiredArgsConstructor +public class JwtService { + private final JwtProperty jwtProperty; + private final RedisService redisService; + + // HttpServletRequest 부터 Access Token 추출 + public Optional extractAccessToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(this.jwtProperty.getAccess().getHeader())) + .filter(StringUtils::hasText) + .filter(accessToken -> accessToken.startsWith(jwtProperty.getBearer())) + .map(accessToken -> accessToken.replace(jwtProperty.getBearer(), "")); + } + + // HttpServletRequest 부터 Refresh Token 추출 + public String extractRefreshToken(HttpServletRequest request) { + return request.getHeader(this.jwtProperty.getRefresh().getHeader()); + } + + // access token 생성 + public String createAccessToken(String payload) { + return this.createToken(payload, this.jwtProperty.getAccess().getExpiration()); + } + + + // refresh token 생성 + public String createRefreshToken() { + return this.createToken(UUID.randomUUID().toString(), this.jwtProperty.getRefresh().getExpiration()); + + } + + + // access token 으로부터 회원 아이디 추출 + public String getUserIdFromToken(String token) { + try { + return Jwts.parser() + .setSigningKey(this.jwtProperty.getSecret()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } catch (Exception exception) { + throw new JwtException("Access Token is not valid"); + } + } + + // kakao oauth 로그인 & 일반 로그인 시 jwt 응답 생성 + redis refresh 저장 + public TokenResponse createServiceToken(User users) { + String accessToken = this.createAccessToken(String.valueOf(users.getId())); + String refreshToken = this.createRefreshToken(); + + /* 서비스 토큰 생성 */ + TokenResponse userServiceTokenResponseDto = TokenResponse.builder() + .accessToken(this.jwtProperty.getBearer() + " " + accessToken) + .refreshToken(refreshToken) + .build(); + + /* redis refresh token 저장 */ + this.redisService.setDataExpire(String.valueOf(users.getId()), + userServiceTokenResponseDto.getRefreshToken(), this.jwtProperty.getRefresh().getExpiration()); + + return userServiceTokenResponseDto; + } + + // token 유효성 검증 + public boolean validateToken(String token) { + try { + Jws claimsJws = Jwts.parser().setSigningKey(this.jwtProperty.getSecret()).parseClaimsJws(token); + return !claimsJws.getBody().getExpiration().before(new Date()); + } catch (ExpiredJwtException exception) { + log.warn("만료된 jwt 입니다."); + } catch (UnsupportedJwtException exception) { + log.warn("지원되지 않는 jwt 입니다."); + } catch (IllegalArgumentException exception) { + log.warn("jwt 에 오류가 존재합니다."); + } + return false; + } + + // 실제 token 생성 로직 + private String createToken(String payload, Long tokenExpiration) { + Claims claims = Jwts.claims().setSubject(payload); + Date tokenExpiresIn = new Date(new Date().getTime() + tokenExpiration); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date()) + .setExpiration(tokenExpiresIn) + .signWith(SignatureAlgorithm.HS512, this.jwtProperty.getSecret()) + .compact(); + } +} diff --git a/src/main/java/net/teumteum/core/security/service/RedisService.java b/src/main/java/net/teumteum/core/security/service/RedisService.java new file mode 100644 index 00000000..3b2e5183 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/service/RedisService.java @@ -0,0 +1,43 @@ +package net.teumteum.core.security.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +/* Redis 관련 작업을 위한 서비스 */ +@Service +@RequiredArgsConstructor +public class RedisService { + private final StringRedisTemplate stringRedisTemplate; + + /* key 에 해당하는 데이터 얻어오는 메소드 */ + public String getData(String key) { + ValueOperations valueOperations = getStringStringValueOperations(); + return valueOperations.get(key); + } + + /* key - value 데이터 설정하는 메소드 */ + public void setData(String key, String value) { + ValueOperations valueOperations = getStringStringValueOperations(); + valueOperations.set(key, value); + } + + /* key 에 해당하는 데이터 삭제하는 메소드 */ + public void deleteData(String key) { + this.stringRedisTemplate.delete(key); + } + + /* key 에 해당하는 데이터 만료기간 설정 메소드 */ + public void setDataExpire(String key, String value, Long duration) { + ValueOperations valueOperations = getStringStringValueOperations(); + Duration expireDuration = Duration.ofSeconds(duration); + valueOperations.set(key, value, expireDuration); + } + + private ValueOperations getStringStringValueOperations() { + return this.stringRedisTemplate.opsForValue(); + } +} diff --git a/src/main/java/net/teumteum/core/security/service/SecurityService.java b/src/main/java/net/teumteum/core/security/service/SecurityService.java new file mode 100644 index 00000000..43613047 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/service/SecurityService.java @@ -0,0 +1,34 @@ +package net.teumteum.core.security.service; + +import net.teumteum.core.security.UserAuthentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +/* Security 관련 작업을 위한 서비스 */ +@Component +public class SecurityService { + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + /* 해당 요청에서 로그인한 회원 id 반환 */ + public Long getCurrentUserId() { + UserAuthentication userAuthentication = getUserAuthentication(); + return userAuthentication.getId(); + } + + /* 해당 요청에서 로그인한 회원 OAuth id 반환 */ + public String getCurrentUserOAuthId() { + UserAuthentication userAuthentication = getUserAuthentication(); + return userAuthentication.getOauthId(); + } + + public void setUserId(Long userId) { + UserAuthentication userAuthentication = getUserAuthentication(); + userAuthentication.setUserId(userId); + } + + private static UserAuthentication getUserAuthentication() { + return (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + } +}