Skip to content

Commit

Permalink
feature/#64 rtr 기반 refresh token 발행 기능 구현 (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
minjo-on authored Jan 10, 2025
1 parent 49e7aad commit c8c2a82
Show file tree
Hide file tree
Showing 27 changed files with 337 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package sorisoop.soridam.api.auth.application;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import sorisoop.soridam.api.auth.presentation.request.jwt.JwtLoginRequest;
import sorisoop.soridam.api.auth.presentation.request.jwt.RefreshTokenRequest;
import sorisoop.soridam.api.auth.presentation.request.oauth.OidcLoginRequest;
import sorisoop.soridam.auth.jwt.application.JwtService;
import sorisoop.soridam.auth.jwt.response.JwtResponse;
import sorisoop.soridam.auth.oauth.google.GoogleOidcService;
import sorisoop.soridam.auth.oauth.kakao.KakaoOidcService;
import sorisoop.soridam.domain.user.domain.User;

@Service
@RequiredArgsConstructor
public class AuthFacade {
private final KakaoOidcService kakaoOidcService;
private final GoogleOidcService googleOidcService;
private final JwtService jwtService;

public JwtResponse jwtLogin(JwtLoginRequest request) {
return jwtService.jwtLogin(request.email(), request.password());
}

@Transactional
public JwtResponse kakaoLogin(OidcLoginRequest idToken) {
User user = kakaoOidcService.processLogin(idToken.idToken());
user.updateLastLoginTime();
return jwtService.getToken(user);
}

@Transactional
public JwtResponse googleLogin(OidcLoginRequest idToken) {
User user = googleOidcService.processLogin(idToken.idToken());
user.updateLastLoginTime();
return jwtService.getToken(user);
}

public JwtResponse reissue(RefreshTokenRequest request) {
return jwtService.reissue(request.refreshToken());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import sorisoop.soridam.api.auth.application.AuthService;
import sorisoop.soridam.api.auth.application.AuthFacade;
import sorisoop.soridam.api.auth.presentation.request.jwt.JwtLoginRequest;
import sorisoop.soridam.api.auth.presentation.response.jwt.JwtResponse;
import sorisoop.soridam.auth.oauth.request.OidcLoginRequest;
import sorisoop.soridam.api.auth.presentation.request.jwt.RefreshTokenRequest;
import sorisoop.soridam.auth.jwt.response.JwtResponse;
import sorisoop.soridam.api.auth.presentation.request.oauth.OidcLoginRequest;

@RestController
@RequiredArgsConstructor
@Tag(name = "Auth", description = "로그인 API")
@RequestMapping("/api/auth")
public class AuthApiController {
private final AuthService authService;
private final AuthFacade authFacade;

@Operation(summary = "JWT 로그인 API", description = """
- Description : 이 API는 로그인 시 JWT를 발급합니다.
Expand All @@ -33,27 +34,37 @@ public ResponseEntity<JwtResponse> login(
@RequestBody
JwtLoginRequest request
) {
JwtResponse response = authService.jwtLogin(request);
JwtResponse response = authFacade.jwtLogin(request);
return ResponseEntity.ok(response);
}

@PostMapping("/oauth2/kakao")
public ResponseEntity<JwtResponse> kakaoSocialLogin(
@Valid
@RequestBody
OidcLoginRequest idToken
OidcLoginRequest request
){
JwtResponse response = authService.kakaoLogin(idToken);
JwtResponse response = authFacade.kakaoLogin(request);
return ResponseEntity.ok(response);
}

@PostMapping("/oauth2/google")
public ResponseEntity<JwtResponse> googleSocialLogin(
@Valid
@RequestBody
OidcLoginRequest idToken
OidcLoginRequest request
){
JwtResponse response = authService.googleLogin(idToken);
JwtResponse response = authFacade.googleLogin(request);
return ResponseEntity.ok(response);
}

@PostMapping("/reissue")
public ResponseEntity<JwtResponse> reissue(
@Valid
@RequestBody
RefreshTokenRequest request
){
JwtResponse response = authFacade.reissue(request);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sorisoop.soridam.api.auth.presentation.request.jwt;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record RefreshTokenRequest(
@Schema(description = "refresh token",
example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsILJdhDYTYzNzQwNjQwMH0.7J",
requiredMode = REQUIRED)
@NotNull
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sorisoop.soridam.auth.oauth.request;
package sorisoop.soridam.api.auth.presentation.request.oauth;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public record NoisePersistResponse(
) {
public static NoisePersistResponse from(Noise noise){
return builder()
.id(noise.getId())
.id(noise.extractUuid())
.build();
}
}
6 changes: 6 additions & 0 deletions soridam-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ spring:
init:
mode: always

data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD}

springdoc:
default-consumes-media-type: application/json
default-produces-media-type: application/json
Expand Down
3 changes: 3 additions & 0 deletions soridam-auth/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ dependencies {
// 도메인 및 공통 모듈
implementation project(':soridam-common')
implementation project(':soridam-domain')
implementation project(':soridam-global-util')

implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1'

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import sorisoop.soridam.auth.common.CustomAccessDeniedHandler;
import sorisoop.soridam.auth.jwt.JwtAuthenticationEntryPoint;
import sorisoop.soridam.auth.jwt.JwtAuthenticationFilter;
import sorisoop.soridam.auth.jwt.JwtProvider;
import sorisoop.soridam.auth.jwt.application.JwtProvider;

@Configuration
@EnableWebSecurity
Expand Down Expand Up @@ -51,7 +51,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers(STATIC_RESOURCES_PATTERNS).permitAll()
.requestMatchers(PERMIT_ALL_PATTERNS).permitAll()
.requestMatchers(PUBLIC_ENDPOINTS).permitAll()
.requestMatchers(OAUTH2_PATTERNS).permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
Expand Down Expand Up @@ -84,12 +83,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/api/auth/**",
};

private static final String[] OAUTH2_PATTERNS = {
"/oauth2/**", // Spring Security OAuth2 기본 경로
"/login/oauth2/**", // 로그인 리다이렉트 처리 경로
"/oauth2/authorization/**" // 인증 요청 트리거 경로
};


CorsConfigurationSource corsConfigurationSource() {
return request -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import sorisoop.soridam.auth.jwt.application.JwtProvider;
import sorisoop.soridam.common.exception.CustomException;
import sorisoop.soridam.common.exception.ExceptionResponse;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sorisoop.soridam.auth.jwt;
package sorisoop.soridam.auth.jwt.application;

import java.time.Duration;
import java.util.Collections;
Expand All @@ -19,12 +19,14 @@
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import lombok.RequiredArgsConstructor;
import sorisoop.soridam.auth.jwt.JwtProperties;
import sorisoop.soridam.auth.jwt.exception.JwtExpiredException;
import sorisoop.soridam.auth.jwt.exception.JwtInvalidException;
import sorisoop.soridam.auth.jwt.exception.JwtMalformedException;
import sorisoop.soridam.auth.jwt.exception.JwtSignatureInvalidException;
import sorisoop.soridam.auth.jwt.exception.JwtUnsupportedException;
import sorisoop.soridam.domain.user.domain.Role;
import sorisoop.soridam.domain.user.exception.UnauthorizedException;

@Service
@RequiredArgsConstructor
Expand All @@ -33,9 +35,19 @@ public class JwtProvider {

private final Header header = Jwts.header().type("JWT").build();

public String generateToken(String userId, Role role, Duration expiredAt) {
public String generateAccessToken(String userId, Role role, Duration duration) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), userId, role);
return makeToken(new Date(now.getTime() + duration.toMillis()), userId, role);
}

public String generateAccessToken(String userId, Role role) {
Date now = new Date();
return makeToken(new Date(now.getTime() + Duration.ofMinutes(30).toMillis()), userId, role);
}

public String generateRefreshToken(String userId, Role role) {
Date now = new Date();
return makeToken(new Date(now.getTime() + Duration.ofDays(1).toMillis()), userId, role);
}

private String makeToken(Date expiry, String userId, Role role) {
Expand Down Expand Up @@ -96,7 +108,7 @@ public Set<SimpleGrantedAuthority> getRoles(Claims claims) {
case "ADMIN" -> Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN"));
case "PAID_USER" -> Collections.singleton(new SimpleGrantedAuthority("ROLE_PAID_USER"));
case "USER" -> Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
default -> Collections.emptySet();
default -> throw new UnauthorizedException();
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package sorisoop.soridam.auth.jwt.application;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import sorisoop.soridam.auth.jwt.response.JwtResponse;
import sorisoop.soridam.domain.refresh.RefreshToken;
import sorisoop.soridam.domain.refresh.application.RefreshTokenService;
import sorisoop.soridam.domain.user.application.UserCommandService;
import sorisoop.soridam.domain.user.application.UserQueryService;
import sorisoop.soridam.domain.user.domain.User;

@Service
@RequiredArgsConstructor
public class JwtService {
private final UserQueryService userQueryService;
private final UserCommandService userCommandService;
private final RefreshTokenService refreshTokenService;
private final JwtProvider jwtProvider;

public JwtResponse jwtLogin(String email, String password) {
User user = userCommandService.login(email, password);
JwtResponse response = getToken(user);
refreshTokenService.save(user.getId(), response.refreshToken());
return response;
}

public JwtResponse reissue(String token) {
RefreshToken refreshToken = refreshTokenService.getToken(token);
String userId = refreshToken.getUserId();
User user = userQueryService.getById(userId);
JwtResponse response = getToken(user);

refreshTokenService.save(userId, response.refreshToken());

return response;
}

public JwtResponse getToken(User user) {
String refreshToken = jwtProvider.generateRefreshToken(user.extractUuid(), user.getRole());
String accessToken = jwtProvider.generateAccessToken(user.extractUuid(), user.getRole());

return JwtResponse.of(accessToken, refreshToken);
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sorisoop.soridam.api.auth.presentation.response.jwt;
package sorisoop.soridam.auth.jwt.response;

import lombok.Builder;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import sorisoop.soridam.auth.oauth.exception.OidcExpiredException;
import sorisoop.soridam.auth.oauth.exception.OidcInvalidAudienceException;
import sorisoop.soridam.auth.oauth.exception.OidcInvalidIssuerException;
import sorisoop.soridam.auth.oauth.request.OidcLoginRequest;
import sorisoop.soridam.common.domain.Provider;
import sorisoop.soridam.domain.user.domain.User;
import sorisoop.soridam.domain.user.domain.UserRepository;
Expand Down Expand Up @@ -44,9 +43,9 @@ private OidcIdToken validateAndDecodeIdToken(String idToken) {
return oidcIdToken;
}

public User processLogin(OidcLoginRequest request) {
OidcIdToken idToken = validateAndDecodeIdToken(request.idToken());
return findOrCreateUser(idToken);
public User processLogin(String idToken) {
OidcIdToken oidcIdToken = validateAndDecodeIdToken(idToken);
return findOrCreateUser(oidcIdToken);
}

private void validateIssuer(String issuer) {
Expand Down
1 change: 1 addition & 0 deletions soridam-domain/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ dependencies {

// 데이터베이스와 상호작용 (JPA 사용)
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
Loading

0 comments on commit c8c2a82

Please sign in to comment.