Programming/Java

SpringSecurity로 OAuth 인증인가 구현. Kakao 소셜인증을 서버로..

armyost 2023. 9. 12. 12:45
728x90

아래는 OAuth 2.0 표준을 따라 UserInfo Endpoint에서 사용자 정보를 가져오는 예제입니다. 

※ 대부분의 Posting에는 Token Payload를 JWT Claim을 통해서 데이터를 복호화 하는 방법이 소개되어 있습니다.

 

Git 주소

https://github.com/armyost/Oauth2Sample/tree/main

 

GitHub - armyost/Oauth2Sample: This is OAuth2 Authentication sample with SpringSecurity

This is OAuth2 Authentication sample with SpringSecurity - GitHub - armyost/Oauth2Sample: This is OAuth2 Authentication sample with SpringSecurity

github.com

 

 

OAuth 2.0는 JWT(Json Web Token)을 활용한 표준 프레임워크로 Token 기반 인증 프로토콜 중 가장 범용적인 생태계를 구축하고 있습니다. 범용성이 있어 OAuth 관련 모듈을 Spring이 프레임워크 레벨에서 제공하고 있습니다. 이에따라 Client Side 에서는 Spring Security에서 제공하는 전용 OAuth 2.0 모듈을 사용하면 개발생산성이 좋습니다. 예를들어 아래 소스코드에서 redirect-uri 로 표시된 {baseUri}/login/oauth2/code/{provider} 로 OAuth Server로 부터 수신한 사용자 정보, Token 정보를 보내기만 하면 Spring OAuth Client가 알아서 객체로 생명주기를 관리합니다.

 

└─ src
   ├─ main
   │  ├─ java
   │  │  └─ com
   │  │     └─ armyost
   │  │        └─ demo
   │  │           ├─ DemoApplication.java
   │  │           ├─ DemoPostController.java
   │  │           ├─ KakaoOAuth2UserService.java
   │  │           ├─ OAuth2AuthenticationSuccessHandler.java
   │  │           ├─ SecurityConfig.java
   │  │           └─ TokenAuthenticationFilter.java
   │  └─ resources
   │     └─ application.yml

 

가) Redis 및 SpringSession 환경설정

spring:
  security:
    oauth2:
      client:
        registration:
          kakao:  // Provider 별로 정의
            client-id: 1a71cXXXXXXXXXXXXXXXXXXXXXXab4ab
            client-secret: DO2nGXXXXXXXXXXXXXXXXXXXXXXM6XUaFdwT
            scope: account_email, profile_nickname
            client-name: Kakao
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            client-authentication-method: POST
 
        provider:
          kakao: // Provider 별로 정의
            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

 

나) Security Filter Chain Bean 생성
※ Security Filter Chain 제공 기능 Document 링크 : https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
    private final KakaoOAuth2UserService kakaoOAuth2UserService;
    private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
    private final TokenAuthenticationFilter tokenAuthenticationFilter;
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(withDefaults())
                .cors(withDefaults()) // if you want to customize cors -> .cors().configurationSource(corsConfigurationSource());
                .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Let it check validation of client's token first.
                .authorizeHttpRequests(request -> request
                        .antMatchers("/index.html","/images/**","/permitAllConents.html") // White list policy.
                        .permitAll()
                        .antMatchers("/**").hasRole("USER")
                        .anyRequest().authenticated())
                .oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo
                        .userService(kakaoOAuth2UserService) // Let it do actions with user entity.
                        .and()
                        .successHandler(oAuth2AuthenticationSuccessHandler))) // Let it do actions after authentication like reponsing the authorised token.
                .logout(withDefaults()); //
        return http.build();
    }
}
 

 

다) Client Request 유입시 Request의 유효성 검증을 위한 Filter
Token 유효성 검사와 같이 Client Request의 유효성 검사를 수행

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class);
 
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";
   
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        logger.info("!!! Check client's token validation first of all spring security process");
 
        // You can check validation of a Bearer token here first of all.
        /*  if () {
            response.setStatus(401);
            return
        }
        */
        filterChain.doFilter(request, response);
    }
}

 

라) 인증 성공 후 사용자 정보에 관련한 로직 구현
사용자 통계를 위한 로깅과 같이 인증 ∙ 인가 프로세스 외 사용자 정보로 부가가치 생성

@RequiredArgsConstructor
@Service
public class KakaoOAuth2UserService extends DefaultOAuth2UserService {
    private static final Logger logger = LoggerFactory.getLogger(KakaoOAuth2UserService.class);
 
    private final HttpSession httpSession;
 
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        Map<String, Object> attributes = oAuth2User.getAttributes();
 
        // How to get informations of authenticated user.
        logger.info("!!! User attributes are : {} !!!", attributes);
        logger.info("!!! User authorities are : {} !!!", oAuth2User.getAuthorities());
        logger.info("!!! User AccessToken Value is {} !!!", userRequest.getAccessToken().getTokenValue());
        logger.info("!!! User AccessToken will be expired at {} !!!", userRequest.getAccessToken().getExpiresAt());
 
        // I use session to some data share through a session.
        httpSession.setAttribute("login_info", attributes);
        httpSession.setAttribute("access_token", userRequest.getAccessToken().getTokenValue());
 
        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                oAuth2User.getAttributes(), "id");
    }
}
 

 

마) 인증 성공 후 수행할 로직 구현
Client에 인증 Token을 Response 하거나, 특정 페이지로 Redirect 하는 등의 역할 수행

@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    private static final Logger logger = LoggerFactory.getLogger(OAuth2AuthenticationSuccessHandler.class);
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException {
 
        // Code here whatever want to do after authetication.
 
        // If you want to response access token to client. Make it as a Cookie.
        logger.info("!!! Response access token : {}", request.getSession().getAttribute("access_token"));
 
        /* Authentication userAuthenticationDetail = SecurityContextHolder.getContext().getAuthentication();
        Long userAuthenticationName = Long.valueOf(userAuthenticationDetail.getName());
        logger.info("!!! UserAuthentication Name is : {} !!!", userAuthenticationName);
        */
    }
}