Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/user-service-rest-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
- name: Upload OpenApi Specification to S3
working-directory: user-service
run: |
aws s3 cp build/resources/main/static/docs/user-service.json s3://mybrary-swagger-ui/docs/user-service.json
aws s3 cp build/resources/main/static/docs/user-service.json s3://mybrary-api-docs/docs/user-service.json
env:
AWS_ACCESS_KEY_ID: ${{ secrets.OPEN_API_GIT_ACTIONS_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.OPEN_API_GIT_ACTIONS_SECRET_KEY }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public static OAuthAttributes of(SocialType socialType, String userNameAttribute
if (socialType == SocialType.NAVER) {
return ofNaver(userNameAttributeName, attributes);
}
if (socialType == SocialType.APPLE) {
return ofApple(userNameAttributeName, attributes);
}
throw new OAuth2AuthenticationException(SOCIAL_TYPE_NOT_SUPPORTED);
}

Expand Down Expand Up @@ -66,6 +69,13 @@ private static OAuthAttributes ofNaver(String userNameAttributeName,
.build();
}

private static OAuthAttributes ofApple(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.nameAttributeKey(userNameAttributeName)
.oAuth2UserInfo(new AppleOAuth2UserInfoV2(attributes))
.build();
}

public User toEntity(SocialType socialType, OAuth2UserInfo oAuth2UserInfo) {
return User.builder()
.socialType(socialType)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kr.mybrary.userservice.authentication.domain.oauth2.converter;

import kr.mybrary.userservice.authentication.domain.oauth2.service.AppleOAuth2UtilService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
import org.springframework.util.MultiValueMap;

@Slf4j
@RequiredArgsConstructor
public class CustomRequestEntityConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {

private final OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;
private final AppleOAuth2UtilService appleOAuth2UtilService;

@Override
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest request) {
RequestEntity<?> entity = defaultConverter.convert(request);
String registrationId = request.getClientRegistration().getRegistrationId();
MultiValueMap<String, String> params = (MultiValueMap<String, String>) entity.getBody();
if(registrationId.contains("apple")) {
params.set("client_secret", appleOAuth2UtilService.createAppleClientSecret(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r3 : 매직넘버를 상수로 처리하면 좋을 것 같습니다.

request.getClientRegistration().getClientId(), request.getClientRegistration().getClientSecret()));
}
return new RequestEntity<>(params, entity.getHeaders(), entity.getMethod(), entity.getUrl());
}
}
Original file line number Diff line number Diff line change
@@ -1,63 +1,59 @@
package kr.mybrary.userservice.authentication.domain.oauth2.service;

import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import kr.mybrary.userservice.authentication.domain.exception.AppleTokenParseException;
import kr.mybrary.userservice.authentication.domain.oauth2.CustomOAuth2User;
import kr.mybrary.userservice.authentication.domain.oauth2.OAuthAttributes;
import kr.mybrary.userservice.user.persistence.SocialType;
import kr.mybrary.userservice.user.persistence.User;
import kr.mybrary.userservice.user.persistence.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import static kr.mybrary.userservice.global.constant.ImageConstant.*;

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final ObjectMapper objectMapper;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService;

private static final String GOOGLE = "google";
private static final String KAKAO = "kakao";
private static final String NAVER = "naver";
private static final String APPLE = "apple";
private static final String SOCIAL_TYPE_NOT_SUPPORTED = "지원하지 않는 소셜 로그인입니다.";

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
log.info("CustomOAuth2UserService.loadUser() 실행 - OAuth2 로그인 요청 진입");
SocialType socialType = getSocialType(userRequest.getClientRegistration().getRegistrationId());

Map<String, Object> attributes = getAttributesFromUserRequest(userRequest, socialType);

/* DefaultOAuth2UserService의 loadUser()는 소셜 로그인 API의 사용자 정보 제공 URI로 요청을 보내서
사용자 정보를 얻은 후, 이를 통해 DefaultOAuth2User 객체를 생성 후 반환한다.
결과적으로, OAuth2User는 OAuth 서비스에서 가져온 유저 정보를 담고 있는 유저 */
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);

// userRequest에서 registrationId, socialType, userNameAttributeName을 가져온다.
String registrationId = userRequest.getClientRegistration().getRegistrationId();
SocialType socialType = getSocialType(registrationId);
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
Map<String, Object> attributes = oAuth2User.getAttributes();

