美文网首页工作生活
2. springsecurity oauth2 资源服务配置

2. springsecurity oauth2 资源服务配置

作者: 莫非的老公 | 来源:发表于2019-07-09 15:58 被阅读0次
    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 <i>not</i> 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<OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition> 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<? extends Condition> 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<Map<String, Object>> 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<HttpServletRequest, ?> 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<HttpServletRequest, ?> 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<String> 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<String, String> formData = new LinkedMultiValueMap<String, String>();
        formData.add(tokenName, accessToken);
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
        Map<String, Object> 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<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
            if (headers.getContentType() == null) {
                headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            }
            @SuppressWarnings("rawtypes")
            Map map = restTemplate.exchange(path, HttpMethod.POST,
                    new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
            @SuppressWarnings("unchecked")
            Map<String, Object> 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<String, ?> 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<String, Object> response = this.accessTokenConverter.convertAccessToken(token, authentication);
                response.put("active", true);
                return response;
            }
        }
    
        @ExceptionHandler({InvalidTokenException.class})
        public ResponseEntity<OAuth2Exception> 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 资源服务配置

        本文链接:https://www.haomeiwen.com/subject/lydncctx.html