springsecurity-oauth2令牌 access_token验证

在客户端获取到令牌后,访问资源时,可以设置如下请求头或者请求参数中加上 access_token。

Authorization: Bearer access_token

​ 在前两篇博客中《access_token的生成》《如何生成jwt》,我们经过源码分析,已经知道 access_token 产生的大体流程,并且知道如何自定义 token 的格式,其中,授权服务器配置中的 AuthorizationServerEndpointsConfigurer 可谓极为重要;而关于access_token 的验证流程,我们就从对应的资源服务器的配置入手。

1 资源服务器配置

​ 与生成access_token 有固定接口 /oauth/token 不同,我们肯定不能通过业务资源的接口地址去找到验证 token 源码的入口。SpringSecurity原理是责任链模式,绝大数功能都是过滤器实现的,这里也不例外。资源服务器的配置如下:

​ 这里重点关注一下ResourceServerSecurityConfigurer。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId("message")//设置资源id
                .tokenStore(new JwtTokenStore(jwtAccessTokenConverter()))
                .tokenServices(new RemoteTokenServices())
                .tokenExtractor(new BearerTokenExtractor())
        ;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

}

2 ResourceServerSecurityConfigurer

在 ResourceServerSecurityConfigurer 中我们看到鉴权所需要的两个重要元素,Filter 和 AuthenticationManager。看到下面的代码,一些比较敏感的小伙伴可能会推测,所谓的 access_token 验证应该就是用这里配置的 Filter 和 AuthenticationManager进行鉴权了。

public final class ResourceServerSecurityConfigurer extends
        SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    ......
    private OAuth2AuthenticationProcessingFilter resourcesServerFilter;

    private AuthenticationManager authenticationManager;
    ......
    
    @Override
    public void configure(HttpSecurity http) throws Exception {

        AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
        //N 指定过滤器
        resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
        resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
        //N 设置 AuthenticationManager
        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()
                //N 将过滤器放到过滤器链中
                .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);
        // @formatter:on
    }

    private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
        OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
        if (this.authenticationManager != null) {
            //如果自己配置了 authenticationManager
            if (authenticationManager instanceof OAuth2AuthenticationManager) {
                oauthAuthenticationManager = (OAuth2AuthenticationManager) authenticationManager;
            } else {
                return authenticationManager;
            }
        }
        oauthAuthenticationManager.setResourceId(resourceId);
        oauthAuthenticationManager.setTokenServices(resourceTokenServices(http));
        oauthAuthenticationManager.setClientDetailsService(clientDetails());
        return oauthAuthenticationManager;
    }
    ......
    
}    

​ 通过上面的代码.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)我们可以知道,用于处理令牌验证的过滤器应该就是 OAuth2AuthenticationProcessingFilter 了,并且默认AuthenticationManager是OAuth2AuthenticationManager。

3 OAuth2AuthenticationProcessingFilter

​ 过滤器肯定是要看 doFilter 了,核心逻辑如下:

  • 从request中提取出 access_token,并构建 authentication 对象;
  • 设置 authentication 对象的 details (token_type、token_value、sessionId、remoteAddress 等,来源于request);
  • 利用 authentication 对象进行鉴权。
  • 鉴权成功对象放到 SecurityContext 中
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
    
    private TokenExtractor tokenExtractor = new BearerTokenExtractor();
    ......
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setTokenExtractor(TokenExtractor tokenExtractor) {
        this.tokenExtractor = tokenExtractor;
    }
    ......
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        boolean debug = logger.isDebugEnabled();
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            //N 从 request 中提取access_token
            Authentication authentication = this.tokenExtractor.extract(request);
            if (authentication == null) {
                if (this.stateless && this.isAuthenticated()) {
                    if (debug) {
                        logger.debug("Clearing security context.");
                    }

                    SecurityContextHolder.clearContext();
                }

                if (debug) {
                    logger.debug("No token in request, will continue chain.");
                }
            } else {
                //N 把access_token 放到request属性中 {"OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE":"access_token"}
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                if (authentication instanceof AbstractAuthenticationToken) {
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                    //request中的 remoteAddress、sessionId、tokenValue、tokenType 等信息放到details中
                    needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
                }

                //N 鉴权
                Authentication authResult = this.authenticationManager.authenticate(authentication);
                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                this.eventPublisher.publishAuthenticationSuccess(authResult);
                //N 设置安全上下文
                SecurityContextHolder.getContext().setAuthentication(authResult);
            }
        } catch (OAuth2Exception e) {
            SecurityContextHolder.clearContext();
            if (debug) {
                logger.debug("Authentication request failed: " + e);
            }

            this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(e.getMessage(), e), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
            this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(e.getMessage(), e));
            return;
        }

        chain.doFilter(request, response);
    }
    ......
}

