로그인 기능 적용하면서 새롬넴 코드 션하게 날려벌임..😇
스프링에서 소셜로그인을 구현하는 방법은 2가지가 있다.
인가코드 받기, 토큰 받기, 로그인처리까지 크게 3가지로 나뉜다.
소셜로그인 흐름을 확실하게 파악할 수 있다는 장점이 있지만,
소셜마다 제공하는 API가 다르기 때문에 여러개라면 각각 처리해야 한다는 단점이 있다.
스프링부트에서 사용할 수 있는 라이브러리로, oauth2와 관련된 설정만 application-properties
파일에 입력하면 스프링이 인가과정을 도와준다.
따라서 구현에 용이하지만, 흐름을 파악하기 힘들다는 단점도 있다.
우리의 경우, 스프링에서 제공하는 라이브러리를 사용하기로 결정했다.
아래와 같은 이유가 있다.
인증이 선행되어야, 인가처리를 할 수 있다.
소셜로그인 간단 흐름
유저가 직접 소셜 로그인을 통해 확실한 사람인지 인증받는다.
→ 우리 서비스는 이 과정에서 포함되지 않는다.
인증이 유효하면, 유저의 권한을 서비스에게 인가해준다.
→ 우리 서비스가 유저의 소셜서비스를 대행할 수 있다.
→ 이 때, 유저는 권한을 부여하는 과정에서 포함되지 않는다.
Resource Owner
: 유저Client
: 우리 서비스Authorization/Resource Server
: 소셜(구글, 카카오, 깃헙 등)ClientID
: 서비스의 식별자SecretKey
: 서비스가 신뢰할 수 있는지 확인하는 비밀번호와 같은 역할RedirectURI
: 권한을 부여할 때, 서비스가 받는 URI 크롬(User-Agent)
에 접속해서 서비스(Client)
에 로그인하기 위해 소셜로그인 기능을 사용한다.소셜(Authorization)
에서 로그인 페이지를 제공하여 사용자가 인증(Authentication)을 한다.소셜(AuthorizationServer)
사이에서 생성된 Authorization Code를 갖고, 서비스(Client)
는 소셜(AuthorizationServer)
에게 AccessToken(RefreshToken)을 발급받는다.spring:
security:
oauth2:
client:
registration:
github:
client-id: ${client_id}
client-secret: ${secret_key}
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- read:user
- user:email
provider:
github:
authorization-uri: https://github.com/login/oauth/authorize
token-uri: https://github.com/login/oauth/access_token
user-info-uri: https://api.github.com/user
user-name-attribute: id
userName
, Email
만 가져올 예정이다.implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private static final String PERMITTED_ROLES[] = {"USER", "ADMIN"};
private final JwtTokenProvider jwtTokenProvider;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
CustomOAuth2UserService customOAuth2UserService) throws Exception {
return http
.httpBasic(HttpBasicConfigurer::disable)
// 모든 요청에 대해 보안 정책을 적용함 (securityMatcher 선택적)
.securityMatcher((request -> true))
// CSRF 보호 비활성화 (JWT 세션을 사용하지 않기 때문에 필요 없음)
.csrf(AbstractHttpConfigurer::disable)
// OAuth 사용으로 인해 기본 로그인 비활성화
.formLogin(FormLoginConfigurer::disable)
// 세션 사용 안함 (STATELESS)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// HTTP 요청에 대한 접근 권한을 설정
.authorizeHttpRequests(request -> request
.requestMatchers("/oauth2/**", "/login/oauth2/code/**").permitAll()
.requestMatchers("/subscription/**").permitAll()
.anyRequest().hasAnyRole(PERMITTED_ROLES)
)
// OAuth2 처리
.oauth2Login(oauth2 -> oauth2
/** .loginPage("/login") -> 커스텀 로그인 페이지 **/
.successHandler(oAuth2LoginSuccessHandler)
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
.userService(customOAuth2UserService)
)
//.defaultSuccessUrl("/home", true) // 로그인 성공 후 이동할 URL
)
// JWT 인증 필터 등록
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class)
// 최종 SecurityFilterChain 반환
.build();
}
}
loginPage
: 커스텀 로그인 페이지successHandler
: OAuth2 로그인이 성공했을 때 실행되는 클래스failureHandler
: OAuth2 로그인이 실패했을 때 실행되는 클래스userInfoEndPoint
: 소셜로부터 받은 사용자정보를 누가 처리할지 명시defaultSuccesUrl
: 로그인 성공하면 이동할 페이지@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
// 서비스를 구분하는 아이디 ex) Kakao, Github ...
String registrationId = userRequest.getClientRegistration().getRegistrationId();
SocialType socialType = SocialType.from(registrationId);
// 서비스에서 제공받은 데이터
Map<String, Object> attributes = oAuth2User.getAttributes();
OAuth2Response oAuth2Response = getOAuth2Response(socialType, attributes);
userRepository.validateSocialJoinEmail(oAuth2Response.getEmail(), socialType);
User loginUser = getUser(oAuth2Response);
return new AuthUser(loginUser);
}
// 소셜에 알맞는 OAuth2 응답객체를 생성하여 반환함
private OAuth2Response getOAuth2Response(SocialType socialType, Map<String, Object> attributes) { ... }
// OAuth2 응답객체로 User 찾아서 없으면 생성하고 있으면 해당 객체를 반환함
private User getUser(OAuth2Response oAuth2Response) { ... }
}
CustomOAuth2UserService
구현체가 실행이 된다.OAuth2User
는 OAuth 로그인 시 받아오는 사용자 정보를 담은 인터페이스getAttributes()
에서 소셜 플랫폼마다 다른 사용자 정보를 Map 형태로 제공해줌.userRequest
: 현재 OAuth2 로그인 요청에 대한 정보를 담고 있음.getClientRegistration()
: application.yml (또는 properties)에 등록한 소셜 로그인 설정 정보를 가져옴.getRegistrationId()
: application.yml
에서 정의한 소셜 이름(ex: Kakao, Github)을 가져옴.@RequiredArgsConstructor
@Getter
public enum SocialType {
KAKAO("kakao_account", "id", "email"),
GITHUB(null, "id", "login");
private final String attributeKey; //소셜로부터 전달받은 데이터를 Parsing하기 위해 필요한 key 값,
// kakao는 kakao_account안에 필요한 정보들이 담겨져있음.
private final String providerCode; // 각 소셜은 판별하는 판별 코드,
private final String identifier; // 소셜로그인을 한 사용자의 정보를 불러올 때 필요한 Key 값
// 어떤 소셜로그인에 해당하는지 찾는 정적 메서드
public static SocialType from(String provider) {
String upperCastedProvider = provider.toUpperCase();
return Arrays.stream(SocialType.values())
.filter(item -> item.name().equals(upperCastedProvider))
.findFirst()
.orElseThrow();
}
}
public interface OAuth2Response {
// 소셜타입을 반환
SocialType getProvider();
// 이메일 값을 반환
String getEmail();
String getName();
}
implements OAuth2Response
)@RequiredArgsConstructor
public class OAuth2GithubResponse implements OAuth2Response{
private final Map<String, Object> attributes;
@Override
public SocialType getProvider() {
return SocialType.GITHUB;
}
@Override
public String getEmail() {
try {
return (String) attributes.get("email");
} catch (Exception e){
throw new IllegalStateException("깃허브 계정정보에 이메일이 존재하지 않습니다.");
}
}
@Override
public String getName() {
try {
String name = (String) attributes.get("name");
return name != null ? name : (String) attributes.get("login");
} catch (Exception e){
throw new IllegalStateException("깃허브 계정정보에 이름이 존재하지 않습니다.");
}
}
}
implements OAuth2Response
)@RequiredArgsConstructor
public class OAuth2KakaoResponse implements OAuth2Response{
private final Map<String, Object> attributes;
@Override
public SocialType getProvider() {
return SocialType.KAKAO;
}
@Override
public String getEmail() {
try {
@SuppressWarnings("unchecked")
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
return kakaoAccount.get("email").toString();
} catch (Exception e){
throw new IllegalStateException("카카오 계정정보에 이메일이 존재하지 않습니다.");
}
}
@Override
public String getName() {
try {
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return properties.get("nickname").toString();
} catch (Exception e){
throw new IllegalStateException("카카오 계정정보에 닉네임이 존재하지 않습니다.");
}
}
}
private OAuth2Response getOAuth2Response(SocialType socialType, Map<String, Object> attributes) {
if(socialType == SocialType.KAKAO) {
return new OAuth2KakaoResponse(attributes);
} else if(socialType == SocialType.GITHUB) {
return new OAuth2GithubResponse(attributes);
} else {
throw new UserException(UserExceptionCode.UNSUPPORTED_SOCIAL_PROVIDER);
}
}
getOAuth2Response()
메서드로 생성한다.OAuth2Response
구현체를 생성하여 반환한다.해당과정을 통해 User
엔티티를 생성하거나, DB에서 가져와
AuthUser
로 변환하여 반환한다.
이후에 OAuth2LoginSuccessHandler
는 AuthUser를 갖고, JWT 토큰을 발급하고 클라이언트에게 발급한 토큰을 응답하는 역할을 한다.
사용자 로그인 요청
↓
CustomOAuth2UserService 호출
↓
CustomOAuth2UserService.getOAuth2Response() 호출하여 각 소셜로그인 정보 반환
↓
OAuth2LoginSuccessHandler 전달 및 호출
↓
JWT 토큰 생성하고, AuthUser로 변환하여 SecurityContextHolder에 저장