Use spring-security-oauth2 as client implementation.

  spring-security

Order

This article focuses on how to use spring security oauth2 as a client.

Four modes

OAuth 2.0 defines four authorization methods.

  • Authorization code
  • Simplified model (Client is a browser/front-end application.)
  • Resource owner password credentials (It is not safe for the user password to be exposed to the client side.)
  • Client credentials (It is mainly used for api authentication and has nothing to do with users.)

Let’s take authorization code mode as an example.

Main Ideas of Realizing client

  • You need to create a new controller or filter to process redirectUri.
  • Request token according to authentication code
  • After acquiring the token, bind the token with the user
  • You can then use token to obtain authorized resources.

OAuth2RestTemplate(Encapsulating the Method of Obtaining token)

Encapsulation of rest template provides a convenient method for obtaining token, etc.
DefaultUserInformateTemplate Factory has instantiated OAuth2RestTemplate

DefaultUserInfoRestTemplateFactory

spring-boot-autoconfigure-1.5.9.RELEASE-sources.jar! /org/springframework/boot/autoconfigure/security/oauth2/resource/DefaultUserInfoRestTemplateFactory.java

/**
 * Factory used to create the {@link OAuth2RestTemplate} used for extracting user info
 * during authentication if none is available.
 *
 * @author Dave Syer
 * @author Stephane Nicoll
 * @since 1.5.0
 */
public class DefaultUserInfoRestTemplateFactory implements UserInfoRestTemplateFactory {

    private static final AuthorizationCodeResourceDetails DEFAULT_RESOURCE_DETAILS;

    static {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId("<N/A>");
        details.setUserAuthorizationUri("Not a URI because there is no client");
        details.setAccessTokenUri("Not a URI because there is no client");
        DEFAULT_RESOURCE_DETAILS = details;
    }

    private final List<UserInfoRestTemplateCustomizer> customizers;

    private final OAuth2ProtectedResourceDetails details;

    private final OAuth2ClientContext oauth2ClientContext;

    private OAuth2RestTemplate oauth2RestTemplate;

    public DefaultUserInfoRestTemplateFactory(
            ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers,
            ObjectProvider<OAuth2ProtectedResourceDetails> details,
            ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {
        this.customizers = customizers.getIfAvailable();
        this.details = details.getIfAvailable();
        this.oauth2ClientContext = oauth2ClientContext.getIfAvailable();
    }

    @Override
    public OAuth2RestTemplate getUserInfoRestTemplate() {
        if (this.oauth2RestTemplate == null) {
            this.oauth2RestTemplate = createOAuth2RestTemplate(
                    this.details == null ? DEFAULT_RESOURCE_DETAILS : this.details);
            this.oauth2RestTemplate.getInterceptors()
                    .add(new AcceptJsonRequestInterceptor());
            AuthorizationCodeAccessTokenProvider accessTokenProvider = new AuthorizationCodeAccessTokenProvider();
            accessTokenProvider.setTokenRequestEnhancer(new AcceptJsonRequestEnhancer());
            this.oauth2RestTemplate.setAccessTokenProvider(accessTokenProvider);
            if (!CollectionUtils.isEmpty(this.customizers)) {
                AnnotationAwareOrderComparator.sort(this.customizers);
                for (UserInfoRestTemplateCustomizer customizer : this.customizers) {
                    customizer.customize(this.oauth2RestTemplate);
                }
            }
        }
        return this.oauth2RestTemplate;
    }

    private OAuth2RestTemplate createOAuth2RestTemplate(
            OAuth2ProtectedResourceDetails details) {
        if (this.oauth2ClientContext == null) {
            return new OAuth2RestTemplate(details);
        }
        return new OAuth2RestTemplate(details, this.oauth2ClientContext);
    }

}

This provides OAuth2RestTemplate.

ResourceServerTokenServicesConfiguration

spring-boot-autoconfigure-1.5.9.RELEASE-sources.jar! /org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java

/**
 * Configuration for an OAuth2 resource server.
 *
 * @author Dave Syer
 * @author Madhura Bhave
 * @author Eddú Meléndez
 * @since 1.3.0
 */
@Configuration
@ConditionalOnMissingBean(AuthorizationServerEndpointsConfiguration.class)
public class ResourceServerTokenServicesConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public UserInfoRestTemplateFactory userInfoRestTemplateFactory(
            ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers,
            ObjectProvider<OAuth2ProtectedResourceDetails> details,
            ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {
        return new DefaultUserInfoRestTemplateFactory(customizers, details,
                oauth2ClientContext);
    }

    //......
}    

The DefaultUserInformateTemplate Factory is mainly created in the ResourceServerTokenServicesConfiguration configuration.
This is for the resource server, so the client needs to create it himself if it wants to use it.

RedirectUri processing (OAuth2ClientAuthenticationProcessingFilter)

Spring securityouth2 also provides convenient classes to handle:
spring-security-oauth2-2.0.14.RELEASE-sources.jar! /org/springframework/security/oauth2/client/filter/OAuth2ClientAuthenticationProcessingFilter.java

/**
 * An OAuth2 client filter that can be used to acquire an OAuth2 access token from an authorization server, and load an
 * authentication object into the SecurityContext
 * 
 * @author Vidya Valmikinathan
 * 
 */
public class OAuth2ClientAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    public OAuth2RestOperations restTemplate;

