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

Issue/#78 #85

Merged
merged 8 commits into from
Sep 5, 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
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
Loading