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

[Feat] 로그인 플로우 변경 #18

Merged
merged 4 commits into from
Aug 30, 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
@@ -0,0 +1,68 @@
package com.join.core.auth.config;

import java.util.Collections;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.session.web.http.CookieHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;
import org.springframework.util.AntPathMatcher;

import com.join.core.common.exception.ErrorCode;
import com.join.core.common.exception.impl.InvalidParamException;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CompositeSessionIdResolver implements HttpSessionIdResolver {

private final String headerName;
private final String authorizationUriPattern;
private final String redirectUriPattern;
private final CookieHttpSessionIdResolver cookieHttpSessionIdResolver = new CookieHttpSessionIdResolver();
private final AntPathMatcher pathMatcher = new AntPathMatcher();

public CompositeSessionIdResolver(String headerName, String authorizationBaseUri, String redirectBaseUri) {
if (StringUtils.isAnyBlank(headerName, authorizationBaseUri, redirectBaseUri)) {
throw new InvalidParamException(ErrorCode.INVALID_PARAMETER, "CompositeSessionIdResolver.String");
}
this.headerName = headerName;
this.authorizationUriPattern = authorizationBaseUri + "/**";
this.redirectUriPattern = redirectBaseUri + "/**";
}

@Override
public List<String> resolveSessionIds(HttpServletRequest request) {
// redirect로 들어오면 쿠키만 확인
if (pathMatcher.match(redirectUriPattern, request.getRequestURI())) {
return cookieHttpSessionIdResolver.resolveSessionIds(request);
}

String headerValue = request.getHeader(this.headerName);
return headerValue != null ? Collections.singletonList(headerValue) : Collections.emptyList();
}

@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
// 인가 요청시 쿠키에 인증용 임시 세션 ID 설정
String uri = request.getRequestURI();
if (pathMatcher.match(authorizationUriPattern, uri)) {
cookieHttpSessionIdResolver.setSessionId(request, response, sessionId);
return;
}

// 리다이렉트 요청시 쿠키에 인증용 임시 세션 만료처리
if (pathMatcher.match(redirectUriPattern, uri)) {
cookieHttpSessionIdResolver.expireSession(request, response);
}

response.setHeader(this.headerName, sessionId);
}

@Override
public void expireSession(HttpServletRequest request, HttpServletResponse response) {
response.setHeader(this.headerName, "");
}
}
21 changes: 16 additions & 5 deletions src/main/java/com/join/core/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -15,7 +16,6 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
Expand All @@ -24,24 +24,35 @@
import com.join.core.auth.handler.JsonLogoutSuccessHandler;
import com.join.core.auth.handler.OAuth2SuccessHandler;
import com.join.core.auth.service.OAuth2UserLoadingService;
import com.join.core.common.exception.ErrorCode;
import com.join.core.common.exception.impl.InvalidParamException;

