Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.ject.studytrip.auth.application.facade;

import com.ject.studytrip.auth.application.service.KakaoLoginService;
import com.ject.studytrip.auth.application.service.TokenService;
import com.ject.studytrip.auth.infra.dto.KakaoUserInfoResponse;
import com.ject.studytrip.auth.presentation.dto.request.KakaoLoginRequest;
import com.ject.studytrip.auth.presentation.dto.request.KakaoSignupRequest;
import com.ject.studytrip.auth.presentation.dto.request.LogoutRequest;
import com.ject.studytrip.auth.presentation.dto.request.TokenReissueRequest;
import com.ject.studytrip.auth.presentation.dto.response.TokenResponse;
import com.ject.studytrip.member.application.dto.CreateMemberCommand;
import com.ject.studytrip.member.application.service.MemberService;
Expand All @@ -16,6 +19,7 @@
@RequiredArgsConstructor
public class AuthFacade {
private final KakaoLoginService kakaoLoginService;
private final TokenService tokenService;
private final MemberService memberService;

public TokenResponse kakaoLogin(KakaoLoginRequest request) {
Expand All @@ -25,7 +29,7 @@ public TokenResponse kakaoLogin(KakaoLoginRequest request) {
memberService.getMemberBySocialProviderAndSocialId(
SocialProvider.KAKAO, response.kakaoId());

return kakaoLoginService.getTokens(member.getId().toString(), member.getRole().name());
return tokenService.getTokens(member.getId().toString(), member.getRole().name());
}

public TokenResponse kakaoSignup(KakaoSignupRequest request) {
Expand All @@ -40,6 +44,17 @@ public TokenResponse kakaoSignup(KakaoSignupRequest request) {

Member member = memberService.createMemberFromKakao(command);

return kakaoLoginService.getTokens(member.getId().toString(), member.getRole().name());
return tokenService.getTokens(member.getId().toString(), member.getRole().name());
}

public TokenResponse reissueToken(TokenReissueRequest request) {
String memberId = tokenService.getMemberIdByRefreshToken(request.refreshToken());
String role = memberService.getRoleByMemberId(memberId);

return tokenService.reissueToken(request.refreshToken(), memberId, role);
}

public void logout(LogoutRequest request) {
tokenService.logout(request.accessToken(), request.refreshToken());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,16 @@
import com.ject.studytrip.auth.infra.dto.KakaoTokenResponse;
import com.ject.studytrip.auth.infra.dto.KakaoUserInfoResponse;
import com.ject.studytrip.auth.infra.provider.KakaoOauthProvider;
import com.ject.studytrip.auth.infra.provider.TokenProvider;
import com.ject.studytrip.auth.presentation.dto.response.TokenResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class KakaoLoginService {
private final KakaoOauthProvider kakaoOauthProvider;
private final TokenProvider tokenProvider;

public KakaoUserInfoResponse getKakaoUserInfo(String code) {
KakaoTokenResponse response = kakaoOauthProvider.getKakaoTokens(code);
return kakaoOauthProvider.getKakaoUserInfo(response.accessToken());
}

public TokenResponse getTokens(String memberId, String memberRole) {
String accessToken = tokenProvider.createAccessToken(memberId, memberRole);
String refreshToken = tokenProvider.createRefreshToken(memberId, memberRole);
return TokenResponse.of(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.ject.studytrip.auth.application.service;

import com.ject.studytrip.auth.domain.error.AuthErrorCode;
import com.ject.studytrip.auth.domain.repository.LogoutTokenRedisRepository;
import com.ject.studytrip.auth.domain.repository.RefreshTokenRedisRepository;
import com.ject.studytrip.auth.infra.provider.TokenProvider;
import com.ject.studytrip.auth.presentation.dto.response.TokenResponse;
import com.ject.studytrip.global.exception.CustomException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class TokenService {
private final TokenProvider tokenProvider;
private final LogoutTokenRedisRepository logoutTokenRedisRepository;
private final RefreshTokenRedisRepository refreshTokenRedisRepository;

public TokenResponse getTokens(String memberId, String role) {
String accessToken = tokenProvider.createAccessToken(memberId, role);
String refreshToken = tokenProvider.createRefreshToken();
long refreshTokenExpirationTime = tokenProvider.getRefreshTokenExpirationTime();

refreshTokenRedisRepository.saveRefreshToken(
memberId, refreshToken, refreshTokenExpirationTime);

return TokenResponse.of(accessToken, refreshToken);
}

public TokenResponse reissueToken(String refreshToken, String memberId, String role) {
long refreshTokenExpirationTime = tokenProvider.getRefreshTokenExpirationTime();
String newAccessToken = tokenProvider.createAccessToken(memberId, role);
String newRefreshToken = tokenProvider.createRefreshToken();

refreshTokenRedisRepository.deleteRefreshToken(refreshToken);
refreshTokenRedisRepository.saveRefreshToken(
memberId, newRefreshToken, refreshTokenExpirationTime);

return TokenResponse.of(newAccessToken, newRefreshToken);
}

public void logout(String accessToken, String refreshToken) {
validateRefreshToken(refreshToken);

long accessTokenRemainingTime = tokenProvider.getAccessTokenRemainingTime(accessToken);

logoutTokenRedisRepository.saveAccessToken(accessToken, accessTokenRemainingTime);
refreshTokenRedisRepository.deleteRefreshToken(refreshToken);
}

public String getMemberIdByRefreshToken(String refreshToken) {
validateRefreshToken(refreshToken);

return refreshTokenRedisRepository.findMemberIdByRefreshToken(refreshToken);
}

public void setAuthenticationByAccessToken(String accessToken) {
String memberId = tokenProvider.extractMemberIdFromToken(accessToken);
String role = tokenProvider.extractRoleFromToken(accessToken);
var authorities = List.of(new SimpleGrantedAuthority(role));
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(memberId, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

public void validateActiveAccessToken(String accessToken) {
if (!tokenProvider.validateAccessToken(accessToken)) {
throw new CustomException(AuthErrorCode.INVALID_JWT_TOKEN);
}
if (logoutTokenRedisRepository.existsAccessToken(accessToken)) {
throw new CustomException(AuthErrorCode.TOKEN_IS_BLACKLISTED);
}
}

private void validateRefreshToken(String refreshToken) {
if (!refreshTokenRedisRepository.existsRefreshToken(refreshToken)) {
throw new CustomException(AuthErrorCode.INVALID_REFRESH_TOKEN);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ject.studytrip.auth.domain.repository;

public interface LogoutTokenRedisRepository {
void saveAccessToken(String accessToken, long accessTokenExpirationTime);

boolean existsAccessToken(String accessToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ject.studytrip.auth.domain.repository;

public interface RefreshTokenRedisRepository {
void saveRefreshToken(String memberId, String refreshToken, long refreshTokenExpireTime);

boolean existsRefreshToken(String refreshToken);

void deleteRefreshToken(String refreshToken);

String findMemberIdByRefreshToken(String refreshToken);
}
58 changes: 0 additions & 58 deletions src/main/java/com/ject/studytrip/auth/infra/filter/JwtFilter.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
Expand All @@ -21,25 +22,31 @@ public String createAccessToken(String memberId, String role) {
return createToken(memberId, role, tokenProperties.accessExpirationTime());
}

public String createRefreshToken(String memberId, String role) {
return createToken(memberId, role, tokenProperties.refreshExpirationTime());
public String createRefreshToken() {
return UUID.randomUUID().toString();
}

public String extractMemberIdFromToken(String token) {
return parseClaims(token).getSubject();
}

public String extractMemberRoleFromToken(String token) {
public String extractRoleFromToken(String token) {
return (String) parseClaims(token).get("role");
}

public boolean validateToken(String token) {
try {
parseClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new CustomException(AuthErrorCode.INVALID_JWT_TOKEN);
}
public boolean validateAccessToken(String accessToken) {
parseClaims(accessToken); // 내부에서 예외 처리
return true;
}

public long getRefreshTokenExpirationTime() {
return tokenProperties.refreshExpirationTime();
}

public long getAccessTokenRemainingTime(String accessToken) {
Claims claims = parseClaims(accessToken); // 내부에서 예외 처리
Date expiration = claims.getExpiration();
return Math.max(expiration.getTime() - System.currentTimeMillis(), 0); // 음수 방지
}

private String createToken(String memberId, String role, long expirationSeconds) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.ject.studytrip.auth.infra.repository.redis;

import static com.ject.studytrip.global.common.constants.CacheKeyConstants.AUTH_LOGOUT_TOKEN_PREFIX;

import com.ject.studytrip.auth.domain.repository.LogoutTokenRedisRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class LogoutTokenRedisRepositoryAdapter implements LogoutTokenRedisRepository {
private final RedisTemplate<String, String> redisTemplate;

@Override
public void saveAccessToken(String accessToken, long accessTokenExpirationTime) {
redisTemplate
.opsForValue()
.set(
AUTH_LOGOUT_TOKEN_PREFIX.getValue() + accessToken,
"LOGOUT",
accessTokenExpirationTime);
}

@Override
public boolean existsAccessToken(String accessToken) {
return Boolean.TRUE.equals(
redisTemplate.hasKey(AUTH_LOGOUT_TOKEN_PREFIX.getValue() + accessToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.ject.studytrip.auth.infra.repository.redis;

import static com.ject.studytrip.global.common.constants.CacheKeyConstants.AUTH_REISSUE_TOKEN_PREFIX;

import com.ject.studytrip.auth.domain.repository.RefreshTokenRedisRepository;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class RefreshTokenRedisRepositoryAdapter implements RefreshTokenRedisRepository {
private final RedisTemplate<String, String> redisTemplate;

@Override
public void saveRefreshToken(
String memberId, String refreshToken, long refreshTokenExpireTime) {
redisTemplate
.opsForValue()
.set(
AUTH_REISSUE_TOKEN_PREFIX.getValue() + refreshToken,
memberId,
refreshTokenExpireTime,
TimeUnit.MILLISECONDS);
}

@Override
public boolean existsRefreshToken(String refreshToken) {
return Boolean.TRUE.equals(
redisTemplate.hasKey(AUTH_REISSUE_TOKEN_PREFIX.getValue() + refreshToken));
}

@Override
public void deleteRefreshToken(String refreshToken) {
redisTemplate.delete(AUTH_REISSUE_TOKEN_PREFIX.getValue() + refreshToken);
}

@Override
public String findMemberIdByRefreshToken(String refreshToken) {
return redisTemplate.opsForValue().get(AUTH_REISSUE_TOKEN_PREFIX.getValue() + refreshToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.ject.studytrip.auth.application.facade.AuthFacade;
import com.ject.studytrip.auth.presentation.dto.request.KakaoLoginRequest;
import com.ject.studytrip.auth.presentation.dto.request.KakaoSignupRequest;
import com.ject.studytrip.auth.presentation.dto.request.LogoutRequest;
import com.ject.studytrip.auth.presentation.dto.request.TokenReissueRequest;
import com.ject.studytrip.auth.presentation.dto.response.TokenResponse;
import com.ject.studytrip.global.common.response.StandardResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -40,4 +42,21 @@ public ResponseEntity<StandardResponse> kakaoSignup(
TokenResponse response = authFacade.kakaoSignup(request);
return ResponseEntity.ok(StandardResponse.success(HttpStatus.OK.value(), response));
}

@Operation(summary = "토큰 재발급", description = "리프레시 토큰을 이용하여, 엑세스 토큰과 리프레시 토큰을 재발급합니다.")
@PostMapping("/token/reissue")
public ResponseEntity<StandardResponse> reissueToken(
@Valid @RequestBody TokenReissueRequest request) {
TokenResponse response = authFacade.reissueToken(request);
return ResponseEntity.ok(StandardResponse.success(HttpStatus.OK.value(), response));
}

@Operation(
summary = "로그아웃",
description = "엑세스 토큰과 리프레시 토큰을 이용하여, 엑세스 토큰을 블랙리스트에 추가하고, 저장된 리프레시 토큰을 제거합니다.")
@PostMapping("/logout")
public ResponseEntity<StandardResponse> logout(@Valid @RequestBody LogoutRequest request) {
authFacade.logout(request);
return ResponseEntity.ok(StandardResponse.success(HttpStatus.OK.value(), null));
}
}
Loading