Skip to content

Commit

Permalink
[refac] 인증 비즈니스 로직 리팩토링 (#253)
Browse files Browse the repository at this point in the history
* [refac] make user info dto

* [refac] extract external service from transaction

* [refac] handle feign exception

* [refac] rename variable

* [refac] apply facade pattern

* [refac] use final keyword

* [refac] use final keyword

* [refac] delete unnecessary transaction

* [refac] delete unnecessary logout logic in validation

* [refac] add transactional annotation

* [refac] extract external network from transactional

* [refac] rename class

* [refac] move package

* [docs] update README.md

* [refac] delete static method

* [fix] validate null name

* [refac] separate responsibilities by creating dedicated classes

* [refac] delete AuthValidator.java

* [refac] summarize parameter

* [refac] refactor to use string constants

* [refac] move location of validation logic

* [refac] divide method

* [fix] Revoke refresh token renewal on access token reissuance

* [fix] change access modifier to public for apply transactional annotation
  • Loading branch information
kgy1008 authored Jan 27, 2025
1 parent 223e05d commit 08a80e0
Show file tree
Hide file tree
Showing 26 changed files with 199 additions and 128 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@


# 🏗️Architecture
![한끼아키텍쳐](https://github.com/user-attachments/assets/e2c71966-893a-478e-bb68-26f66ecd17de)
![한끼아키텍쳐](https://github.com/user-attachments/assets/3ed4c337-3e4f-426b-999e-8f1c0c796cd8)

<br><br>
# 📍ERD
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.hankki.hankkiserver.api.auth.controller.request.UserLoginRequest;
import org.hankki.hankkiserver.api.auth.service.AuthService;
import org.hankki.hankkiserver.api.auth.service.AuthFacade;
import org.hankki.hankkiserver.api.auth.service.response.UserLoginResponse;
import org.hankki.hankkiserver.api.auth.service.response.UserReissueResponse;
import org.hankki.hankkiserver.api.common.annotation.UserId;
Expand All @@ -24,32 +24,31 @@
@RequestMapping("/api/v1")
public class AuthController {

private final AuthService authService;
private final AuthFacade authFacade;

@PostMapping("/auth/login")
public HankkiResponse<UserLoginResponse> login(@RequestHeader(HttpHeaders.AUTHORIZATION) final String token,
@Valid @RequestBody final UserLoginRequest request) {
UserLoginResponse response = authService.login(token, request);
UserLoginResponse response = authFacade.login(token, request);
return HankkiResponse.success(CommonSuccessCode.OK, response);
}

@PatchMapping("/auth/logout")
public HankkiResponse<Void> signOut(@UserId final Long userId) {
authService.logout(userId);
authFacade.logout(userId);
return HankkiResponse.success(CommonSuccessCode.OK);
}

@DeleteMapping("/auth/withdraw")
public HankkiResponse<Void> withdraw(@UserId final Long userId,
@Nullable @RequestHeader("X-Apple-Code") final String code) {
authService.withdraw(userId, code);
authFacade.withdraw(userId, code);
return HankkiResponse.success(CommonSuccessCode.NO_CONTENT);
}

@PostMapping("/auth/reissue")
public HankkiResponse<UserReissueResponse> reissue(
@RequestHeader(HttpHeaders.AUTHORIZATION) final String refreshToken) {
UserReissueResponse response = authService.reissue(refreshToken);
public HankkiResponse<UserReissueResponse> reissue(@RequestHeader(HttpHeaders.AUTHORIZATION) final String token) {
UserReissueResponse response = authFacade.reissue(token);
return HankkiResponse.success(CommonSuccessCode.OK, response);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package org.hankki.hankkiserver.api.auth.controller.request;

import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.hankki.hankkiserver.domain.user.model.Platform;

public record UserLoginRequest(
@Nullable
String name,
@NotBlank
String platform
@NotNull
Platform platform
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.hankki.hankkiserver.api.auth.service;

import lombok.RequiredArgsConstructor;
import org.hankki.hankkiserver.api.auth.controller.request.UserLoginRequest;
import org.hankki.hankkiserver.api.auth.service.response.UserInfoResponse;
import org.hankki.hankkiserver.api.auth.service.response.UserLoginResponse;
import org.hankki.hankkiserver.api.auth.service.response.UserReissueResponse;
import org.hankki.hankkiserver.api.user.service.UserFinder;
import org.hankki.hankkiserver.api.user.service.UserInfoFinder;
import org.hankki.hankkiserver.auth.jwt.Token;
import org.hankki.hankkiserver.domain.user.model.User;
import org.hankki.hankkiserver.domain.user.model.UserInfo;
import org.hankki.hankkiserver.external.openfeign.oauth.SocialInfoResponse;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class AuthFacade {

private final UserFinder userFinder;
private final UserInfoFinder userInfoFinder;
private final ExternalService externalService;
private final AuthService authService;

public UserLoginResponse login(final String token, final UserLoginRequest request) {
SocialInfoResponse response = externalService.getUserInfo(token, request.platform(), request.name());
UserInfoResponse userInfoResponse = UserInfoResponse.of(request.platform(), response.serialId(),
response.name(), response.email());
return authService.saveOrGetUser(userInfoResponse);
}

public void withdraw(final long userId, final String code) {
User user = userFinder.getUser(userId);
externalService.revoke(user.getPlatform(), code, user.getSerialId());
authService.deleteUser(user);
}

@Transactional
public void logout(final long userId) {
UserInfo findUserInfo = userInfoFinder.getUserInfo(userId);
findUserInfo.updateRefreshToken(null);
}

@Transactional
public UserReissueResponse reissue(final String refreshToken) {
Token issuedTokens = authService.generateAccessToken(refreshToken);
return UserReissueResponse.of(issuedTokens);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@

import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.hankki.hankkiserver.api.auth.controller.request.UserLoginRequest;
import org.hankki.hankkiserver.api.auth.service.response.UserInfoResponse;
import org.hankki.hankkiserver.api.auth.service.response.UserLoginResponse;
import org.hankki.hankkiserver.api.auth.service.response.UserReissueResponse;
import org.hankki.hankkiserver.api.user.service.UserFinder;
import org.hankki.hankkiserver.api.user.service.UserInfoFinder;
import org.hankki.hankkiserver.api.user.service.UserInfoUpdater;
import org.hankki.hankkiserver.api.user.service.UserUpdater;
import org.hankki.hankkiserver.auth.jwt.JwtProvider;
import org.hankki.hankkiserver.auth.jwt.JwtValidator;
import org.hankki.hankkiserver.auth.jwt.Token;
import org.hankki.hankkiserver.common.exception.UnauthorizedException;
import org.hankki.hankkiserver.domain.user.model.Platform;
import org.hankki.hankkiserver.domain.user.model.User;
import org.hankki.hankkiserver.domain.user.model.UserInfo;
import org.hankki.hankkiserver.domain.user.model.UserStatus;
import org.hankki.hankkiserver.event.EventPublisher;
import org.hankki.hankkiserver.event.user.CreateUserEvent;
import org.hankki.hankkiserver.external.openfeign.oauth.SocialInfoResponse;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -33,44 +34,29 @@ public class AuthService {
private final UserInfoUpdater userInfoUpdater;
private final JwtProvider jwtProvider;
private final JwtValidator jwtValidator;
private final OAuthProviderFactory oAuthProviderFactory;
private final EventPublisher eventPublisher;

@Transactional
public UserLoginResponse login(final String token, final UserLoginRequest request) {
Platform platform = Platform.getEnumPlatformFromStringPlatform(request.platform());
SocialInfoResponse socialInfo = getSocialInfo(token, platform, request.name());
Optional<User> user = userFinder.findUserByPlatFormAndSeralId(platform, socialInfo.serialId());
public UserLoginResponse saveOrGetUser(final UserInfoResponse userInfo) {
Optional<User> user = userFinder.findUserByPlatFormAndSeralId(userInfo.platform(), userInfo.serialId());
boolean isRegistered = isRegistered(user);
User findUser = loadOrCreateUser(user, platform, socialInfo);
User findUser = loadOrCreateUser(user, userInfo);
Token issuedToken = generateTokens(findUser.getId());
return UserLoginResponse.of(issuedToken, isRegistered);
}

@Transactional
public void logout(final long userId) {
UserInfo findUserInfo = userInfoFinder.getUserInfo(userId);
findUserInfo.updateRefreshToken(null);
}

@Transactional
public void withdraw(final long userId, final String code) {
User user = userFinder.getUser(userId);
Platform platform = user.getPlatform();
OAuthProvider oAuthProvider = oAuthProviderFactory.findProvider(platform);
oAuthProvider.requestRevoke(code, user.getSerialId());
public void deleteUser(final User user) {
user.softDelete();
userInfoFinder.getUserInfo(userId).softDelete();
userInfoFinder.getUserInfo(user.getId()).softDelete();
}

@Transactional
public UserReissueResponse reissue(final String refreshToken) {
Long userId = jwtProvider.getSubject(refreshToken.substring(BEARER.length()));
protected Token generateAccessToken(final String refreshToken) {
String strippedToken = refreshToken.substring(BEARER.length());
long userId = jwtProvider.getSubject(strippedToken);
validateRefreshToken(refreshToken, userId);
UserInfo findUserInfo = userInfoFinder.getUserInfo(userId);
Token issuedTokens = jwtProvider.issueTokens(userId, getUserRole(userId));
findUserInfo.updateRefreshToken(issuedTokens.refreshToken());
return UserReissueResponse.of(issuedTokens);
String accessToken = jwtProvider.generateAccessToken(userId, getUserRole(userId));
return Token.of(accessToken, strippedToken);
}

private Token generateTokens(final long userId) {
Expand All @@ -80,63 +66,49 @@ private Token generateTokens(final long userId) {
return issuedTokens;
}

private String getUserRole(final long userId) {
return userFinder.getUser(userId).getRole().getValue();
private void validateRefreshToken(final String refreshToken, final long userId) {
jwtValidator.validateRefreshToken(refreshToken);
String storedRefreshToken = userInfoFinder.getUserInfo(userId).getRefreshToken();
jwtValidator.checkTokenEquality(refreshToken, storedRefreshToken);
}

private boolean isRegistered(final Optional<User> user) {
return user.map(u -> u.getStatus() == ACTIVE)
.orElse(false);
}

private SocialInfoResponse getSocialInfo(final String providerToken, final Platform platform, final String name) {
OAuthProvider oAuthProvider = oAuthProviderFactory.findProvider(platform);
return oAuthProvider.getUserInfo(providerToken, name);
}

private User loadOrCreateUser(final Optional<User> findUser, final Platform platform, final SocialInfoResponse socialInfo) {
return findUser.map(user -> updateOrGetUserInfo(user, user.getStatus(), socialInfo))
.orElseGet(() -> createNewUser(socialInfo, platform));
private User loadOrCreateUser(final Optional<User> findUser, final UserInfoResponse userInfo) {
return findUser.map(user -> updateOrGetUserInfo(user, user.getStatus(), userInfo))
.orElseGet(() -> createNewUser(userInfo, userInfo.platform()));
}

private User updateOrGetUserInfo(final User user, final UserStatus status, final SocialInfoResponse socialInfo) {
private User updateOrGetUserInfo(final User user, final UserStatus status, final UserInfoResponse userInfo) {
if (status == ACTIVE) {
return user;
}
return updateUserInfo(user, socialInfo);
return updateUserInfo(user, userInfo);
}

private User updateUserInfo(final User user, final SocialInfoResponse socialInfo) {
user.rejoin(socialInfo);
userInfoFinder.getUserInfo(user.getId()).updateNickname(socialInfo.name());
private User updateUserInfo(final User user, final UserInfoResponse userInfo) {
user.rejoin(userInfo.name(), userInfo.email());
userInfoFinder.getUserInfo(user.getId()).updateNickname(userInfo.name());
return user;
}

private User createNewUser(final SocialInfoResponse socialInfo, final Platform platform) {
User newUser = createUser(socialInfo.name(), socialInfo.email(), socialInfo.serialId(), platform);
private User createNewUser(final UserInfoResponse userInfo, final Platform platform) {
User newUser = createUser(userInfo.name(), userInfo.email(), userInfo.serialId(), platform);
saveUserAndUserInfo(newUser);
eventPublisher.publish(CreateUserEvent.of(newUser.getId(), newUser.getName(), newUser.getPlatform().toString()));
return newUser;
}

private String getRefreshToken(final long userId) {
return userInfoFinder.getUserInfo(userId).getRefreshToken();
private String getUserRole(final long userId) {
return userFinder.getUser(userId).getRole().getValue();
}

private void saveUserAndUserInfo(final User user) {
userUpdater.saveUser(user);
UserInfo userInfo = UserInfo.createMemberInfo(user, null);
userInfoUpdater.saveUserInfo(userInfo);
}

private void validateRefreshToken(final String refreshToken, final Long userId) {
try {
jwtValidator.validateRefreshToken(refreshToken);
String storedRefreshToken = getRefreshToken(userId);
jwtValidator.equalsRefreshToken(refreshToken, storedRefreshToken);
} catch (UnauthorizedException e) {
logout(userId);
throw e;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.hankki.hankkiserver.api.auth.service;

import lombok.RequiredArgsConstructor;
import org.hankki.hankkiserver.domain.user.model.Platform;
import org.hankki.hankkiserver.external.openfeign.oauth.SocialInfoResponse;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ExternalService {

private final OAuthProviderFactory oAuthProviderFactory;

protected SocialInfoResponse getUserInfo(final String token, final Platform platform, final String name) {
OAuthProvider oAuthProvider = oAuthProviderFactory.findProvider(platform);
return oAuthProvider.getUserInfo(token, name);
}

protected void revoke(final Platform platform, final String code, final String serialId) {
OAuthProvider oAuthProvider = oAuthProviderFactory.findProvider(platform);
oAuthProvider.requestRevoke(code, serialId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.hankki.hankkiserver.api.auth.service.response;

import org.hankki.hankkiserver.domain.user.model.Platform;

public record UserInfoResponse(
Platform platform,
String serialId,
String name,
String email
) {
public static UserInfoResponse of(final Platform platform, final String serialId, final String name, final String email) {
return new UserInfoResponse(platform, serialId, name, email);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.hankki.hankkiserver.api.common.advice;

import feign.FeignException;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.hankki.hankkiserver.api.dto.HankkiResponse;
import org.hankki.hankkiserver.common.code.AuthErrorCode;
import org.hankki.hankkiserver.common.code.BusinessErrorCode;
import org.hankki.hankkiserver.common.code.StoreErrorCode;
import org.hankki.hankkiserver.common.code.StoreImageErrorCode;
Expand Down Expand Up @@ -108,4 +110,10 @@ public HankkiResponse<Void> handleConstraintViolationException(ConstraintViolati
log.warn("handleConstraintViolationException() in GlobalExceptionHandler throw ConstraintViolationException : {}", e.getMessage());
return HankkiResponse.fail(BusinessErrorCode.BAD_REQUEST);
}

@ExceptionHandler(FeignException.class)
public HankkiResponse<Void> handleFeignException(FeignException e) {
log.warn("handleFeignException() in GlobalExceptionHandler throw FeignException : {}", e.getMessage());
return HankkiResponse.fail(AuthErrorCode.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.hankki.hankkiserver.api.config;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.hankki.hankkiserver.api.common.PriceCategoryConverter;
import org.hankki.hankkiserver.api.common.UserIdArgumentResolver;
Expand All @@ -8,13 +9,12 @@
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

private final UserIdArgumentResolver userIdArgumentResolver;
private final PriceCategoryConverter priceCategoryConverter;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
Expand All @@ -23,6 +23,6 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new PriceCategoryConverter());
registry.addConverter(priceCategoryConverter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.hankki.hankkiserver.api.auth.service.UserFinder;
import org.hankki.hankkiserver.api.user.service.UserFinder;
import org.hankki.hankkiserver.api.favorite.service.command.FavoriteSharedPostCommand;
import org.hankki.hankkiserver.api.favorite.service.command.FavoriteStoreDeleteCommand;
import org.hankki.hankkiserver.api.favorite.service.command.FavoriteStorePostCommand;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.util.ArrayList;
import lombok.RequiredArgsConstructor;
import org.hankki.hankkiserver.api.auth.service.UserInfoFinder;
import org.hankki.hankkiserver.api.user.service.UserInfoFinder;
import org.hankki.hankkiserver.api.favorite.service.command.FavoriteOwnershipGetCommand;
import org.hankki.hankkiserver.api.favorite.service.command.FavoritesGetCommand;
import org.hankki.hankkiserver.api.favorite.service.command.FavoritesWithStatusGetCommand;
Expand Down
Loading

0 comments on commit 08a80e0

Please sign in to comment.