@Configuration
@EnableMethodSecurity
@EnableWebSecurity
public class SecurityConfig {

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String OAUTH2_AUTHORIZATION_URI_POSTFIX = "/oauth2/authorization";
private static final String OAUTH2_REDIRECT_URI_POSTFIX = "/oauth2/code";
private final OAuth2SuccessHandler oauth2SuccessHandler;
private final OAuth2UserLoadingService oauth2UserLoadingService;
private final JsonLogoutSuccessHandler logoutSuccessHandler;
private final String apiPrefix;
private final String oauth2AuthorizationURI;
private final String oauth2RedirectURI;

public SecurityConfig(OAuth2SuccessHandler oauth2SuccessHandler, OAuth2UserLoadingService oauth2UserLoadingService,
JsonLogoutSuccessHandler logoutSuccessHandler, @Value("${api.prefix}") String apiPrefix) {
if (StringUtils.isBlank(apiPrefix))
throw new InvalidParamException(ErrorCode.INVALID_PARAMETER, "유효하지 않은 API prefix");

this.oauth2SuccessHandler = oauth2SuccessHandler;
this.oauth2UserLoadingService = oauth2UserLoadingService;
this.logoutSuccessHandler = logoutSuccessHandler;
this.apiPrefix = apiPrefix;
this.oauth2AuthorizationURI = apiPrefix + OAUTH2_AUTHORIZATION_URI_POSTFIX;
this.oauth2RedirectURI = apiPrefix + OAUTH2_REDIRECT_URI_POSTFIX;
}

@Bean
Expand All @@ -50,8 +61,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.oauth2Login(oauth ->
oauth.authorizationEndpoint(authorization -> authorization.baseUri(apiPrefix + "/oauth2/authorization"))
.redirectionEndpoint(redirection -> redirection.baseUri(apiPrefix + "/oauth2/code"))
oauth.authorizationEndpoint(authorization -> authorization.baseUri(oauth2AuthorizationURI))
.redirectionEndpoint(redirection -> redirection.baseUri(oauth2RedirectURI))
.userInfoEndpoint(userInfo -> userInfo.userService(oauth2UserLoadingService))
.successHandler(oauth2SuccessHandler))
.logout(logout -> logout.logoutUrl(apiPrefix + "/logout")
Expand All @@ -76,8 +87,8 @@ CorsConfigurationSource websiteConfigurationSource() {
}

@Bean
static HttpSessionIdResolver httpSessionIdResolver() {
return new HeaderHttpSessionIdResolver(AUTHORIZATION_HEADER);
HttpSessionIdResolver httpSessionIdResolver() {
return new CompositeSessionIdResolver(AUTHORIZATION_HEADER, oauth2AuthorizationURI, oauth2RedirectURI);
}

@Bean
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/join/core/auth/constant/UserType.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public OAuth2Attributes extract(String attributeKey, Map<String, Object> attribu
KAKAO("kakao") {
@Override
public OAuth2Attributes extract(String attributeKey, Map<String, Object> attributes) {
String keyAttribute = (String)attributes.get(attributeKey);
String keyAttribute = attributes.get(attributeKey).toString();
Map<String, Object> kakaoAccount = (Map<String, Object>)attributes.get("kakao_account");
Map<String, Object> kakaoProfile = (Map<String, Object>)kakaoAccount.get("profile");

Expand Down
37 changes: 0 additions & 37 deletions src/main/java/com/join/core/auth/controller/OAuth2Controller.java

This file was deleted.

9 changes: 6 additions & 3 deletions src/main/java/com/join/core/auth/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.join.core.common.exception.ErrorCode.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -12,6 +13,7 @@

import com.join.core.auth.constant.UserType;
import com.join.core.avatar.domain.Avatar;
import com.join.core.avatar.domain.ProfilePhoto;
import com.join.core.common.domain.BaseTimeEntity;
import com.join.core.common.exception.impl.InvalidParamException;
import com.join.core.common.util.TokenGenerator;
Expand Down Expand Up @@ -67,17 +69,17 @@ public enum Status {
private boolean termsAgreed;

@NotNull
@OneToOne(mappedBy = "user")
@OneToOne(mappedBy = "user", cascade = CascadeType.PERSIST)
private Avatar avatar;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserRole> userRoles;
private List<UserRole> userRoles = new ArrayList<>();

public boolean isUserOf(UserType providerType) {
return providerType.equals(this.platform);
}

public User(String email, UserType platform) {
public User(String email, UserType platform, ProfilePhoto photo) {
if (StringUtils.isEmpty(email)) throw new InvalidParamException(INVALID_PARAMETER, "User.email");
if (platform == null) throw new InvalidParamException(INVALID_PARAMETER, "User.platform");

Expand All @@ -87,6 +89,7 @@ public User(String email, UserType platform) {
this.singUpDate = LocalDateTime.now();
this.status = Status.PENDING;
this.termsAgreed = false;
this.avatar = new Avatar(this, photo);
}

public void addRole(Role role) {
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/com/join/core/auth/model/OAuth2Attributes.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.join.core.auth.constant.UserType;
import com.join.core.auth.domain.User;
import com.join.core.avatar.domain.ProfilePhoto;

import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -41,8 +42,8 @@ public Map<String, Object> toMap() {
"profileImage", profileImage);
}

public User toEntity() {
return new User(email, platform);
public User toEntity(ProfilePhoto photo) {
return new User(email, platform, photo);
}

public boolean hasImage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,28 @@
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Component
public class UserReaderImpl implements UserReader {

private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final UserInfoMapper userInfoMapper;

@Transactional(readOnly = true)
@Override
public UserInfo.SigIn getSignInInfo(String email) {
return userRepository.findByEmail(email)
.map(user -> userInfoMapper.of(user, false))
.orElseGet(UserInfo.SigIn::unregistered);
}

@Transactional(readOnly = true)
@Transactional
@Override
public Role getGuestRole() {
return getRole(Role.Type.GUEST);
}

@Transactional(readOnly = true)
@Transactional
@Override
public Role getUserRole() {
return getRole(Role.Type.USER);
Expand Down

This file was deleted.

25 changes: 17 additions & 8 deletions src/main/java/com/join/core/auth/repository/UserStoreImpl.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
package com.join.core.auth.repository;

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

import com.join.core.auth.domain.User;
import com.join.core.auth.domain.UserInfo;
import com.join.core.auth.domain.UserInfoMapper;
import com.join.core.auth.model.OAuth2Attributes;
import com.join.core.auth.service.UserReader;
import com.join.core.auth.service.UserStore;
import com.join.core.avatar.domain.Avatar;
import com.join.core.avatar.domain.ProfilePhoto;
import com.join.core.file.domain.ImageFile;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class UserStoreImpl implements UserStore {

private static final String DEFAULT_PROFILE_PHOTO_URL = "https://fastly.picsum.photos/id/688/200/200.jpg?hmac=SPM6DXITCd9R3P5BMqgFMw6QdW-SJ2mPKUvq2g9eF-g";
private final UserRepository userRepository;
private final AvatarRepository avatarRepository;
private final UserInfoMapper userInfoMapper;
private final UserReader userReader;

@Transactional
@Override
public User store(User user) {
return userRepository.save(user);
public UserInfo.SigIn store(OAuth2Attributes attributes) {
String photoUrl = attributes.hasImage() ? attributes.getProfileImage() : DEFAULT_PROFILE_PHOTO_URL;
ProfilePhoto photo = new ProfilePhoto(ImageFile.externalImage(photoUrl));
User user = userRepository.save(attributes.toEntity(photo));
user.addRole(userReader.getGuestRole());
return userInfoMapper.of(user, true);
}

@Override
public Avatar store(Avatar avatar) {
return avatarRepository.save(avatar);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic

OAuth2Attributes attributes = type.extract(userNameAttributeName, oAuth2User.getAttributes());

return new DefaultOAuth2User(List.of(), attributes.toMap(), oAuth2User.getName());
return new DefaultOAuth2User(List.of(), attributes.toMap(), "key");
}

}
Loading
Loading