스프링부트 OAuth2 구글로그인 구현하기.
0.
기존에 아이디/패스워드 방식으로 로그인이 동작하던 로그인단에서 추가로 구글로그인도 가능하게 해달라는 요청이 들어왔다.
먼저 구글로그인을 사용하려면 구글 API에 등록과정이 필요하다. 해당과정은 하단 블로그 참조
https://blogan99.tistory.com/90
1. 의존성
build.gradle에 다음과 같이 소셜로그인용 의존성을 넣어준다
implementation "org.springframework.boot:spring-boot-starter-oauth2-client" // 소셜로그인 용
OAuth2 토큰발급의 주체는 구글서버이고, 우리인가 인증 서버는 구글서버의 클라이언트인 상황이다. 구글로그인 성공시 자체 jwt 토큰을 내려줄 의도이기에 'org.springframework.security:spring-security-oauth2-authorization-server'는 의존성을 추가하지 않기로 했다.
2. SecurityFilterChain
기존 아이디/패스워드 로그인 방식을 유지하면서 구글로그인도 가능하게 하는 방법을 필요로 하는 상황인데 SecurityConfig에서 SecurityFilterChain에 약간만 추가해주면 된다.
기존 SecurityFilterChain
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, PrincipalOauth2UserService principalOauth2UserService, OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) // CORS 필터 추가
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/user/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가
return http.build();
}
OAuth2 추가 FilterChain
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, PrincipalOauth2UserService principalOauth2UserService, OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) // CORS 필터 추가
.authorizeHttpRequests(auth -> auth
.requestMatchers("/oauth2/**", "/login/**").permitAll() // ✅ 소셜로그인 경로는 허용경로로 추가.
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/user/**").permitAll()
.anyRequest().authenticated()
)
// OAuth 소셜로그인 도입후 추가한 부분.
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userService(principalOauth2UserService))
.defaultSuccessUrl("/", true)
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler((request, response, e) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"error\": \"" + e.getMessage() + "\"}");
})
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가
return http.build();
}
소셜 로그인 경로로 접근시에 로그인은 누구나 허용해야 하므로 authorizeHttpRequests()에 해당경로를 추가해주고,
authorizeHttpRequests() 하단에 .oauth2Login()을 추가해주면 된다. oauth2Login()에는 로그인 정책을 지정하는 PrinciapOauth2UserService와, 로그인 성공후 진행할 정책에 대한 내용인 OAuth2LoginSuccessHanler를 등록해주고, 로그인 실패를 컨트롤 할 failureHanlder()를 채워주면 된다.
2-1 PrincipalOauth2UserService
@Service
@RequiredArgsConstructor
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
OAuth2User oAuth2User = super.loadUser(userRequest);
String email = oAuth2User.getAttribute("email");
OAuth2User oAuth2User2 = super.loadUser(userRequest);
Optional<Users> users = userRepository.findUsersByEmail(email);
if (users.isPresent()) {
// 토큰발급은 OAuth2LoginSuccessHandler에서 진행.
return new UserDetailsImpl(users.get(),oAuth2User2.getAttributes());
}
else{
throw new GoogleUserException("회원가입이 필요합니다. 관리자에게 문의하세요");
}
}
}
우선 UserRepository를 살펴본 뒤, 해당 메일로 가입한 이력이 있는지 확인한 뒤, 가입 이력이 있으면 로그인 성공을, 실패시 예외를 던지도록 구현하였다.
정책따라 다르긴 하지만, 소셜로그인 이력이 없으면 여기서 바로 회원가입을 진행하록 많이들 구현한다. 하지만 현재 요구사항에선 소셜가입은 관리자를 통해서만 가능하게 해달라는 요청이 있어 가입이 되어있지 않다면 예외를 뱉어내도록 구현하였다.
2-2 OAuth2LoginSuccessHandler
@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = jwtTokenProvider.generateAuthToken(authentication);
ResponseCookie jwtCookie = ResponseCookie.from("jwt", token)
.httpOnly(true)// js에서 접근 불가능(xss 방어)
.secure(false) // true면 https에서만 사용 가능. 운영은 true로
.path("/") //모든 경로에서 사용가능
.maxAge(3600)
.sameSite("Strict") //Csrf 방어
.build();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, jwtCookie.toString());
response.addHeader(jwtCookie.getName(), jwtCookie.getValue());
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"response\":\"SUCCESS\"}");
response.setHeader("jwt", jwtCookie.toString());
}
}
로그인 성공시엔 기존에 아이디/패스워드 로그인단에서 발급해주던 방식과 동일하게 토큰틀 발급하도록 해주었다.
3.
위 과정이 모두 완료되었다면 로그인 테스트를 진행한다. 우선 서버를 구동하고, 브라우저를 열어서 localhost:8080/oauth2/authorization/google 경로로 접근을 해주고 로그인을 진행해준다.
미리 등록된 계정으로 로그인을 진행하면 SUCCESS 리스폰스가 잘 떨어지는 모습.