Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 토큰 갱신 기능 구현 #364

Merged
merged 2 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion secret
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@RequiredArgsConstructor
public enum JwtErrorCode implements ErrorCode {

INVALID_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않은 토큰입니다"),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다"),
EMPTY_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다"),
REFRESH_TOKEN_EXPIRE_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다"),
ACCESS_TOKEN_EXPIRE_TOKEN(HttpStatus.FORBIDDEN, "액세스 토큰이 만료되었습니다");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package codesquad.fineants.global.security.ajax.handler;

import java.io.IOException;
import java.util.Date;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand Down Expand Up @@ -37,7 +38,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("utf-8");

Token token = tokenService.generateToken(MemberAuthentication.from(member));
Token token = tokenService.generateToken(MemberAuthentication.from(member), new Date());
ApiResponse<LoginResponse> body = ApiResponse.success(MemberSuccessCode.OK_LOGIN);

CookieUtils.setCookie(response, tokenFactory.createAccessTokenCookie(token));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.web.cors.CorsConfiguration;

import codesquad.fineants.domain.member.repository.MemberRepository;
Expand Down Expand Up @@ -92,7 +92,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(urlParamFilter(),
OAuth2AuthorizationRequestRedirectFilter.class);
http.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtAuthFilter(), AuthorizationFilter.class);

