Skip to content

Commit

Permalink
Merge pull request #85 from KNU-HAEDAL/issue/#78
Browse files Browse the repository at this point in the history
Issue/#78
  • Loading branch information
bayy1216 authored Sep 5, 2024
2 parents debdc6f + da94a6c commit 1e74ca8
Show file tree
Hide file tree
Showing 16 changed files with 468 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ public ApiResponse<AuthRes.LoginResponse> login(

@Operation(summary = "액세스 토큰 재발급", description = "리프레시 토큰을 이용하여 액세스 토큰을 재발급한다.")
@PostMapping("/api/auth/refresh")
public ApiResponse<AuthRes.AccessTokenResponse> refresh(
public ApiResponse<AuthRes.JwtResponse> refresh(
@RequestHeader("Authorization") String authorization
) {
if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new IllegalArgumentException("Bearer 토큰이 필요합니다.");
}
String rawToken = authorization.substring("Bearer ".length());
String accessToken = authService.reissueToken(rawToken);
var response = AuthRes.AccessTokenResponse.of(accessToken);
JwtToken jwtToken = authService.reissueToken(rawToken);
var response = AuthRes.JwtResponse.from(jwtToken);
return ApiResponse.success(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ public static LoginResponse from(JwtToken jwtToken, UserModel.Main userMain) {
}

@Builder
public record AccessTokenResponse(
String accessToken
public record JwtResponse(
String accessToken,
String refreshToken
) {
public static AccessTokenResponse of(String accessToken) {
return AccessTokenResponse.builder()
.accessToken(accessToken)
public static JwtResponse from(JwtToken jwtToken) {
return JwtResponse.builder()
.accessToken(jwtToken.getAccessToken())
.refreshToken(jwtToken.getRefreshToken())
.build();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package org.haedal.zzansuni.auth.domain;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.haedal.zzansuni.global.jwt.JwtToken;
import org.haedal.zzansuni.global.jwt.JwtUser;
import org.haedal.zzansuni.global.jwt.JwtUtils;
import org.haedal.zzansuni.user.domain.*;
import org.haedal.zzansuni.user.domain.port.UserReader;
import org.haedal.zzansuni.user.domain.port.UserStore;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.util.Pair;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
Expand All @@ -17,36 +18,40 @@
import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
public class AuthService {

private final List<OAuth2Client> oAuth2Clients;
private final BCryptPasswordEncoder passwordEncoder;
private final JwtUtils jwtUtils;
private final UserReader userReader;
private final UserStore userStore;
private final CreateJwtUseCase createJwtUseCase;

/**
* OAuth2 로그인 또는 회원가입 <br> [state]는 nullable한 입력 값이다.<br> 1. OAuth2Client를 이용해 해당 provider로부터
* 유저정보를 가져옴 2. authToken으로 유저를 찾거나 없으면 회원가입 3. 토큰 발급, 유저정보 반환
* OAuth2 로그인 또는 회원가입 <br>
* [state]는 nullable한 입력 값이다.<br>
* 1. OAuth2Client를 이용해 해당 provider로부터 유저정보를 가져옴
* 2. authToken으로 유저를 찾거나 없으면 회원가입
* 3. 토큰 발급, 유저정보 반환
*/
public Pair<JwtToken, UserModel.Main> oAuth2LoginOrSignup(OAuth2Provider provider,
@NonNull String code, @Nullable String state) {
OAuth2Client oAuth2Client = oAuth2Clients.stream()
.filter(client -> client.canHandle(provider))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 OAuth2Provider 입니다."));
.filter(client -> client.canHandle(provider))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 OAuth2Provider 입니다."));

// OAuth2Client를 이용해 해당 provider로부터 유저정보를 가져옴
OAuthUserInfoModel oAuthUserInfoModel = oAuth2Client.getAuthToken(code, state);

// authToken으로 유저를 찾아서 없으면 [OAuthUserInfoModel]를 통해서 회원가입 진행
User user = userReader
.findByAuthToken(oAuthUserInfoModel.authToken())
.orElseGet(() -> signup(oAuthUserInfoModel, provider));
.findByAuthToken(oAuthUserInfoModel.authToken())
.orElseGet(() -> signup(oAuthUserInfoModel, provider));

// 토큰 발급, 유저정보 반환
JwtToken jwtToken = createToken(user);
JwtToken jwtToken = createJwtToken(user);
UserModel.Main userMain = UserModel.Main.from(user);
return Pair.of(jwtToken, userMain);
}
Expand All @@ -58,20 +63,15 @@ private User signup(OAuthUserInfoModel oAuthUserInfoModel, OAuth2Provider provid
return userStore.store(user);
}

private JwtToken createToken(User user) {
JwtUser jwtUser = JwtUser.of(user.getId(), user.getRole());
return jwtUtils.createToken(jwtUser);
}

@Transactional
public Pair<JwtToken, UserModel.Main> signup(UserCommand.Create command) {
if (userReader.existsByEmail(command.getEmail())) {
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
}
command = command.copyEncodedPassword(passwordEncoder.encode(command.getPassword()));
User user = User.create(command);
userStore.store(user);
JwtToken jwtToken = createToken(user);
JwtToken jwtToken = createJwtToken(user);

UserModel.Main userMain = UserModel.Main.from(user);
return Pair.of(jwtToken, userMain);
}
Expand All @@ -86,27 +86,47 @@ public void createManager(UserCommand.Create command) {
userStore.store(user);
}

@Transactional(readOnly = true)
public Pair<JwtToken, UserModel.Main> login(String email, String password) {
User user = userReader.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 이메일입니다."));
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 이메일입니다."));

if (!passwordEncoder.matches(password, user.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}

JwtToken jwtToken = createToken(user);
JwtToken jwtToken = createJwtToken(user);
UserModel.Main userMain = UserModel.Main.from(user);
return Pair.of(jwtToken, userMain);
}

public String reissueToken(String rawToken) {
if (!jwtUtils.validateToken(rawToken)) {
throw new IllegalArgumentException("RefreshToken이 유효하지 않습니다.");
public JwtToken reissueToken(String rawToken) {
JwtUtils.UserIdAndUuid userIdAndUuid = jwtUtils.validateAndGetUserIdAndUuid(rawToken);

for(int i = 0; i < 5; i++) {
try {
return createJwtUseCase.removeRefreshTokenAndCreateJwt(userIdAndUuid);
} catch (DataIntegrityViolationException e) {
log.error("중복된 uuid 발생, 재시도 : {}", i);
}
}
JwtToken.ValidToken token = JwtToken.ValidToken.of(rawToken);
jwtUtils.reissueAccessToken(token);
throw new RuntimeException("로그인 처리중에 문제가 발생하였습니다. 잠시 후 다시 시도해주세요.");
}

return jwtUtils.reissueAccessToken(JwtToken.ValidToken.of(rawToken));
/**
* 중복 uuid 저장이 발생하는 경우를 대비하여 10번까지 시도한다.
* 유저 생성과 리프래시토큰 저장을 한 트랜잭션에서 처리하게 된다면
* 모든 처리가 롤백되어야 한다. <br>
* 데이터베이스에서 발생한 에러는 트랜잭션 상태에 영향을 미쳐 예외가 발생한 후의 추가 처리에 실패한다.
* `Transaction`과 관련된 AOP에서 `noRollbackFor`에서의 에러로 처리가 불가능하다.
*/
private JwtToken createJwtToken(User user) {
for(int i = 0; i < 5; i++) {
try {
return createJwtUseCase.invoke(user);
} catch (DataIntegrityViolationException e) {
log.error("중복된 uuid 발생, 재시도 : {}", i);
}
}
throw new RuntimeException("로그인 처리중에 문제가 발생하였습니다. 잠시 후 다시 시도해주세요.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.haedal.zzansuni.auth.domain;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.haedal.zzansuni.common.domain.UuidHolder;
import org.haedal.zzansuni.global.jwt.JwtToken;
import org.haedal.zzansuni.global.jwt.JwtUser;
import org.haedal.zzansuni.global.jwt.JwtUtils;
import org.haedal.zzansuni.user.domain.User;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Slf4j
@Component
@RequiredArgsConstructor
public class CreateJwtUseCase {
private final JwtUtils jwtUtils;
private final RefreshTokenReader refreshTokenReader;
private final RefreshTokenStore refreshTokenStore;
private final UuidHolder uuidHolder;
/**
* JWT 발급
* 1. 리프래시토큰의 uuid 생성
* 2. JWT 토큰 생성
* 3. DB에 리프래시토큰 정보를 저장
*/
@Transactional
public JwtToken invoke(User user) {
JwtUser jwtUser = JwtUser.of(user.getId(), user.getRole());
String uuid = uuidHolder.random();
JwtToken jwtToken = jwtUtils.generateToken(jwtUser, uuid);
RefreshToken refreshToken = RefreshToken.create(uuid, user, jwtToken.getRefreshTokenExpireAt());
refreshTokenStore.flushSave(refreshToken);
return jwtToken;
}

@Transactional
public JwtToken removeRefreshTokenAndCreateJwt(JwtUtils.UserIdAndUuid userIdAndUuid) {
RefreshToken refreshToken = refreshTokenReader.findById(userIdAndUuid.uuid())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 토큰입니다."));

// jwtUtils에서 이미 검증하였으나, 방어적으로 다시 한번 검증
if (!refreshToken.getUser().getId().equals(userIdAndUuid.userId())) {
throw new IllegalArgumentException("토큰의 유저정보가 일치하지 않습니다.");
} else if (refreshToken.getExpiredAt().isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("만료된 토큰입니다.");
}

refreshTokenStore.delete(refreshToken.getId());
User user = refreshToken.getUser();
return invoke(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.haedal.zzansuni.auth.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.haedal.zzansuni.user.domain.User;

import java.time.LocalDateTime;

@Entity
@Builder
@AllArgsConstructor
@Getter
@NoArgsConstructor
public class RefreshToken {
@Id @Column(columnDefinition = "CHAR(36)")
private String id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@Column(columnDefinition ="TIMESTAMP(0)", nullable = false)
private LocalDateTime expiredAt;

public static RefreshToken create(String id,User user,LocalDateTime refreshTokenExpireAt) {
return RefreshToken.builder()
.id(id)
.user(user)
.expiredAt(refreshTokenExpireAt)
.build();
}

public Long getUserId() {
return user.getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.haedal.zzansuni.auth.domain;

import java.util.List;
import java.util.Optional;

public interface RefreshTokenReader {
Optional<RefreshToken> findById(String id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.haedal.zzansuni.auth.domain;


public interface RefreshTokenStore {
void flushSave(RefreshToken refreshToken);

void delete(String id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.haedal.zzansuni.auth.infrastructure;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.haedal.zzansuni.auth.domain.RefreshToken;
import org.haedal.zzansuni.auth.domain.RefreshTokenReader;
import org.haedal.zzansuni.auth.domain.RefreshTokenStore;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Repository
@RequiredArgsConstructor
public class RefreshTokenReaderStoreImpl implements RefreshTokenReader, RefreshTokenStore {
private final RefreshTokenRepository refreshTokenRepository;
private final EntityManager entityManager;

@Override
@Transactional
public void flushSave(RefreshToken refreshToken) {
entityManager.persist(refreshToken);
entityManager.flush();
}

@Override
public void delete(String id) {
refreshTokenRepository.deleteById(id);
}

@Override
public Optional<RefreshToken> findById(String id) {
return refreshTokenRepository.findById(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.haedal.zzansuni.auth.infrastructure;

import org.haedal.zzansuni.auth.domain.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.haedal.zzansuni.common.domain;

public interface UuidHolder {
String random();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.haedal.zzansuni.common.infrastructure;

import org.haedal.zzansuni.common.domain.UuidHolder;
import org.springframework.stereotype.Component;

@Component
public class SystemUuidHolder implements UuidHolder {
@Override
public String random() {
return java.util.UUID.randomUUID().toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class JwtToken {
private String accessToken;
private String refreshToken;
private LocalDateTime refreshTokenExpireAt;

/**
* 유효한 토큰을 나타내는 VO
Expand Down
Loading

0 comments on commit 1e74ca8

Please sign in to comment.