​ 关于 BearerTokenExtractor,作用就是从request 的请求头或者请求参数中提取access_token,并设置 tokenType(例如bearer类型),逻辑相对简单,这里就不展开了。当然我们也可以定义自己的 token 提取器,只要在配置资源服务器时设置给 ResourceServerSecurityConfigurer 对象即可。

4 OAuth2AuthenticationManager

​ AuthenticationManager 的作用就是鉴权,这里的逻辑也很简单,就是验证token是否是真的由授权服务器产生,如果是,继续校验resourceId 和scope。资源服务器默认的resourceId 在 ResourceServerSecurityConfigurer 类中,是 “oauth2-resource”。

​ 这里如果令牌对应的 resourceIds 是空的,就不校验resourceId了,换种说法就是如果我们数据库接入端表中没配置resourceId,就拥有所有资源服务器的访问权限,总感觉不爽。

public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean {

    private ResourceServerTokenServices tokenServices;
    ......
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (authentication == null) {
            throw new InvalidTokenException("Invalid token (token not found)");
        }
        //N 提取 access_token
        String token = (String) authentication.getPrincipal();

        //N 验证 access_token 是否真的由授权服务器产生
        OAuth2Authentication auth = tokenServices.loadAuthentication(token);
        if (auth == null) {
            throw new InvalidTokenException("Invalid token: " + token);
        }

        Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
        //N 校验该令牌是否拥有访问资源服务器的权限
        if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
            throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
        }
        //N 校验令牌和client 的scope是否相符
        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;

    }    
    
    //clientDetailsService 可能为null,后面我们单独说明。
    private void checkClientDetails(OAuth2Authentication auth) {
        if (clientDetailsService != null) {
            //N 进入这里说明要么我们自己设置了clientDetailsService,要么资源服务与授权服务是同一个服务
            ClientDetails client;
            try {
                client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
            }
            catch (ClientRegistrationException e) {
                throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
            }
            //数据库查询到的客户端配置的scope
            Set<String> allowed = client.getScope();
            //N 校验令牌的scope
            for (String scope : auth.getOAuth2Request().getScope()) {
                if (!allowed.contains(scope)) {
                    throw new OAuth2AccessDeniedException(
                            "Invalid token contains disallowed scope (" + scope + ") for this client");
                }
            }
        }
    }

}    

接下来我们继续探索 tokenServices.loadAuthentication(token);方法都做了什么。

5 ResourceServerTokenServices

​ ResourceServerTokenServices 的默认实现是 DefaultTokenServices, 这段代码可以在ResourceServerSecurityConfigurer 中看到,当然,如果我们配置了 resourceTokenServices,在 if 中则会直接返回配置的实现。

public final class ResourceServerSecurityConfigurer extends
        SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
    ......
    private ResourceServerTokenServices tokenServices(HttpSecurity http) {
       if (resourceTokenServices != null) {
          return resourceTokenServices;
       }
       DefaultTokenServices tokenServices = new DefaultTokenServices();
       tokenServices.setTokenStore(tokenStore());
       tokenServices.setSupportRefreshToken(true);
       tokenServices.setClientDetailsService(clientDetails());
       this.resourceTokenServices = tokenServices;
       return tokenServices;
    }

    private ClientDetailsService clientDetails() {
        //获取授权服务设置的ClientDetailsService
        return getBuilder().getSharedObject(ClientDetailsService.class);
    }
    ......
}

