Skip to content

Commit

Permalink
Merge pull request #135 from studio-recoding/feat-refresh-token
Browse files Browse the repository at this point in the history
[🚀feat] refresh token 로직 구현
  • Loading branch information
JeonHaeseung authored Jul 9, 2024
2 parents a4e845b + 010223d commit eb62130
Show file tree
Hide file tree
Showing 17 changed files with 188 additions and 81 deletions.
31 changes: 31 additions & 0 deletions src/main/java/Ness/Backend/domain/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package Ness.Backend.domain.auth;

import Ness.Backend.domain.auth.dto.request.PostRefreshTokenDto;
import Ness.Backend.domain.auth.dto.response.GetJwtTokenDto;
import Ness.Backend.domain.member.entity.Member;
import Ness.Backend.global.auth.AuthUser;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/auth")
public class AuthController {
private final AuthService authService;

@PostMapping("/logout")
@Operation(summary = "로그아웃 요청", description = "로그아웃 요청 API 입니다.")
public void logout(@AuthUser Member member, @RequestBody PostRefreshTokenDto postRefreshTokenDto) {
authService.logout(member, postRefreshTokenDto);
}

@PostMapping("/reIssuance")
@Operation(summary = "JWT access 토큰 재발급 요청", description = "JWT access 토큰 재발급 요청 API 입니다.")
public GetJwtTokenDto reIssuance(@AuthUser Member member, @RequestBody PostRefreshTokenDto postRefreshTokenDto) {
return authService.reIssuance(member, postRefreshTokenDto);
}
}
53 changes: 53 additions & 0 deletions src/main/java/Ness/Backend/domain/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package Ness.Backend.domain.auth;

import Ness.Backend.domain.auth.dto.request.PostRefreshTokenDto;
import Ness.Backend.domain.auth.dto.response.GetJwtTokenDto;
import Ness.Backend.domain.auth.inmemory.RefreshTokenRepository;
import Ness.Backend.domain.auth.jwt.JwtTokenProvider;
import Ness.Backend.domain.member.entity.Member;
import Ness.Backend.global.error.ErrorCode;
import Ness.Backend.global.error.exception.UnauthorizedException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

@Service
@RequiredArgsConstructor
public class AuthService {
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenRepository refreshTokenRepository;

@Transactional
public void logout(Member member, PostRefreshTokenDto postRefreshTokenDto) {
/* refreshToken 만료 여부 확인 */
if(refreshTokenRepository.findRefreshTokenByJwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken()).isEmpty()){
throw new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN);
}

refreshTokenRepository.deleteRefreshTokenByJwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken());
SecurityContextHolder.clearContext();
}

@Transactional
public GetJwtTokenDto reIssuance(Member member, PostRefreshTokenDto postRefreshTokenDto) {
/* refreshToken 유효성 확인 */
if (!jwtTokenProvider.validRefreshToken(postRefreshTokenDto.getJwtRefreshToken())) {
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN);
}

/* refreshToken 만료 여부 확인 */
if(refreshTokenRepository.findRefreshTokenByJwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken()).isEmpty()){
throw new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN);
}

final GetJwtTokenDto generateToken = GetJwtTokenDto.builder()
.jwtAccessToken(jwtTokenProvider.generateAccessToken(member.getEmail(), new Date()))
.jwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken())
.build();

return generateToken;
}
}
20 changes: 0 additions & 20 deletions src/main/java/Ness/Backend/domain/auth/dto/LoginRequestDto.java

This file was deleted.

20 changes: 0 additions & 20 deletions src/main/java/Ness/Backend/domain/auth/dto/RegisterRequestDto.java

This file was deleted.

31 changes: 0 additions & 31 deletions src/main/java/Ness/Backend/domain/auth/dto/ResourceDto.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package Ness.Backend.domain.auth.dto.request;