// socialType에 따라 유저 정보를 통해 OAuthAttributes 객체를 생성한다.
OAuthAttributes extractAttributes = OAuthAttributes.of(socialType, userNameAttributeName,
OAuthAttributes extractAttributes = OAuthAttributes.of(socialType,
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(),
attributes);

// OAuthAttributes 객체와 SocialType를 통해 User 객체를 생성한다.
User createdUser = getUser(extractAttributes, socialType);

// DefaultOAuth2User를 구현한 CustomOAuth2User 객체를 생성하여 반환한다.
return new CustomOAuth2User(
Collections.singleton(
new SimpleGrantedAuthority(createdUser.getRole().getDescription())),
Expand All @@ -66,7 +62,34 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) {
createdUser.getLoginId(),
createdUser.getRole()
);
}

private Map<String, Object> getAttributesFromUserRequest(OAuth2UserRequest userRequest, SocialType socialType) {
if(socialType == SocialType.APPLE) {
return getAttributesFromApple(userRequest);
} else {
return oAuth2UserService.loadUser(userRequest).getAttributes();
}
}

private Map<String, Object> getAttributesFromApple(OAuth2UserRequest userRequest) {
Map<String, Object> tokenInfo = new HashMap<>();
JSONObject payload = parseAppleToken(userRequest.getAdditionalParameters().get("id_token").toString());

tokenInfo.put("sub", String.valueOf(payload.get("sub")));
tokenInfo.put("email", String.valueOf(payload.get("email")));
tokenInfo.put("emailId", String.valueOf(payload.get("email")).split("@")[0]);
return tokenInfo;
}

private JSONObject parseAppleToken(String idToken) {
try {
SignedJWT signedJWT = SignedJWT.parse(idToken);
JWTClaimsSet getPayload = signedJWT.getJWTClaimsSet();
return objectMapper.convertValue(getPayload.getClaims(), JSONObject.class);
} catch (ParseException e) {
throw new AppleTokenParseException();
}
}

private SocialType getSocialType(String registrationId) {
Expand All @@ -79,6 +102,9 @@ private SocialType getSocialType(String registrationId) {
if (registrationId.equals(NAVER)) {
return SocialType.NAVER;
}
if(registrationId.equals(APPLE)) {
return SocialType.APPLE;
}
throw new OAuth2AuthenticationException(SOCIAL_TYPE_NOT_SUPPORTED);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.mybrary.userservice.authentication.domain.oauth2.userinfo;

import java.util.Map;

public class AppleOAuth2UserInfoV2 extends OAuth2UserInfo {

public AppleOAuth2UserInfoV2(Map<String, Object> attributes) {
super(attributes);
}

@Override
public String getId() {
return (String) attributes.get("sub");
}

@Override
public String getNickname() {
return (String) attributes.get("emailId");
}

@Override
public String getEmail() {
return (String) attributes.get("email");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import kr.mybrary.userservice.authentication.domain.login.handler.LoginSuccessHandler;
import kr.mybrary.userservice.authentication.domain.logout.filter.LogoutExceptionFilter;
import kr.mybrary.userservice.authentication.domain.logout.handler.CustomLogoutHandler;
import kr.mybrary.userservice.authentication.domain.oauth2.converter.CustomRequestEntityConverter;
import kr.mybrary.userservice.authentication.domain.oauth2.handler.OAuth2LoginFailureHandler;
import kr.mybrary.userservice.authentication.domain.oauth2.handler.OAuth2LoginSuccessHandler;
import kr.mybrary.userservice.authentication.domain.oauth2.service.AppleOAuth2UtilService;
import kr.mybrary.userservice.authentication.domain.oauth2.service.CustomOAuth2UserService;
import kr.mybrary.userservice.global.util.JwtUtil;
import kr.mybrary.userservice.global.util.RedisUtil;
import kr.mybrary.userservice.user.persistence.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -25,6 +28,14 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
Expand All @@ -39,7 +50,8 @@ public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
private final RedisUtil redisUtil;
private final CustomOAuth2UserService customOAuth2UserService;
private final UserRepository userRepository;
private final AppleOAuth2UtilService appleOAuth2UtilService;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;

Expand All @@ -58,8 +70,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.oauth2Login(oauth2 -> oauth2
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService))
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService()))
.tokenEndpoint(token -> token.accessTokenResponseClient(accessTokenResponseClient()))
)
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
Expand All @@ -74,6 +86,27 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.build();
}

@Bean CustomOAuth2UserService customOAuth2UserService() {
return new CustomOAuth2UserService(userRepository, passwordEncoder, objectMapper, oAuth2UserService());
}

@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService() {
return new DefaultOAuth2UserService();
}

@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient(){
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter(oAuth2AuthorizationCodeGrantRequestEntityConverter(), appleOAuth2UtilService));
return accessTokenResponseClient;
}

@Bean
public OAuth2AuthorizationCodeGrantRequestEntityConverter oAuth2AuthorizationCodeGrantRequestEntityConverter() {
return new OAuth2AuthorizationCodeGrantRequestEntityConverter();
}

@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
Expand Down