-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: jwtAuthenticationFilter 구현 및 관련 Service 구현 (#17)
- Loading branch information
1 parent
eb2f318
commit 4e27f1a
Showing
4 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
71 changes: 71 additions & 0 deletions
71
src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
111 changes: 111 additions & 0 deletions
111
src/main/java/net/teumteum/core/security/service/JwtService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> 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<Claims> 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(); | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
src/main/java/net/teumteum/core/security/service/RedisService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, String> valueOperations = getStringStringValueOperations(); | ||
return valueOperations.get(key); | ||
} | ||
|
||
/* key - value 데이터 설정하는 메소드 */ | ||
public void setData(String key, String value) { | ||
ValueOperations<String, String> 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<String, String> valueOperations = getStringStringValueOperations(); | ||
Duration expireDuration = Duration.ofSeconds(duration); | ||
valueOperations.set(key, value, expireDuration); | ||
} | ||
|
||
private ValueOperations<String, String> getStringStringValueOperations() { | ||
return this.stringRedisTemplate.opsForValue(); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
src/main/java/net/teumteum/core/security/service/SecurityService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |