2. springsecurity oauth2 资源服务配置 版本2.2.2

1. @EnableResourceServer 注解
  • @EnableResourceServer 配置接口为ResourceServerConfigurer,接口内容如下
public interface ResourceServerConfigurer {

    /**
     * Add resource-server specific properties (like a resource id). The defaults should work for many applications, but
     * you might want to change at least the resource id.
     * 
     * @param resources configurer for the resource server
     * @throws Exception if there is a problem
     */
    void configure(ResourceServerSecurityConfigurer resources) throws Exception;

    /**
     * Use this to configure the access rules for secure resources. By default all resources not in "/oauth/**"
     * are protected (but no specific rules about scopes are given, for instance). You also get an
     * {@link OAuth2WebSecurityExpressionHandler} by default.
     * 
     * @param http the current http filter configuration
     * @throws Exception if there is a problem
     */
    void configure(HttpSecurity http) throws Exception;

}

其默认实现为OAuth2ResourceServerConfiguration

@Configuration
@Conditional({OAuth2ResourceServerConfiguration.ResourceServerCondition.class})
@ConditionalOnClass({EnableResourceServer.class, SecurityProperties.class})
@ConditionalOnWebApplication
@ConditionalOnBean({ResourceServerConfiguration.class})
@Import({ResourceServerTokenServicesConfiguration.class})
public class OAuth2ResourceServerConfiguration {
    private final ResourceServerProperties resource;

    public OAuth2ResourceServerConfiguration(ResourceServerProperties resource) {
        this.resource = resource;
    }

    @Bean
    @ConditionalOnMissingBean({ResourceServerConfigurer.class})
    public ResourceServerConfigurer resourceServer() {
        return new OAuth2ResourceServerConfiguration.ResourceSecurityConfigurer(this.resource);
    }

    @ConditionalOnBean({AuthorizationServerEndpointsConfiguration.class})
    private static class AuthorizationServerEndpointsConfigurationBeanCondition {
        private AuthorizationServerEndpointsConfigurationBeanCondition() {
        }

        public static boolean matches(ConditionContext context) {
            Class type = OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition.class;
            Conditional conditional = (Conditional)AnnotationUtils.findAnnotation(type, Conditional.class);
            StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(type);
            Class[] var4 = conditional.value();
            int var5 = var4.length;

            for(int var6 = 0; var6 < var5; ++var6) {
                Class conditionType = var4[var6];
                Condition condition = (Condition)BeanUtils.instantiateClass(conditionType);
                if (condition.matches(context, metadata)) {
                    return true;
                }
            }

            return false;
        }
    }

    protected static class ResourceServerCondition extends SpringBootCondition implements ConfigurationCondition {
        private static final Bindable> STRING_OBJECT_MAP = Bindable.mapOf(String.class, Object.class);
        private static final String AUTHORIZATION_ANNOTATION = "org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration";

        protected ResourceServerCondition() {
        }

        public ConfigurationPhase getConfigurationPhase() {
            return ConfigurationPhase.REGISTER_BEAN;
        }

        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Builder message = ConditionMessage.forCondition("OAuth ResourceServer Condition", new Object[0]);
            Environment environment = context.getEnvironment();
            if (!(environment instanceof ConfigurableEnvironment)) {
                return ConditionOutcome.noMatch(message.didNotFind("A ConfigurableEnvironment").atAll());
            } else if (this.hasOAuthClientId(environment)) {
                return ConditionOutcome.match(message.foundExactly("client-id property"));
            } else {
                Binder binder = Binder.get(environment);
                String prefix = "security.oauth2.resource.";
                if (binder.bind(prefix + "jwt", STRING_OBJECT_MAP).isBound()) {
                    return ConditionOutcome.match(message.foundExactly("JWT resource configuration"));
                } else if (binder.bind(prefix + "jwk", STRING_OBJECT_MAP).isBound()) {
                    return ConditionOutcome.match(message.foundExactly("JWK resource configuration"));
                } else if (StringUtils.hasText(environment.getProperty(prefix + "user-info-uri"))) {
                    return ConditionOutcome.match(message.foundExactly("user-info-uri property"));
                } else if (StringUtils.hasText(environment.getProperty(prefix + "token-info-uri"))) {
                    return ConditionOutcome.match(message.foundExactly("token-info-uri property"));
                } else {
                    return ClassUtils.isPresent("org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration", (ClassLoader)null) && OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition.matches(context) ? ConditionOutcome.match(message.found("class").items(new Object[]{"org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration"})) : ConditionOutcome.noMatch(message.didNotFind("client ID, JWT resource or authorization server").atAll());
                }
            }
        }

        private boolean hasOAuthClientId(Environment environment) {
            return StringUtils.hasLength(environment.getProperty("security.oauth2.client.client-id"));
        }
    }

    protected static class ResourceSecurityConfigurer extends ResourceServerConfigurerAdapter {
        private ResourceServerProperties resource;

        public ResourceSecurityConfigurer(ResourceServerProperties resource) {
            this.resource = resource;
        }

        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(this.resource.getResourceId());
        }

        public void configure(HttpSecurity http) throws Exception {
            ((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
        }
    }
}