import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public class PostRefreshTokenDto {
@NotNull
private String jwtRefreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package Ness.Backend.domain.auth.dto.response;


import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@ToString
@NoArgsConstructor
public class GetJwtTokenDto {
private String jwtAccessToken;
private String jwtRefreshToken;

@Builder
public GetJwtTokenDto(String jwtAccessToken, String jwtRefreshToken) {
this.jwtAccessToken = jwtAccessToken;
this.jwtRefreshToken = jwtRefreshToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
import Ness.Backend.domain.auth.inmemory.entity.RefreshToken;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, Long> {
Optional<RefreshToken> findRefreshTokenByJwtRefreshToken(String refreshToken);

void deleteRefreshTokenByJwtRefreshToken(String refreshToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -19,4 +21,10 @@ public void saveRefreshToken(String refreshToken, String authKey) {
.build();
refreshTokenRepository.save(token);
}

@Transactional
public void removeRefreshToken(String refreshToken) {
refreshTokenRepository.findRefreshTokenByJwtRefreshToken(refreshToken)
.ifPresent(token -> refreshTokenRepository.delete(token));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@

@Getter
@NoArgsConstructor
@RedisHash(value = "refreshToken")
@RedisHash(value = "refreshToken", timeToLive = 60*60*24*14)
public class RefreshToken { /* Redis에 저장해서 RefreshToken이 유효한지 검증 */
@Id
@Indexed
private String jwtRefreshToken;

// 맴버 이메일로 설정
private String authKey;

//리프레시 토큰의 생명 주기(14일)
@TimeToLive
private Long ttl;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

} catch (TokenExpiredException e){
log.error(e + " EXPIRED_TOKEN");
//request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN.getCode());
setResponse(response, ErrorCode.EXPIRED_TOKEN);
} catch (SignatureVerificationException e){
log.error(e + " INVALID_TOKEN_SIGNATURE");
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/Ness/Backend/domain/auth/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,27 @@ public Member validJwtToken(String jwtToken){

return tokenMember;
}

public boolean validRefreshToken(String refreshToken){
/* email 값이 null이 아닌지 확인 */
String authKey = getAuthKeyClaim(refreshToken);
if (authKey == null){
return false;
}

/* JWT_EXPIRATION_TIME이 지나지 않았는지 확인 */
Date expiresAt = getExpireTimeClaim(refreshToken);
if (!this.validExpiredTime(expiresAt)) {
return false;
}

/* email 값이 정상적으로 있고, JWT_EXPIRATION_TIME도 지나지 않았다면,
* 해당 토큰의 email 정보를 가진 맴버가 있는지 DB에서 확인 */
Member tokenMember = memberRepository.findMemberByEmail(authKey);
if (tokenMember == null) {
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package Ness.Backend.domain.auth.oAuth;

import Ness.Backend.domain.auth.inmemory.RefreshTokenService;
import Ness.Backend.domain.auth.jwt.JwtTokenProvider;
import Ness.Backend.domain.auth.jwt.entity.JwtToken;
import Ness.Backend.domain.auth.security.AuthDetails;
Expand All @@ -10,6 +11,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
Expand All @@ -19,6 +21,7 @@
@RequiredArgsConstructor
public class OAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;

@Value("${frontend.redirect-url}")
private String frontRedirectUrl;
Expand All @@ -32,11 +35,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
/*인증에 성공한 사용자*/
AuthDetails oAuth2User = (AuthDetails) authentication.getPrincipal();

/*JwtToken 생성*/
/* JwtToken 생성*/
JwtToken jwtToken = jwtTokenProvider.generateJwtToken(oAuth2User.getUsername());

//TODO: RefreshToken update
//refreshTokenService.saveRefreshToken(jwtToken.getJwtRefreshToken(),oAuth2User.getUser().getAuthKey());
/* RefreshToken 저장하기 */
refreshTokenService.saveRefreshToken(jwtToken.getJwtRefreshToken(), oAuth2User.getUsername());

/*JwtToken과 함께 리다이렉트*/
String targetUrl = UriComponentsBuilder.fromUriString(setRedirectUrl(request.getServerName()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public AuthenticationManager authenticationManager() throws Exception {

@Bean
public OAuthSuccessHandler oAuthSuccessHandler(){
return new OAuthSuccessHandler(jwtTokenProvider());
return new OAuthSuccessHandler(jwtTokenProvider(), refreshTokenService);
}

@Bean
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/Ness/Backend/global/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ public enum ErrorCode {

/* Auth */
UNAUTHORIZED_ACCESS(UNAUTHORIZED, "AUTH000", "권한이 없습니다."),
EXPIRED_TOKEN(BAD_REQUEST, "AUTH001", "만료된 엑세스 토큰입니다."),
INVALID_REFRESH_TOKEN(BAD_REQUEST, "AUTH002", "리프레시 토큰이 유효하지 않습니다."),
MISMATCH_REFRESH_TOKEN(BAD_REQUEST, "AUTH003", "리프레시 토큰의 유저 정보가 일치하지 않습니다."),
EXPIRED_TOKEN(UNAUTHORIZED, "AUTH001", "만료된 엑세스 토큰입니다."),
INVALID_REFRESH_TOKEN(UNAUTHORIZED, "AUTH002", "리프레시 토큰이 유효하지 않습니다."),
MISMATCH_REFRESH_TOKEN(UNAUTHORIZED, "AUTH003", "리프레시 토큰의 유저 정보가 일치하지 않습니다."),
INVALID_AUTH_TOKEN(UNAUTHORIZED, "AUTH004", "권한 정보가 없는 토큰입니다."),
UNAUTHORIZED_USER(UNAUTHORIZED, "AUTH005", "현재 내 계정 정보가 존재하지 않습니다."),
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "AUTH006", "로그아웃 된 사용자입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package Ness.Backend.global.error.exception;

import Ness.Backend.global.error.ErrorCode;
import lombok.Getter;

@Getter
public class UnauthorizedException extends BaseException {
public UnauthorizedException() {
super(ErrorCode.UNAUTHORIZED_ACCESS, ErrorCode.UNAUTHORIZED_ACCESS.getMessage());
}
public UnauthorizedException(String message) {
super(ErrorCode.UNAUTHORIZED_ACCESS, message);
}
public UnauthorizedException(ErrorCode errorCode) {
super(errorCode, errorCode.getMessage());
}
}
6 changes: 5 additions & 1 deletion src/main/java/Ness/Backend/infra/redis/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package Ness.Backend.infra.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -13,6 +14,7 @@
import java.time.Duration;

@Configuration
@Slf4j
public class RedisConfig {

@Value("${spring.data.redis.host}")
Expand All @@ -35,10 +37,12 @@ public RedisConnectionFactory redisConnectionFactory() {
.commandTimeout(Duration.ofSeconds(1)) //No longer than 1 second
.shutdownTimeout(Duration.ZERO) //Immediately shutdown after application shutdown
.build();

log.info("Connected to Redis at {}:{}", host, port);
return new LettuceConnectionFactory(redisConfig, clientConfig);
}

/* Use redis template for redis*/
/* Use redis template for redis */
@Bean
public RedisTemplate<String, Object> redisTemplate() {
/* Most basic configuration */
Expand Down

0 comments on commit eb62130

Please sign in to comment.