Security 为我们提供了两个 ResourceServerTokenServices 子类。

在这里插入图片描述

5.1 DefaultTokenServices

​ 通过 tokenStore 可以推测出,这个必须要和授权服务器能够访问相同的存储才行。(JwtTokenStore比较特殊,它并没有存储token,因为验证jwt只需要对称密钥或者公钥就可以,感兴趣的小伙伴可以看 JwtTokenStore 的源码)

public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
    //N 从存储中读取access_token,这里 JwtTokenStore 实现的比较特殊,是返回接收到的jwt
    //N 显然这要求授权服务器和资源服务器能否访问相同的存储,例如DB 或者redis
    OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue);
    if (accessToken == null) {
        throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
    } else if (accessToken.isExpired()) {
        //令牌过期处理
        this.tokenStore.removeAccessToken(accessToken);
        throw new InvalidTokenException("Access token expired: " + accessTokenValue);
    } else {
        //N 验证token ,如果是 JwtTokenStore ,验证jwt签名
        OAuth2Authentication result = this.tokenStore.readAuthentication(accessToken);
        if (result == null) {
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        } else {
            //这里如果我们自己没有配置clientDetailsService,默认共享授权服务的,可能是null
            if (this.clientDetailsService != null) {
                String clientId = result.getOAuth2Request().getClientId();
                try {
                    this.clientDetailsService.loadClientByClientId(clientId);
                } catch (ClientRegistrationException var6) {
                    throw new InvalidTokenException("Client not valid: " + clientId, var6);
                }
            }
            return result;
        }
    }
}

​ 这里说明一下 clientDetailsService,上面我们提到 OAuth2AuthenticationManager 和 DefaultTokenServices 中的this.clientDetailsService 都可能是 null 的问题。这是因为他们的取值都是 ResourceServerSecurityConfigurer 中的如下方法,getSharedObject获取的是授权服务配置的 ClientDetailsService。当授权服务与资源服务不是同一个服务的时候,getSharedObject 就会取不到值。

 private ClientDetailsService clientDetails() {
        //获取授权服务设置的ClientDetailsService
        return getBuilder().getSharedObject(ClientDetailsService.class);
}

​ 在前面的博文,我们分析令牌生成时,授权服务只是提到了 AuthorizationServerEndpointsConfigurer 的配置,这里我们补充一下另外两个配置。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private DataSource dataSource;

    /**
     * 对auth2 提供的接口做访问规则配置和添加自定义过滤器
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                //允许表单认证,对于接口/oauth/token,如果开启此配置,并且url中有client_id和client_secret会触发ClientCredentialsTokenEndpointFilter用于校验客户端是否有权限
                .allowFormAuthenticationForClients()
                //设置接口/oauth/check_token 访问权限,默认denyAll(),资源服务器可以调用这个验证token,如果是jwt,资源服务器也可以自己通过密钥验证
                .checkTokenAccess("isAuthenticated()")
                //提供jwt的公钥接口 /oauth/token_key
                .tokenKeyAccess("permitAll()")
                //N 添加自定义的过滤器
                //通过AbstractAuthenticationProcessingFilter断点的additionalFilter可以看到该过滤器再链中的位置
                //.addTokenEndpointAuthenticationFilter(new MyFilterFive())
                //.passwordEncoder()
        ;
        
    }

    /**
     * 接入端管理配置,对应的是 oauth_client_details 表,实体是 BaseClientDetails
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /*clients
                .inMemory()
                //client_1是客户端授权方式
                .withClient("client_1")//接入端id
                .secret(new BCryptPasswordEncoder().encode("123456"))//接入端密钥
                //.resourceIds(DEMO_RESOURCE_ID)//资源id
                .authorizedGrantTypes("client_credentials", "refresh_token")//授权方式
                .scopes("select")//访问域
                .authorities("client");//权限*/
        clients.jdbc(dataSource);
        //clients.withClientDetails(new JdbcClientDetailsService(dataSource));
    }

    /**
     * SpringSecurity-OAuth2 提供端口
     * /oauth/authorize:授权端口
     * /oauth/token:令牌端口
     * /oauth/confirm_access:用户确认授权提交端口
     * /oauth/error:授权服务错误信息端口
     * /oauth/check_token:用于资源服务器访问的令牌解析端口
     * /oauth/token_key:提供公有秘钥端口,如果使用的是 JWT 令牌的话
     * pathMapping 可以映射成其他地址
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .pathMapping("/oauth/token","/cloneli/token")
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)//允许的请求方式
                //tokenStore默认内存存储,重启服务token就会失效
                 .tokenStore(new InMemoryTokenStore())
                .reuseRefreshTokens(true)
                //.tokenEnhancer()
                .accessTokenConverter(jwtAccessTokenConverter())
                //用于配置密码式的授权方式,如果不设置,密码模式请求token是,token为null,TokenEndpoint会提示不支持password授权模式,这里配置就是parent AuthenticationManager
                //.authenticationManager(authenticationManager())
                /*.tokenGranter(new TokenGranter() {
                    @Override
                    public OAuth2AccessToken grant(String s, TokenRequest tokenRequest) {
                        return null;
                    }
                })*/
        ;
    }



}