我们可以提供自己的配置,如下所示

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    @Autowired
    protected ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
    @Autowired
    protected PlatformAccessDeniedHandler platformAccessDeniedHandler;
    @Autowired
    protected RemoteTokenServices remoteTokenServices;
    @Autowired
    protected UserDetailsService userDetailsService;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/actuator/**"
                , "/v2/api-docs").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable();
    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
        userTokenConverter.setUserDetailsService(userDetailsService);
        accessTokenConverter.setUserTokenConverter(userTokenConverter);
        remoteTokenServices.setRestTemplate(lbRestTemplate());
        remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
        resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
                .accessDeniedHandler(platformAccessDeniedHandler)
                .tokenServices(remoteTokenServices);
    }
    @Bean
    @LoadBalanced
    public RestTemplate lbRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        //设置自定义异常处理
        restTemplate.setErrorHandler(new PlatformResponseErrorHandler());
        return restTemplate;
    }

}

ResourceServerSecurityConfigurer 在方法configure(HttpSecurity http) 配置了OAuth2AuthenticationProcessingFilter过滤器,代码如下

@Override
    public void configure(HttpSecurity http) throws Exception {

        AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
        resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
        resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
        resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
        if (eventPublisher != null) {
            resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
        }
        if (tokenExtractor != null) {
            resourcesServerFilter.setTokenExtractor(tokenExtractor);
        }
        resourcesServerFilter = postProcess(resourcesServerFilter);
        resourcesServerFilter.setStateless(stateless);

        // @formatter:off
        http
            .authorizeRequests().expressionHandler(expressionHandler)
        .and()
            .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);
        // @formatter:on
    }
2.资源认证的核心 OAuth2AuthenticationProcessingFilter过滤器。

来看一下它的源码

public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {

    private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);

    private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();

    private AuthenticationManager authenticationManager;

    private AuthenticationDetailsSource authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();

    private TokenExtractor tokenExtractor = new BearerTokenExtractor();

    private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();

    private boolean stateless = true;

    /**
     * Flag to say that this filter guards stateless resources (default true). Set this to true if the only way the
     * resource can be accessed is with a token. If false then an incoming cookie can populate the security context and
     * allow access to a caller that isn't an OAuth2 client.
     * 
     * @param stateless the flag to set (default true)
     */
    public void setStateless(boolean stateless) {
        this.stateless = stateless;
    }

    /**
     * @param authenticationEntryPoint the authentication entry point to set
     */
    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    /**
     * @param authenticationManager the authentication manager to set (mandatory with no default)
     */
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /**
     * @param tokenExtractor the tokenExtractor to set
     */
    public void setTokenExtractor(TokenExtractor tokenExtractor) {
        this.tokenExtractor = tokenExtractor;
    }

    /**
     * @param eventPublisher the event publisher to set
     */
    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    /**
     * @param authenticationDetailsSource The AuthenticationDetailsSource to use
     */
    public void setAuthenticationDetailsSource(
            AuthenticationDetailsSource authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void afterPropertiesSet() {
        Assert.state(authenticationManager != null, "AuthenticationManager is required");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
            ServletException {

        final boolean debug = logger.isDebugEnabled();
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;

        try {

            Authentication authentication = tokenExtractor.extract(request);
            
            if (authentication == null) {
                if (stateless && isAuthenticated()) {
                    if (debug) {
                        logger.debug("Clearing security context.");
                    }
                    SecurityContextHolder.clearContext();
                }
                if (debug) {
                    logger.debug("No token in request, will continue chain.");
                }
            }
            else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                if (authentication instanceof AbstractAuthenticationToken) {
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                    needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
                }
                Authentication authResult = authenticationManager.authenticate(authentication);

                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                eventPublisher.publishAuthenticationSuccess(authResult);
                SecurityContextHolder.getContext().setAuthentication(authResult);

            }
        }
        catch (OAuth2Exception failed) {
            SecurityContextHolder.clearContext();

            if (debug) {
                logger.debug("Authentication request failed: " + failed);
            }
            eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
                    new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

            authenticationEntryPoint.commence(request, response,
                    new InsufficientAuthenticationException(failed.getMessage(), failed));

            return;
        }

        chain.doFilter(request, response);
    }

    private boolean isAuthenticated() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
            return false;
        }
        return true;
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void destroy() {
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher {
        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
        }

        public void publishAuthenticationSuccess(Authentication authentication) {
        }
    }

}

