기본적인 이론을 모두 정리했으니 직접 구현해봅시다.
Spring Security에서 제공하는 로그인 기능이 아닌
OAuth2를 사용하는 로그인 기능을 구현해보겠습니다.
( form 로그인 방법도 노션에 정리는 해뒀지만,,, OAuth2 보다 작성할 내용이 많아 추후에 하겠습니다. )
구현할 로그인 과정의 대략적인 순서는 다음과 같습니다.
- 클라이언트에서 Spring Boot 서버로 로그인을 요청합니다.
- 클라이언트에게 OAuth2 로그인 redirect url을 전달하여 로그인을 시도합니다.
- 로그인에 성공하면 OAuth2 리소스 서버에서 사용자의 정보를 가져와 Refresh JWT 를 발급합니다.
- 발급된 Refresh JWT를 Redis 에 저장하여 관리하고, 클라이언트에게 쿠키(Http only) 형태로 전달해줍니다.
- 클라이언트는 Refresh JWT를 통해 Access JWT를 발급받아 사용합니다.
이번 포스트에서는 클라이언트에서 로그인 요청부터
로그인에 성공해서 사용자의 정보를 가져오는 것 까지의 과정을 정리해보겠습니다.
OAuth2 등록 및 설정
원하는 OAuth2 제공 서비스의 문서를 참고하여 애플리케이션을 등록합니다.
아래 블로그는 구글 소셜 로그인을 설정하는 방법이 잘 정리되어 있어 참고하시면 좋을 것 같습니다!
구글 소셜 로그인 설정
4th UMC Server-Spring 시리즈에서 카카오 소셜로그인을 구현해보았는데요. 이번 시간에는 구글 소셜로그인을 구현해보겠습니다. 카카오 소셜로그인이 궁금하시다면 아래의 링크를 참조해주세요. >>
velog.io
application.properties 파일에 OAuth2 설정을 입력합니다.
#registration
spring.security.oauth2.client.registration.서비스명.client-name=서비스명
spring.security.oauth2.client.registration.서비스명.client-id=서비스에서 발급 받은 아이디
spring.security.oauth2.client.registration.서비스명.client-secret=서비스에서 발급 받은 비밀번호
spring.security.oauth2.client.registration.서비스명.redirect-uri=서비스에 등록한 우리쪽 로그인 성공 URI
spring.security.oauth2.client.registration.서비스명.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.서비스명.scope=리소스 서버에서 가져올 데이터 범위
#provider
spring.security.oauth2.client.provider.서비스명.authorization-uri=서비스 로그인 창 주소
spring.security.oauth2.client.provider.서비스명.token-uri=토큰 발급 서버 주소
spring.security.oauth2.client.provider.서비스명.user-info-uri=사용자 정보 획득 주소
spring.security.oauth2.client.provider.서비스명.user-name-attribute=응답 데이터 변수
SecurityConfig를 설정합니다.
이후 구현에 맞게 변경되는 설정들이 있습니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//csrf disable
http
.csrf((auth) -> auth.disable());
//From 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());
//HTTP Basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());
//세션 설정 : STATELESS
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
//oauth2
http
.oauth2Login(Customizer.withDefaults());
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
// 등등의 설정들 ...
return http.build();
}
}
구현
앞서 대략적인 로그인 과정을 말씀드렸는데,
이번에는 동작 하나씩 살펴보며 구현해보겠습니다.
사용 스택 : Vue 3, Spring Boot 3.xx, Spring Security 6.xx, JWT 0.12.3, OAuth 2.0 Client
- (Client) OAuth2 로그인 url 중에서 원하는 url을 선택해 하이퍼링크로 로그인을 시도합니다.
- Server로 HTTP 요청을 보내는 것이 아니라 하이퍼 링크를 사용하는 이유는 OAuth2 인증서버의 CORS 정책 때문입니다.
- 관습적으로 아래와 같은 url을 사용합니다.
window.location.href = "http://localhost:8080/oauth2/authorization/서비스명";
- (Server) OAuth2AuthorizationRequestRedirectFilter에 의해서 해당 url로 등록된 OAuth2 인증서버로 리디렉트 됩니다.
- application.properties 파일에서 provider 설정 중 authorization_uri
- (OAuth2 인증서버) 로그인을 성공하면 OAuth2 인증 서버에서 등록된 애플리케이션 서버의 URL로 권한부여 코드를 파라미터(혹은 바디)로 함께 리디렉션합니다.
-
- application.properties 파일에서 registration 설정 중 redirect-uri
-
- (Server) OAuth2LoginAuthenticationFilter가 요청을 가로채고 OAuth2LoginAuthenticationProvider에 의해서 OAuth2 인증 서버에 코드를 전달하여 OAuth2 리소스 서버로 접근할 수 있는 Access 토큰을 받아오고 Access 토큰을 통해 리소스 서버에 접근해서 해당 사용자의 정보를 받아옵니다.
- OAuth2 설정만 잘 해두면 자동적으로 동작됨.
- 이 Access 토큰을 저장하는 등의 커스텀이 가능하지만, Access 토큰을 클라이언트로 옮기는 등은 권장하지 않는 방법
- (Server) 리소스 서버에서 받아온 사용자의 정보는 OAuth2UserService의 구현체인 DefaultOAuth2UserService를 상속받은 클래스를 구현해 필요한 비즈니스 로직을 수행한 후 OAuth2User 의 구현체로 반환합니다.
저는 여기서 DB와 연동해 회원이 존재하는지를 파악하고 데이터를 추가하거나 업데이트 해줍니다.
-
@Service @RequiredArgsConstructor @Transactional(readOnly = true) public class CustomOAuth2UserService extends DefaultOAuth2UserService { private final UserRepository userRepository; @Transactional @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // 부모 클래스인 DefaultOAuth2UserService 를 통해 // 리소스 서버로부터 받아온 OAuth2User 객체를 불러옵니다. OAuth2User oAuth2User = super.loadUser(userRequest); // 서비스 명 획득 String registrationId = userRequest.getClientRegistration().getRegistrationId(); OAuth2Response oAuth2Response = null; // 각각의 리소스 서버가 반환해주는 데이터가 다르기 때문에 전처리 작업을 해줌. if (registrationId.equals("google")) { oAuth2Response = new GoogleResponse(oAuth2User.getAttributes()); } else { return null; } String oauthData = oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId(); // DB 조회 및 생성 Users existData = userRepository.findByOauthData(oauthData); if (existData == null) { Users user = Users.createUserWithOAuth2(oAuth2Response.getName(), oAuth2Response.getEmail(), oauthData); userRepository.save(user); UserInfoDTO userInfo = new UserInfoDTO(Role.USER.value(), oAuth2Response.getName(), oauthData ); return new CustomOAuth2User(userInfo); } else { // 더티 체킹 existData.updateUserInfo(oAuth2Response.getName(), oAuth2Response.getEmail()); UserInfoDTO userInfo = new UserInfoDTO(existData.getRole(), oAuth2Response.getName(), oauthData); return new CustomOAuth2User(userInfo); } } }
-
@RequiredArgsConstructor public class CustomOAuth2User implements OAuth2User { private final UserInfoDTO userInfoDTO; @Override public Map<String, Object> getAttributes() { return null; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> collection = new ArrayList<>(); collection.add(new GrantedAuthority() { @Override public String getAuthority() { return userInfoDTO.getRole(); } }); return collection; } @Override public String getName() { return userInfoDTO.getName(); } public String getOAuthData() { return userInfoDTO.getOauthData(); } public String getUsername(){ return userInfoDTO.getName(); } }
- 또한 기존의 OAuth2UserService를 대체하기 위해서 WebConfig 설정을 추가해줍니다.
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .oauth2Login((oauth2) -> oauth2 .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig .userService(customOAuth2UserService)); // ... return http.build(); } }
-
- (Server) SimpleUrlAuthenticationSuccessHandler를 상속받아 성공적인 로그인 시 Refresh JWT를 발급합니다.
-
@Component @RequiredArgsConstructor public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler { // 이 두 클래스는 편의를 위해 구현했습니다. // JWT 생성 등의 유틸 기능 private final JwtUtil jwtUtil; // JWT 를 데이터베이스와 연동하기 위한 비즈니스 로직을 포함하는 서비스 private final JWTService jwtService; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // Refresh 토큰 발급 CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal(); String oauthData = customUserDetails.getOAuthData(); String name = customUserDetails.getName(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); Iterator<? extends GrantedAuthority> iterator = authorities.iterator(); GrantedAuthority auth = iterator.next(); String role = auth.getAuthority(); String refresh = TokenType.REFRESH.value(); String refreshToken = jwtUtil.createJwt(refresh, role, name, oauthData, JwtExpiredMs.REFRESH.value()); // 생성된 Refresh 토큰을 Redis 데이터베이스에 저장함. jwtService.addRefreshToken(name, oauthData, refreshToken); response.addCookie(HttpHeaderUtil.createCookie(refresh, refreshToken)); response.setStatus(HttpStatus.PERMANENT_REDIRECT.value()); response.sendRedirect("http://localhost:3000/issue/accessToken"); } }
- 이 성공 핸들러도 WebConfig 파일에 추가합시다.
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; // 보안 필터 체인 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http //== OAuth2 .oauth2Login((oauth2) -> oauth2 .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig .userService(customOAuth2UserService)) .successHandler(oAuth2LoginSuccessHandler)); // ... return http.build(); } }
-
동작 원리
추가적으로 보다 정확하고 자세한 동작 원리를 알기 위해서는 빌드를 이용하셔도 좋습니다.
빌드를 통해 OAuth2AuthorizationRequestRedirectFilter와 OAuth2LoginAuthenticationFilter의 동작을 하나씩 살펴보면 됩니다!!
( 저도 처음에 보다 정확한 이해를 위해 클래스들을 타고타고 들어가 기능을 확인하니 이해가 빠르게 됐습니다. )
이 과정을 하게되면 훨씬 정확하게 OAuth2 로그인 과정을 이해하고 추가적인 커스텀 또한 가능합니다!
아래 블로그에 이해하기 쉽게 작성되어 있어서 이 글을 참고하셨으면 좋겠습니다!!
스프링 시큐리티 OAuth2 동작 원리
이번 포스트에서는 프론트엔드로부터 로그인 요청이 왔을 때 스프링 시큐리티에서 어떻게 처리하는지 코드를 통해 살펴보겠습니다.
velog.io
주의할 점
이번 포스트에서는 CORS 설정과 Http Header 설정 등을 하지 않았지만,
구현할 때 필요에 따라 해당 설정들을 추가할 필요가 있습니다.
Reference
OAuth2AuthorizationRequestRedirectFilter (spring-security-docs 6.2.3 API)
OAuth2AuthorizationRequestRedirectFilter (spring-security-docs 6.2.3 API)
public class OAuth2AuthorizationRequestRedirectFilter extends org.springframework.web.filter.OncePerRequestFilter This Filter initiates the authorization code grant flow by redirecting the End-User's user-agent to the Authorization Server's Authorization E
docs.spring.io
OAuth2LoginAuthenticationFilter (spring-security-docs 6.2.3 API)
OAuth2LoginAuthenticationFilter (spring-security-docs 6.2.3 API)
Constructs an OAuth2LoginAuthenticationFilter using the provided parameters. Constructs an OAuth2LoginAuthenticationFilter using the provided parameters. Constructs an OAuth2LoginAuthenticationFilter using the provided parameters.
docs.spring.io
OAuth2LoginAuthenticationProvider (spring-security-docs 6.2.3 API)
An implementation of an AuthenticationProvider for OAuth 2.0 Login, which leverages the OAuth 2.0 Authorization Code Grant Flow. This AuthenticationProvider is responsible for authenticating an Authorization Code credential with the Authorization Server's
docs.spring.io
https://substantial-park-a17.notion.site/Docs-002024551c294889863d0c7923590568
개발자 유미 Docs 모음 | Notion
Docs 모음
substantial-park-a17.notion.site
https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/
Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 1
In this article, You'll learn how to add social as well as email and password based login to your spring boot application using Spring Security and Spring Security's OAuth2 client. You'll build a full stack application with Spring Boot and React containing
www.callicoder.com
'WEB > Spring' 카테고리의 다른 글
RestControllerAdvice로 전역에서 발생된 예외 처리하기. (0) | 2024.05.01 |
---|---|
Spring Security 프레임워크로 로그인에 성공하면 JWT 발급하기 (0) | 2024.04.17 |
Spring Security가 뭐죠? (0) | 2024.03.24 |
[Spring JPA] IncorrectResultSizeDataAccessException (0) | 2023.12.05 |
[Spring Boot] API Docs를 자동으로 만들어보자! (1) | 2023.11.25 |