스프링 시큐리티 OAuth2/OAuth 2.0 Client - oauth2Client()

DefaultOAuth2AuthorizedClientManager -필터 기반으로 구현하기

webmaster 2023. 2. 23. 23:21
728x90

Filter 기반 구현

  • OAuth2AuthorizeRequest 이후, 과정은 이전과 동일하다.
  • CustomOAuth2LoginAuthenticationFilter를 사용해서, 요청을 보낸다는 부분만 다르다.

Test

CustomOAuth2AuthenticationFilter

public class CustomOAuth2AuthenticationFilter extends AbstractAuthenticationProcessingFilter {

  private static final String DEFAULT_FILTER_PROCESSING_URI = "/oauth2Login/**";

  private DefaultOAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
  private OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository;

  private OAuth2AuthorizationSuccessHandler successHandler;

  private Duration clockSkew = Duration.ofSeconds(3600);

  private Clock clock = Clock.systemUTC();

  public CustomOAuth2AuthenticationFilter(
      DefaultOAuth2AuthorizedClientManager oAuth2AuthorizedClientManager,
      OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository) {
    super(DEFAULT_FILTER_PROCESSING_URI); //Filter가 동작하기 위해 매칭될 URL 정보를 전달해준다.

    this.oAuth2AuthorizedClientRepository = oAuth2AuthorizedClientRepository;
    this.oAuth2AuthorizedClientManager = oAuth2AuthorizedClientManager;
    this.successHandler = (authorizedClient, principal, attributes) -> { //최종 인가를 받고 난 후, 해당 핸들러 실행
      //아직 해당 상태에서는 principal은 비인증 상태이다.
      oAuth2AuthorizedClientRepository
          .saveAuthorizedClient(authorizedClient, principal,
              (HttpServletRequest) attributes.get(HttpServletRequest.class.getName()),
              (HttpServletResponse) attributes.get(HttpServletResponse.class.getName()));
      System.out.println("authorizedClient = " + authorizedClient);
      System.out.println("principal = " + principal);
      System.out.println("attributes = " + attributes);
    };
    oAuth2AuthorizedClientManager.setAuthorizationSuccessHandler(successHandler);
  }

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null) { //익명사용자용 토큰을 만들어 주면 된다
      authentication = new AnonymousAuthenticationToken("anonymous", "anonymousUser",
          AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
    }
    OAuth2AuthorizeRequest authorizeRequest =
        OAuth2AuthorizeRequest.withClientRegistrationId("keycloak")
            .principal(authentication)
            .attribute(HttpServletRequest.class.getName(), request)
            .attribute(HttpServletResponse.class.getName(), response)
            .build();

    OAuth2AuthorizedClient authorizedClient =
        oAuth2AuthorizedClientManager.authorize(authorizeRequest);
    if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken())
        && authorizedClient.getRefreshToken() != null) {
      authorizedClient = oAuth2AuthorizedClientManager.authorize(authorizeRequest);
    }
    if (authorizedClient != null) {
      OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
      ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
      OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
      OAuth2UserRequest oAuth2UserRequest = new OAuth2UserRequest(clientRegistration, accessToken);
      OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);

      SimpleAuthorityMapper authorityMapper = new SimpleAuthorityMapper();
      authorityMapper.setPrefix("SYSTEM_");
      Set<GrantedAuthority> grantedAuthorities = authorityMapper.mapAuthorities(
          oAuth2User.getAuthorities());

      OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(
          oAuth2User, grantedAuthorities, clientRegistration.getRegistrationId());

      SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken);
      this.successHandler.onAuthorizationSuccess(authorizedClient, oAuth2AuthenticationToken,
          createAttributes(request, response));
      return oAuth2AuthenticationToken;
    }

    return null;
  }

  private boolean hasTokenExpired(OAuth2Token token) {
    return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
  }

  private static Map<String, Object> createAttributes(HttpServletRequest servletRequest,
      HttpServletResponse servletResponse) {
    Map<String, Object> attributes = new HashMap<>();
    attributes.put(HttpServletRequest.class.getName(), servletRequest);
    attributes.put(HttpServletResponse.class.getName(), servletResponse);
    return attributes;
  }
}
  • 빈으로 등록할 것이 아니기 때문에 생성자를 통해 OAuth2AuthorizedClientManager와 OAuth2AuthorizedClientRepository를 주입받는다.
  • SuccessHandler를 인증까지 완료 후에 호출하기 위해 SecurityContextHolder에 인증 Token을 넣은 뒤, onAuthorizationSuccess를 호출해 준다.
  • UsernamePasswordAuthenticationFilter 이전에 Filter를 동작시킬 것이기 때문에 authentication은 null일 수밖에 없다.
    • 이전에는 controller에서 해당 값을 불러 문제가 없었다(모든 필터를 다 통과하고 호출)
    • authentication에 익명사용자용 토큰을 만들어서 넣어주면 된다.

OAuth2ClientConfig

@Configuration(proxyBeanMethods = false)
public class OAuth2ClientConfig {

  @Autowired
  private DefaultOAuth2AuthorizedClientManager auth2AuthorizedClientManager;

  @Autowired
  private OAuth2AuthorizedClientRepository authorizedClientRepository;

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(
        authRequest -> authRequest.antMatchers("/", "/oauth2Login", "/client").permitAll()
            .anyRequest().authenticated());
    http
        .oauth2Client(Customizer.withDefaults());
    http
        .addFilterBefore(customOAuth2AuthenticationFilter(),
            UsernamePasswordAuthenticationFilter.class);
    return http.build();
  }

  private CustomOAuth2AuthenticationFilter customOAuth2AuthenticationFilter() {
    CustomOAuth2AuthenticationFilter auth2AuthenticationFilter = new CustomOAuth2AuthenticationFilter(
        auth2AuthorizedClientManager, authorizedClientRepository);
    auth2AuthenticationFilter.setAuthenticationSuccessHandler(
        (request, response, authentication) -> response.sendRedirect("/home"));
    return auth2AuthenticationFilter;
  }
}
  • UsernamePasswordAuthenticationFilter 이전에 내가 만든 필터를 넣어준다.
  • 빈으로 등록되어 있는 OAuth2AuthorizedClientManager와 OAuth2AuthorizedClientRepository를 주입받아 생성자로 넣어준다.
  • 해당 filter를 성공적으로 통과한 이후에는 /home으로 redirect 해준다

HomeController

@Controller
public class HomeController {

  @Autowired
  private OAuth2AuthorizedClientService oAuth2AuthorizedClientService;

  @GetMapping("/home")
  public String home(Model model, OAuth2AuthenticationToken oAuth2AuthenticationToken) {
    OAuth2AuthorizedClient authorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(
        "keycloak", oAuth2AuthenticationToken.getName());
    model.addAttribute("oAuth2AuthenticationToken", oAuth2AuthenticationToken);
    model.addAttribute("AccessToken", authorizedClient.getAccessToken().getTokenValue());
    model.addAttribute("RefreshToken", authorizedClient.getRefreshToken().getTokenValue());
    return "home";
  }
}
  • OAuth2AuthorizedClientService를 주입받아 keycloak으로 설정한 값을 읽어온다.
  • 인증이 완료된 객체로 AccessToken, RefreshToken 모두 존재하므로 model에 담아 리턴한다.
728x90