当access_token 不为空时,认证管理器authenticationManager 即 OAuth2AuthenticationManager进行身份认证。身份认证代码如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (authentication == null) {
            throw new InvalidTokenException("Invalid token (token not found)");
        }
        String token = (String) authentication.getPrincipal();
        OAuth2Authentication auth = tokenServices.loadAuthentication(token);
        if (auth == null) {
            throw new InvalidTokenException("Invalid token: " + token);
        }

        Collection resourceIds = auth.getOAuth2Request().getResourceIds();
        if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
            throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
        }

        checkClientDetails(auth);

        if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
            OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
            // Guard against a cached copy of the same details
            if (!details.equals(auth.getDetails())) {
                // Preserve the authentication details from the one loaded by token services
                details.setDecodedDetails(auth.getDetails());
            }
        }
        auth.setDetails(authentication.getDetails());
        auth.setAuthenticated(true);
        return auth;

    }

tokenServices(实现类为RemoteTokenServices) 调用loadAuthentication(token) 方法进行身份认证

@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

    MultiValueMap formData = new LinkedMultiValueMap();
    formData.add(tokenName, accessToken);
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
    Map map = postForMap(checkTokenEndpointUrl, formData, headers);

    if (map.containsKey("error")) {
        if (logger.isDebugEnabled()) {
            logger.debug("check_token returned error: " + map.get("error"));
        }
        throw new InvalidTokenException(accessToken);
    }

    // gh-838
    if (!Boolean.TRUE.equals(map.get("active"))) {
        logger.debug("check_token returned active attribute: " + map.get("active"));
        throw new InvalidTokenException(accessToken);
    }

    return tokenConverter.extractAuthentication(map);
}
... 省略其它方法 ...
private Map postForMap(String path, MultiValueMap formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }
        @SuppressWarnings("rawtypes")
        Map map = restTemplate.exchange(path, HttpMethod.POST,
                new HttpEntity>(formData, headers), Map.class).getBody();
        @SuppressWarnings("unchecked")
        Map result = map;
        return result;
    }

跟踪代码,我们可以看到最终是使用 restTemplatecheckTokenEndpointUrl 进行认证,checkTokenEndpointUrl 的值是我们在配置文件中配置的 token-info-uri

image.png

接下来便进入 CheckTokenEndpoint 完成校验

@FrameworkEndpoint
public class CheckTokenEndpoint {
    private ResourceServerTokenServices resourceServerTokenServices;
    private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
    protected final Log logger = LogFactory.getLog(this.getClass());
    private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

    public CheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
        this.resourceServerTokenServices = resourceServerTokenServices;
    }

    public void setExceptionTranslator(WebResponseExceptionTranslator exceptionTranslator) {
        this.exceptionTranslator = exceptionTranslator;
    }

    public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
        this.accessTokenConverter = accessTokenConverter;
    }

    @RequestMapping({"/oauth/check_token"})
    @ResponseBody
    public Map checkToken(@RequestParam("token") String value) {
        OAuth2AccessToken token = this.resourceServerTokenServices.readAccessToken(value);
        if (token == null) {
            throw new InvalidTokenException("Token was not recognised");
        } else if (token.isExpired()) {
            throw new InvalidTokenException("Token has expired");
        } else {
            OAuth2Authentication authentication = this.resourceServerTokenServices.loadAuthentication(token.getValue());
            Map response = this.accessTokenConverter.convertAccessToken(token, authentication);
            response.put("active", true);
            return response;
        }
    }

    @ExceptionHandler({InvalidTokenException.class})
    public ResponseEntity handleException(Exception e) throws Exception {
        this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
            public int getHttpErrorCode() {
                return 400;
            }
        };
        return this.exceptionTranslator.translate(e400);
    }
}

至此,资源服务器主要内容全部完成。

PS:
本人发现 CheckTokenEndpoint 认证失败时会抛出异常,restTemplate 调用 check_token 时,如果使用默认的异常处理类DefaultResponseErrorHandler异常处理逻辑如下图所示,会继续抛出异常,导致调用端出现500错误。

protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
        switch (statusCode.series()) {
            case CLIENT_ERROR:
                throw new HttpClientErrorException(statusCode, response.getStatusText(),
                        response.getHeaders(), getResponseBody(response), getCharset(response));
            case SERVER_ERROR:
                throw new HttpServerErrorException(statusCode, response.getStatusText(),
                        response.getHeaders(), getResponseBody(response), getCharset(response));
            default:
                throw new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(),
                        response.getHeaders(), getResponseBody(response), getCharset(response));
        }
    }

可以通过 restTemplate.setErrorHandler(new PlatformResponseErrorHandler()); 设置自定义异常处理类来进行异常处理。

@Slf4j
public class PlatformResponseErrorHandler extends DefaultResponseErrorHandler {

    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException{
        return super.hasError(response);
    }

    @Override
    protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
        log.error("RestTemplate 异常信息 statusCode={}, response={}",statusCode, response.toString());
//        super.handleError(response, statusCode);
    }
}

以上仅仅是个人的一些理解及查看的源码,如果有错误或不足,欢迎指正!

你可能感兴趣的:(2. springsecurity oauth2 资源服务配置 版本2.2.2)