-
Spring Security OAuth2.0(kakao)Spring 2024. 8. 17. 03:53
https://kimtahwn.tistory.com/58
이전의 OAuth2.0 글을 읽고 오시면 좋습니다.
Spring Security OAuth2.0을 사용하여 kakao 로그인 구현하기
이 방식은 프론트엔드에서 Authorization Code를 받아올 필요없이 백엔드에서 모든 OAuth 로그인을 처리하는 방식입니다.
Spring Security 에서는 쉽게 OAuth2를 사용할 수 있도록 지원해줍니다.
1. 로그인 요청의 EndPoint를 /oauth2/authorization/{provider} 로 하게 되면 Spring Security에서 요청을 가로채 로그인을 시작하고 인증서버로 리디렉션을 수행하는데 사용합니다.
ex) kakao login : /oauth/authorization/kakao
2. 리디렉션의 EndPoint를 /login/oauth2/code/{provider} 로 하게 되면 Authorization Code와 Access Token을 획득하기 위한 Redirect URI로 사용합니다.
ex) local 환경 : http://localhost:8080/login/oauth2/code/kakao
Spring Security OAuth를 사용할 때 흐름
출처 : https://techblog.uplus.co.kr/spring-security-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-oauth-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%EA%B8%B0-ea2d649bea3b - /oauth2/authorization/{provider} 로 OAuth 로그인 요청
- OAuth2LoginAuthenticationFilter가 요청을 가로채고 외부 인증 제공자에 의해 인증이 완료되면, 인증 제공자로부터 Authorization Code를 받음.
- OAuth2LoginAuthenticationProvider는 인증 제공자로부터 받은 Authorization Code를 사용하여 Access Token을 요청하고, Access Token을 받음.
- OAuth2UserService가 Access Token을 사용하여 인증 제공자로부터 사용자 정보를 요청하고, 사용자 정보를 받음.
- OAuth2UserService가 사용자 정보를 이용하여 OAuth2User 객체를 생성하여 DB에 조회
- OAuth2User 객체를 OAuth2AuthenticationProvider에 전달
- OAuth2AuthenticationProvider는 OAuth2User의 정보를 검증하여 인증을 완료
- OAuth2AuthenticationProvider는 인증된 OAuth2AuthenticationToken 객체를 반환
- OAuth2AuthenticationSuccessHandler가 호출 이후 인증 성공 후 추가 처리를 수행함 (예: JWT 토큰 생성)
- OAuth2AuthenticationToken 객체를 SecurityContext에 저장, 사용자에게 Redirect
SpringBoot 설정
1. build.gradle에 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
2. appilcation.yml 파일 설정 추가
spring: security: oauth2: client: registration: kakao: client-authentication-method: client_secret_post client-name: kakao client-id: ${KAKAO_CLIENT_ID} client-secret: ${KAKAO_CLIENT_SECRET} authorization-grant-type: authorization_code provider: kakao redirect-uri: ${KAKAO_REDIRECT_URI} scope: profile_nickname, account_email // 동의항목 provider: kakao: authorization-uri: https://kauth.kakao.com/oauth/authorize token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me user-name-attribute: id
위의 구성을 사용한다면 애플리케이션이 위에 설명한 2개의 추가 엔드포인트를 지원하게 됩니다.
3.SecurityConfig
@Configuration @RequiredArgsConstructor @EnableWebSecurity public class SecurityConfig{ private final CorsFilter corsFilter; private final CustumOauth2UserService custumOauth2UserService; private final AuthenticationSuccessHandler authenticationSuccessHandler; private final JwtAuthenticationFilter jwtAuthenticationFilter; private final AuthenticationFailHandler authenticationFailHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .addFilter(corsFilter) .csrf(CsrfConfigurer::disable) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(HttpBasicConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/manager/**").hasAuthority("ROLE_MANAGER") .anyRequest().permitAll() ).oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo.userService(custumOauth2UserService)) .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailHandler) ).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }
4. OAuth2UserService
@Service @RequiredArgsConstructor public class CustumOauth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { private final UsersRepository usersRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // 기본 OAuth2UserService 객체 생성 OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService(); OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); // 클라이언트 등록 id(kakao, google 등), 사용자 이름 속성 가져오기 String registrationId = userRequest.getClientRegistration().getRegistrationId(); String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails() .getUserInfoEndpoint().getUserNameAttributeName(); // OAuth2User 의 정보로 OAuth2Attribute 객체를 생성 OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); // email을 통해 중복 확인 String email = oAuth2Attribute.getEmail(); Users user = usersRepository.findByEmail(email); if (user == null) { user = oAuth2Attribute.toEntity(); usersRepository.save(user); } // 권한 설정 List<SimpleGrantedAuthority> authorities; if (user.getAuthor() == Author.roleManager) { authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_MANAGER")); } else { authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); } return new DefaultOAuth2User( authorities, oAuth2Attribute.getAttributes(), oAuth2Attribute.getAttributeKey() ); } }
DefaultOAuth2UserService의 loadUser 메서드가 userRequest를 사용해 provider(예: Google, Kakao 등)로부터 사용자 정보를 가져옵니다.
5. OAuth2Attribute
@Builder @Getter public class OAuth2Attribute { private Map<String, Object> attributes; // 사용자 속성 정보 담는 Map private String attributeKey; // 사용자 속성의 키 값 private String email; // email로 중복 검사 private String name; private String provider; // 제공자 정보 // 서비스에 따라 OAuth2Attribute 객체를 생성하는 메서드 static OAuth2Attribute of(String provider, String attributeKey, Map<String, Object> attributes) { return switch (provider) { case "kakao" -> ofKakao("email", attributes); //case "google" -> ofGoogle(provider, attributeKey, attributes); //case "naver" -> ofNaver(provider, "id", attributes); default -> throw new RuntimeException(); }; } /* * Kakao 로그인일 경우 사용하는 메서드, 필요한 사용자 정보가 kakaoAccount -> kakaoProfile 두번 감싸져 있어서, * 두번 get() 메서드를 이용해 사용자 정보를 담고있는 Map을 꺼내야한다. * */ private static OAuth2Attribute ofKakao(String attributeKey, Map<String, Object> attributes) { Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account"); Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile"); return OAuth2Attribute.builder() .name((String) kakaoProfile.get("nickname")) .email((String) kakaoAccount.get("email")) .provider("kakao") .attributes(kakaoAccount) .attributeKey(attributeKey) .build(); } public Users toEntity() { return Users.builder() .userName(name) .email(email) .author(Author.roleUser) .build(); } }
각 OAuth2 서비스에 따라 반환하는 사용자의 형태가 다르기 때문에 provider에 따라 구분하여 필요한 사용자의 정보를 가져와 처리를 하는 클래스입니다.
6. AuthenticationSucessHandler
@Slf4j @Component @RequiredArgsConstructor public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtUtil jwtUtil; private final UsersRepository usersRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); Map<String, Object> attributes = oAuth2User.getAttributes(); String email = (String) attributes.get("email"); Users users = usersRepository.findByEmail(email); JwtToken jwtToken = jwtUtil.generatedToken(email, users.getAuthor()); String redirectUrl = "프론트서버?accessToken=" + jwtToken.getAccessToken(); getRedirectStrategy().sendRedirect(request, response, redirectUrl); } }
인증이 성공되었을 때 호출되는 successHandler를 구현한 클래스입니다.
필요한 정보를 추출하여, JWT Access Token을 생성하여 쿼리파라미터로 담아 프론트서버에 리디렉션합니다.
프론트에서 localStorage에 JWT Token을 저장하여 사용하기 때문에 리디렉션 할 때 쿼리파라미터로 담아 전달했습니다.
public void createCookieAccessToken(String accessToken, HttpServletResponse httpServletResponse) throws UnsupportedEncodingException { ResponseCookie cookie = ResponseCookie.from("Authorization", URLEncoder.encode("Bearer " + accessToken, "utf-8").replaceAll("\\+", "%20")) .path("/") .sameSite("None") .httpOnly(false) .secure(true) .maxAge(604800) .build(); httpServletResponse.addHeader("Set-Cookie", cookie.toString()); }
이외의 방식으로는 JWT Token을 생성할 때, Cookie에 넣어 전달하는 방법이 있습니다.
하지만 백엔드에서 "Set-Cookie" Header를 통해 Response 하게 된다면 해당 Cookie는 프론트에서 접근하거나 수정할 수 없습니다.
출처, 참고 :
https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html
'Spring' 카테고리의 다른 글
Spring STOMP로 채팅, 알림 구현 + Spring Security, JWT (0) 2024.08.19 Spring OAuth2.0(Google) (0) 2024.08.18 spring boot 3 버전 이상에서 swagger 사용하기 (0) 2024.07.02 refresh token redis로 구현하기 (0) 2024.02.22 Spring에서 Redis로 분산락 사용하기 (0) 2024.02.22