http
.oauth2Login(configurer -> configurer
Expand All @@ -112,7 +112,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti

@Bean
public JwtAuthenticationFilter jwtAuthFilter() {
return new JwtAuthenticationFilter(tokenService, oauthMemberRedisService);
return new JwtAuthenticationFilter(tokenService, oauthMemberRedisService, tokenFactory);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
package codesquad.fineants.global.security.oauth.filter;

import java.io.IOException;
import java.time.LocalDateTime;

import org.apache.logging.log4j.util.Strings;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import codesquad.fineants.domain.member.service.OauthMemberRedisService;
import codesquad.fineants.global.security.factory.TokenFactory;
import codesquad.fineants.global.security.oauth.dto.MemberAuthentication;
import codesquad.fineants.global.security.oauth.dto.Token;
import codesquad.fineants.global.security.oauth.service.TokenService;
import codesquad.fineants.global.util.CookieUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -25,23 +30,48 @@
public class JwtAuthenticationFilter extends GenericFilterBean {
private final TokenService tokenService;
private final OauthMemberRedisService oauthMemberRedisService;
private final TokenFactory tokenFactory;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
IOException,
ServletException {
String token = CookieUtils.getAccessToken((HttpServletRequest)request);

if (token != null && !oauthMemberRedisService.isAlreadyLogout(token)) {
if (tokenService.verifyToken(token)) {
MemberAuthentication memberAuthentication = tokenService.parseMemberAuthenticationToken(token);
Authentication auth = getAuthentication(memberAuthentication);
SecurityContextHolder.getContext().setAuthentication(auth);
ServletException,
AuthenticationException {
String accessToken = CookieUtils.getAccessToken((HttpServletRequest)request);
String refreshToken = CookieUtils.getRefreshToken((HttpServletRequest)request);

// accessToken 만료 and refreshToken 유효 => accessToken 갱신
// accessToken 만료 and refreshToken 만료 임박 => accessToken 갱신, refreshToken 갱신
// accessToken 유효 and refreshToken 만료 임박 => accessToken 갱신, refreshToken 갱신
// accessToken 만료 and refreshToken 만료 => 401
// accessToken 유효 and refreshToken 만료 => nothing
// accessToken 유효 and refreshToken 유효 => nothing
Token token = null;
if (accessToken != null && !oauthMemberRedisService.isAlreadyLogout(accessToken)) {
if (tokenService.isExpiredToken(accessToken)) {
token = tokenService.refreshToken(refreshToken, LocalDateTime.now());
} else if (tokenService.verifyToken(accessToken) && tokenService.isRefreshTokenNearExpiry(refreshToken)) {
token = tokenService.refreshToken(refreshToken, LocalDateTime.now());
} else if (tokenService.verifyToken(accessToken)) {
token = Token.create(accessToken, refreshToken);
}
}

if (token != null) {
setAuthentication(token.getAccessToken());
CookieUtils.setCookie((HttpServletResponse)response, tokenFactory.createAccessTokenCookie(token));
CookieUtils.setCookie((HttpServletResponse)response, tokenFactory.createRefreshTokenCookie(token));
}

chain.doFilter(request, response);
}

private void setAuthentication(String accessToken) {
MemberAuthentication memberAuthentication = tokenService.parseMemberAuthenticationToken(accessToken);
Authentication auth = getAuthentication(memberAuthentication);
SecurityContextHolder.getContext().setAuthentication(auth);
}

private Authentication getAuthentication(MemberAuthentication memberAuthentication) {
return new UsernamePasswordAuthenticationToken(
memberAuthentication,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package codesquad.fineants.global.security.oauth.handler;

import java.io.IOException;
import java.util.Date;

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
Expand Down Expand Up @@ -41,7 +42,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
log.debug("oAuth2User : {}", oAuth2User);
log.debug("userDto : {}", memberAuthentication);

Token token = tokenService.generateToken(memberAuthentication);
Token token = tokenService.generateToken(memberAuthentication, new Date());
log.debug("token : {}", token);

String redirectUrl = (String)request.getSession().getAttribute("redirect_url");
Expand All @@ -50,6 +51,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
}

String targetUrl = UriComponentsBuilder.fromUriString(redirectUrl)
.queryParam("success", "true")
.build()
.toUriString();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package codesquad.fineants.global.security.oauth.service;

import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Base64;
Expand All @@ -9,13 +11,14 @@
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Service;

import codesquad.fineants.global.errors.errorcode.JwtErrorCode;
import codesquad.fineants.global.errors.exception.FineAntsException;
import codesquad.fineants.global.security.oauth.dto.MemberAuthentication;
import codesquad.fineants.global.security.oauth.dto.Token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
Expand All @@ -24,6 +27,8 @@
@Service
@Slf4j
public class TokenService {

private static final Duration EXPIRY_IMMINENT_TIME = Duration.ofHours(1);
private final String secretKey;
private final Long tokenPeriod;
private final Long refreshPeriod;
Expand All @@ -38,10 +43,8 @@ public TokenService(
this.refreshPeriod = refreshPeriod;
}

public Token generateToken(MemberAuthentication authentication) {
public Token generateToken(MemberAuthentication authentication, Date now) {
Claims claims = generateClaims(authentication);

Date now = new Date();
String accessToken = generateAccessToken(claims, now);
String refreshToken = generateRefreshToken(claims, now);
return Token.create(accessToken, refreshToken);
Expand Down Expand Up @@ -110,8 +113,64 @@ public Token refreshToken(String refreshToken, LocalDateTime now) {
MemberAuthentication memberAuthentication = parseMemberAuthenticationToken(refreshToken);
Claims claims = generateClaims(memberAuthentication);
String accessToken = generateAccessToken(claims, Timestamp.valueOf(now));
return Token.create(accessToken, refreshToken);
String newRefreshToken = refreshToken;
if (isRefreshTokenNearExpiry(refreshToken)) {
newRefreshToken = generateRefreshToken(claims, Timestamp.valueOf(now));
}
return Token.create(accessToken, newRefreshToken);
}
throw new BadCredentialsException(JwtErrorCode.INVALID_TOKEN.getMessage());
}

/**
* 리프레시 토큰의 만료 임박 시간 체크
* @param token 리프레시 토큰
* @return 만료 임박 시간 체크 결과
*/
public boolean isRefreshTokenNearExpiry(String token) {
Date expiration;
try {
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
expiration = claims.getBody()
.getExpiration();
} catch (Exception e) {
return false;
}

// 현재 시간
Instant now = Instant.now();

// 만료 시간
Instant expirationInstant = expiration.toInstant();

// 만료 까지의 시간 간격
Duration duration = Duration.between(now, expirationInstant);

// 만료 까지의 시간 간격이 EXPIRY_IMMINENT_TIME 미만 인지 확인
return duration.compareTo(EXPIRY_IMMINENT_TIME) < 0;
}

/**
* 토큰 만료 여부 확인
* @param token JWT 토큰
* @return 토큰 만료 여부
*/
public boolean isExpiredToken(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
return claims.getBody()
.getExpiration()
.before(new Date());
} catch (ExpiredJwtException e) {
return true;
} catch (Exception e) {
return false;
}
throw new FineAntsException(JwtErrorCode.INVALID_TOKEN);
}
}
Loading