    private ResourceServerTokenServices tokenServices;

    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();

    private ApplicationEventPublisher eventPublisher;

    public OAuth2ClientAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(new NoopAuthenticationManager());
        setAuthenticationDetailsSource(authenticationDetailsSource);
    }
    //......
}    

Its constructor needs to pass in defaultFilterProcessesUrl to specify which url this filter intercepts.
It relies on OAuth2RestTemplate to obtain token.
It also relies on ResourceServerTokenServices to verify token.

oauth client config

After the above analysis, this config is mainly configured with 3

  • OAuth2RestTemplate(Get token)
  • ResourceServerTokenServices(Verify token)
  • OAuth2ClientAuthenticationProcessingFilter(Intercept redirectUri, obtain token according to authentication code, and rely on the first two objects.)
@Configuration
@EnableOAuth2Client
public class Oauth2ClientConfig {

    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext context, OAuth2ProtectedResourceDetails details) {
        OAuth2RestTemplate template = new OAuth2RestTemplate(details, context);

        AuthorizationCodeAccessTokenProvider authCodeProvider = new AuthorizationCodeAccessTokenProvider();
        authCodeProvider.setStateMandatory(false);
        AccessTokenProviderChain provider = new AccessTokenProviderChain(
                Arrays.asList(authCodeProvider));
        template.setAccessTokenProvider(provider);
    }

    /**
     * 注册处理redirect uri的filter
     * @param oauth2RestTemplate
     * @param tokenService
     * @return
     */
    @Bean
    public OAuth2ClientAuthenticationProcessingFilter oauth2ClientAuthenticationProcessingFilter(
            OAuth2RestTemplate oauth2RestTemplate,
            RemoteTokenServices tokenService) {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(redirectUri);
        filter.setRestTemplate(oauth2RestTemplate);
        filter.setTokenServices(tokenService);


        //设置回调成功的页面
        filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler() {
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                this.setDefaultTargetUrl("/home");
                super.onAuthenticationSuccess(request, response, authentication);
            }
        });
        return filter;
    }

    /**
     * 注册check token服务
     * @param details
     * @return
     */
    @Bean
    public RemoteTokenServices tokenService(OAuth2ProtectedResourceDetails details) {
        RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setCheckTokenEndpointUrl(checkTokenUrl);
        tokenService.setClientId(details.getClientId());
        tokenService.setClientSecret(details.getClientSecret());
        return tokenService;
    }
}

security config

The OAuth 2 ClientauthenticationProcessingFilter is defined above, and the most important step is to configure the sequence of filter. If the configuration is improper, all previous efforts will be wasted.

This needs to be configured before BasicAuthenticationFilter.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    OAuth2ClientAuthenticationProcessingFilter oauth2ClientAuthenticationProcessingFilter;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll()
        .and()
        .addFilterBefore(oauth2ClientAuthenticationProcessingFilter,BasicAuthenticationFilter.class)
        .csrf().disable();
    }
}

abnormal

Possible CSRF detected - state parameter was required but no state could be found

Some people say that the development is local. auth server and client are both localhost, causing the problem of JSESSIONID interaction. This can be solved by configuring the client’s context-path or session name.

Session is configured here.

server:
  port: 8081
  session:
    cookie:
      name: OAUTH2SESSION

However, it seems that it has not been solved. Finally, the stateMandatory property of authorizationCodeAccessTokenProvider is temporarily closed first.

Client-related yml configuration

security:
  oauth2:
    client:
      clientId: demoApp
      clientSecret: demoAppSecret
      accessTokenUri: ${TOKEN_URL:http://localhost:8080}/oauth/token
      userAuthorizationUri: ${USER_AUTH_URL:http://localhost:8080}/oauth/authorize
      pre-established-redirect-uri: http://localhost:8081/callback

Verification

http://localhost:8080/oauth/authorize?response_type=code&client_id=demoApp&redirect_uri=http://localhost:8081/callback

After that, login, authorization, callback, and jump to the set /home

After callback, token will be bound to the current session, and then OAuth2RestTemplate can be used to transparently access authorized resources.

@RequestMapping("")
@RestController
public class DemoController {

    @Autowired
    OAuth2RestTemplate oAuth2RestTemplate;

    @Value("${client.resourceServerUrl}")
    String resourceServerUrl;

    @GetMapping("/demo/{id}")
    public String getDemoAuthResource(@PathVariable Long id){
        ResponseEntity<String> responseEntity = oAuth2RestTemplate.getForEntity(resourceServerUrl+"/demo/"+id, String.class);
        return responseEntity.getBody();
    }
}

This is the end of the story.

doc