​ 在上面AuthorizationServerSecurityConfigurer 的配置中,可以看到 我们设置了 “/oauth/check_token” 接口.checkTokenAccess(“isAuthenticated()”),即需要授权才能访问该接口。

​ 而权限的配置就在下面 AuthorizationServerSecurityConfiguration 的方法中,同时有包括setSharedObject 的ClientDetailsService,因此授权服务和资源服务不是同一个服务,也就没有@EnableAuthorizationServer注解,就不会执行setSharedObject 的操作,这就是为什么getSharedObject(ClientDetailsService.class)可能是null的原因。

@Configuration
@Order(0)
//ClientDetailsServiceConfiguration 提供clientDetailsService的bean
@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })
public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
    ......
    @Autowired
	private ClientDetailsService clientDetailsService;
    ......
    @Override
	protected void configure(HttpSecurity http) throws Exception {
		AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
		FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
		http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
		configure(configurer);
		http.apply(configurer);
		String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
		String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
		String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
		if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
			UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
			endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
		}
		// @formatter:off
		http
        	.authorizeRequests()
            	.antMatchers(tokenEndpointPath).fullyAuthenticated()
            	//设置接口的访问权限
            	.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
            	.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
        .and()
        	.requestMatchers()
            	.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
        .and()
        	.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
		// @formatter:on
         //setSharedObject clientDetailsService
		http.setSharedObject(ClientDetailsService.class, clientDetailsService);
	}
    ......
}    

//没有@EnableAuthorizationServer注解,就不会执行AuthorizationServerSecurityConfiguration的方法
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {

}
5.2 RemoteTokenServices

​ 见名知意,这个需要调用其他服务验证 access_token,显然能提供这个服务的应该就是授权服务了。授权服务提供了 CheckTokenEndpoint 用于验证 access_token 的接口如下,而其底层实现也是 resourceServerTokenServices 。

/oauth/check_token

​ 调用授权服务器的 “/oauth/check_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();
    //N 设置请求头,客户端凭证,访问授权服务器需要认证
    headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
    //N 设置请求地址,并发送请求获取验证结果
    Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

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

    Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
    //N 构造 OAuth2Authentication 对象
    return tokenConverter.extractAuthentication(map);
}
5.3 LocalTokenServices (自定义)

​ 当然如果上面两种校验 access_token 的实现不能满足项目需求,也可以自定义自己的ResourceServerTokenServices 实现类,重写验证access_token的逻辑。

​ 综上,整个令牌验证的流程就是 ResourceServerSecurityConfigurer指定了OAuth2AuthenticationProcessingFilter过滤器,过滤器调用 AuthenticationManager, AuthenticationManager 又调用 ResourceServerTokenServices 实现令牌验证,其中具体实现我们可以通过资源服务配置进行修改。

​ 到这里,令牌验证通过,Authentication的信息就会放到线程变量SecurityContext中,然后过滤器就会放行请求。

​ 不过验证令牌后得到的权限信息还没有用到。这个我们后续的文章再继续研究

public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
    ......
}
  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值