在客户端获取到令牌后,访问资源时,可以设置如下请求头或者请求参数中加上accesstoken。Authorization:Beareraccesstoken 在前两篇《auth2生成token》和《token转jwt》中,我们已经知道accesstoken产生的大体流程,并且知道如何自定义token的格式,其中,授权服务器配置中的AuthorizationServerEndpointsConfigurer可谓极为重要;而关于accesstoken的验证流程,我们就从对应的资源服务器的配置入手。1资源服务器配置 与生成accesstoken有固定接口oauthtoken不同,我们肯定不能通过业务资源的接口地址去找到验证token源码的入口。SpringSecurity原理是责任链模式,绝大数功能都是过滤器实现的,这里也不例外。资源服务器的配置如下: 这里重点关注一下ResourceServerSecurityConfigurer。ConfigurationEnableResourceServerpublicclassResourceServerConfigextendsResourceServerConfigurerAdapter{Overridepublicvoidconfigure(ResourceServerSecurityConfigurerresources)throwsException{resources。resourceId(message)设置资源id。tokenStore(newJwtTokenStore(jwtAccessTokenConverter()))。tokenServices(newRemoteTokenServices())。tokenExtractor(newBearerTokenExtractor());}Overridepublicvoidconfigure(HttpSecurityhttp)throwsException{super。configure(http);}}2ResourceServerSecurityConfigurer 在ResourceServerSecurityConfigurer中我们看到鉴权所需要的两个重要元素,Filter和AuthenticationManager。看到下面的代码,一些比较敏感的小伙伴可能会推测,所谓的accesstoken验证应该就是用这里配置的Filter和AuthenticationManager进行鉴权了。publicfinalclassResourceServerSecurityConfigurerextendsSecurityConfigurerAdapterDefaultSecurityFilterChain,HttpSecurity{。。。。。。privateOAuth2AuthenticationProcessingFilterresourcesServerFilter;privateAuthenticationManagerauthenticationManager;。。。。。。Overridepublicvoidconfigure(HttpSecurityhttp)throwsException{AuthenticationManageroauthAuthenticationManageroauthAuthenticationManager(http);N指定过滤器resourcesServerFilternewOAuth2AuthenticationProcessingFilter();resourcesServerFilter。setAuthenticationEntryPoint(authenticationEntryPoint);N设置AuthenticationManagerresourcesServerFilter。setAuthenticationManager(oauthAuthenticationManager);if(eventPublisher!null){resourcesServerFilter。setAuthenticationEventPublisher(eventPublisher);}if(tokenExtractor!null){resourcesServerFilter。setTokenExtractor(tokenExtractor);}resourcesServerFilterpostProcess(resourcesServerFilter);resourcesServerFilter。setStateless(stateless);formatter:offhttp。authorizeRequests()。expressionHandler(expressionHandler)。and()N将过滤器放到过滤器链中。addFilterBefore(resourcesServerFilter,AbstractPreAuthenticatedProcessingFilter。class)。exceptionHandling()。accessDeniedHandler(accessDeniedHandler)。authenticationEntryPoint(authenticationEntryPoint);formatter:on}privateAuthenticationManageroauthAuthenticationManager(HttpSecurityhttp){OAuth2AuthenticationManageroauthAuthenticationManagernewOAuth2AuthenticationManager();if(this。authenticationManager!null){如果自己配置了authenticationManagerif(authenticationManagerinstanceofOAuth2AuthenticationManager){oauthAuthenticationManager(OAuth2AuthenticationManager)authenticationManager;}else{returnauthenticationManager;}}oauthAuthenticationManager。setResourceId(resourceId);oauthAuthenticationManager。setTokenServices(resourceTokenServices(http));oauthAuthenticationManager。setClientDetailsService(clientDetails());returnoauthAuthenticationManager;}。。。。。。} 通过上面的代码。addFilterBefore(resourcesServerFilter,AbstractPreAuthenticatedProcessingFilter。class)我们可以知道,用于处理令牌验证的过滤器应该就是OAuth2AuthenticationProcessingFilter了,并且默认AuthenticationManager是OAuth2AuthenticationManager。3OAuth2AuthenticationProcessingFilter 过滤器肯定是要看doFilter了,核心逻辑如下:从request中提取出accesstoken,并构建authentication对象;设置authentication对象的details(tokentype、tokenvalue、sessionId、remoteAddress等,来源于request);利用authentication对象进行鉴权。鉴权成功对象放到SecurityContext中publicclassOAuth2AuthenticationProcessingFilterimplementsFilter,InitializingBean{privateTokenExtractortokenExtractornewBearerTokenExtractor();。。。。。。publicvoidsetAuthenticationManager(AuthenticationManagerauthenticationManager){this。authenticationManagerauthenticationManager;}publicvoidsetTokenExtractor(TokenExtractortokenExtractor){this。tokenExtractortokenExtractor;}。。。。。。publicvoiddoFilter(ServletRequestreq,ServletResponseres,FilterChainchain)throwsIOException,ServletException{booleandebuglogger。isDebugEnabled();HttpServletRequestrequest(HttpServletRequest)req;HttpServletResponseresponse(HttpServletResponse)res;try{N从request中提取accesstokenAuthenticationauthenticationthis。tokenExtractor。extract(request);if(authenticationnull){if(this。statelessthis。isAuthenticated()){if(debug){logger。debug(Clearingsecuritycontext。);}SecurityContextHolder。clearContext();}if(debug){logger。debug(Notokeninrequest,willcontinuechain。);}}else{N把accesstoken放到request属性中{OAuth2AuthenticationDetails。ACCESSTOKENVALUE:accesstoken}request。setAttribute(OAuth2AuthenticationDetails。ACCESSTOKENVALUE,authentication。getPrincipal());if(authenticationinstanceofAbstractAuthenticationToken){AbstractAuthenticationTokenneedsDetails(AbstractAuthenticationToken)authentication;request中的remoteAddress、sessionId、tokenValue、tokenType等信息放到details中needsDetails。setDetails(this。authenticationDetailsSource。buildDetails(request));}N鉴权AuthenticationauthResultthis。authenticationManager。authenticate(authentication);if(debug){logger。debug(Authenticationsuccess:authResult);}this。eventPublisher。publishAuthenticationSuccess(authResult);N设置安全上下文SecurityContextHolder。getContext()。setAuthentication(authResult);}}catch(OAuth2Exceptione){SecurityContextHolder。clearContext();if(debug){logger。debug(Authenticationrequestfailed:e);}this。eventPublisher。publishAuthenticationFailure(newBadCredentialsException(e。getMessage(),e),newPreAuthenticatedAuthenticationToken(accesstoken,NA));this。authenticationEntryPoint。commence(request,response,newInsufficientAuthenticationException(e。getMessage(),e));return;}chain。doFilter(request,response);}。。。。。。} 关于BearerTokenExtractor,作用就是从request的请求头或者请求参数中提取accesstoken,并设置tokenType(例如bearer类型),逻辑相对简单,这里就不展开了。当然我们也可以定义自己的token提取器,只要在配置资源服务器时设置给ResourceServerSecurityConfigurer对象即可。4OAuth2AuthenticationManager AuthenticationManager的作用就是鉴权,这里的逻辑也很简单,就是验证token是否是真的由授权服务器产生,如果是,继续校验resourceId和scope。资源服务器默认的resourceId在ResourceServerSecurityConfigurer类中,是oauth2resource。 如果令牌对应的resourceIds是空的,就不校验resourceId了,换种说法就是:如果我们数据库接入端表中没配置resourceId,就拥有所有资源服务器的访问权限,总感觉不爽。publicclassOAuth2AuthenticationManagerimplementsAuthenticationManager,InitializingBean{privateResourceServerTokenServicestokenServices;。。。。。。publicAuthenticationauthenticate(Authenticationauthentication)throwsAuthenticationException{if(authenticationnull){thrownewInvalidTokenException(Invalidtoken(tokennotfound));}N提取accesstokenStringtoken(String)authentication。getPrincipal();N验证accesstoken是否真的由授权服务器产生OAuth2AuthenticationauthtokenServices。loadAuthentication(token);if(authnull){thrownewInvalidTokenException(Invalidtoken:token);}CollectionStringresourceIdsauth。getOAuth2Request()。getResourceIds();N校验该令牌是否拥有访问资源服务器的权限if(resourceId!nullresourceIds!null!resourceIds。isEmpty()!resourceIds。contains(resourceId)){thrownewOAuth2AccessDeniedException(Invalidtokendoesnotcontainresourceid(resourceId));}N校验令牌和client的scope是否相符checkClientDetails(auth);if(authentication。getDetails()instanceofOAuth2AuthenticationDetails){OAuth2AuthenticationDetailsdetails(OAuth2AuthenticationDetails)authentication。getDetails();Guardagainstacachedcopyofthesamedetailsif(!details。equals(auth。getDetails())){Preservetheauthenticationdetailsfromtheoneloadedbytokenservicesdetails。setDecodedDetails(auth。getDetails());}}auth。setDetails(authentication。getDetails());auth。setAuthenticated(true);returnauth;}clientDetailsService可能为null,后面我们单独说明。privatevoidcheckClientDetails(OAuth2Authenticationauth){if(clientDetailsService!null){N进入这里说明要么我们自己设置了clientDetailsService,要么资源服务与授权服务是同一个服务ClientDetailsclient;try{clientclientDetailsService。loadClientByClientId(auth。getOAuth2Request()。getClientId());}catch(ClientRegistrationExceptione){thrownewOAuth2AccessDeniedException(Invalidtokencontainsinvalidclientid);}数据库查询到的客户端配置的scopeSetStringallowedclient。getScope();N校验令牌的scopefor(Stringscope:auth。getOAuth2Request()。getScope()){if(!allowed。contains(scope)){thrownewOAuth2AccessDeniedException(Invalidtokencontainsdisallowedscope(scope)forthisclient);}}}}} 接下来我们继续探索tokenServices。loadAuthentication(token);方法都做了什么。5ResourceServerTokenServices ResourceServerTokenServices的默认实现是DefaultTokenServices,这段代码可以在ResourceServerSecurityConfigurer中看到,当然,如果我们配置了resourceTokenServices,在if中则会直接返回配置的实现。publicfinalclassResourceServerSecurityConfigurerextendsSecurityConfigurerAdapterDefaultSecurityFilterChain,HttpSecurity{。。。。。。privateResourceServerTokenServicestokenServices(HttpSecurityhttp){if(resourceTokenServices!null){returnresourceTokenServices;}DefaultTokenServicestokenServicesnewDefaultTokenServices();tokenServices。setTokenStore(tokenStore());tokenServices。setSupportRefreshToken(true);tokenServices。setClientDetailsService(clientDetails());this。resourceTokenServicestokenServices;returntokenServices;}privateClientDetailsServiceclientDetails(){获取授权服务设置的ClientDetailsServicereturngetBuilder()。getSharedObject(ClientDetailsService。class);}。。。。。。} Security为我们提供了两个ResourceServerTokenServices子类。 5。1DefaultTokenServices 通过tokenStore可以推测出,这个必须要和授权服务器能够访问相同的存储才行。(JwtTokenStore比较特殊,它并没有存储token,因为验证jwt只需要对称密钥或者公钥就可以,感兴趣的小伙伴可以看JwtTokenStore的源码)publicOAuth2AuthenticationloadAuthentication(StringaccessTokenValue)throwsAuthenticationException,InvalidTokenException{N从存储中读取accesstoken,这里JwtTokenStore实现的比较特殊,是返回接收到的jwtN显然这要求授权服务器和资源服务器能否访问相同的存储,例如DB或者redisOAuth2AccessTokenaccessTokenthis。tokenStore。readAccessToken(accessTokenValue);if(accessTokennull){thrownewInvalidTokenException(Invalidaccesstoken:accessTokenValue);}elseif(accessToken。isExpired()){令牌过期处理this。tokenStore。removeAccessToken(accessToken);thrownewInvalidTokenException(Accesstokenexpired:accessTokenValue);}else{N验证token,如果是JwtTokenStore,验证jwt签名OAuth2Authenticationresultthis。tokenStore。readAuthentication(accessToken);if(resultnull){thrownewInvalidTokenException(Invalidaccesstoken:accessTokenValue);}else{这里如果我们自己没有配置clientDetailsService,默认共享授权服务的,可能是nullif(this。clientDetailsService!null){StringclientIdresult。getOAuth2Request()。getClientId();try{this。clientDetailsService。loadClientByClientId(clientId);}catch(ClientRegistrationExceptionvar6){thrownewInvalidTokenException(Clientnotvalid:clientId,var6);}}returnresult;}}} 这里说明一下clientDetailsService,上面我们提到OAuth2AuthenticationManager和DefaultTokenServices中的this。clientDetailsService都可能是null的问题。这是因为他们的取值都是ResourceServerSecurityConfigurer中的如下方法,getSharedObject获取的是授权服务配置的ClientDetailsService。当授权服务与资源服务不是同一个服务的时候,getSharedObject就会取不到值。privateClientDetailsServiceclientDetails(){获取授权服务设置的ClientDetailsServicereturngetBuilder()。getSharedObject(ClientDetailsService。class);} 在上面分析令牌生成时,授权服务只是提到了AuthorizationServerEndpointsConfigurer的配置,这里我们补充一下另外两个配置。ConfigurationEnableAuthorizationServerpublicclassAuthorizationServerConfigextendsAuthorizationServerConfigurerAdapter{ResourceprivateDataSourcedataSource;对auth2提供的接口做访问规则配置和添加自定义过滤器Overridepublicvoidconfigure(AuthorizationServerSecurityConfigurersecurity)throwsException{security允许表单认证,对于接口oauthtoken,如果开启此配置,并且url中有clientid和clientsecret会触发ClientCredentialsTokenEndpointFilter用于校验客户端是否有权限。allowFormAuthenticationForClients()设置接口oauthchecktoken访问权限,默认denyAll(),资源服务器可以调用这个验证token,如果是jwt,资源服务器也可以自己通过密钥验证。checkTokenAccess(isAuthenticated())提供jwt的公钥接口oauthtokenkey。tokenKeyAccess(permitAll())N添加自定义的过滤器通过AbstractAuthenticationProcessingFilter断点的additionalFilter可以看到该过滤器再链中的位置。addTokenEndpointAuthenticationFilter(newMyFilterFive())。passwordEncoder();}接入端管理配置,对应的是oauthclientdetails表,实体是BaseClientDetailsOverridepublicvoidconfigure(ClientDetailsServiceConfigurerclients)throwsException{clients。inMemory()client1是客户端授权方式。withClient(client1)接入端id。secret(newBCryptPasswordEncoder()。encode(123456))接入端密钥。resourceIds(DEMORESOURCEID)资源id。authorizedGrantTypes(clientcredentials,refreshtoken)授权方式。scopes(select)访问域。authorities(client);权限clients。jdbc(dataSource);clients。withClientDetails(newJdbcClientDetailsService(dataSource));}SpringSecurityOAuth2默认提供以下端口oauthauthorize:授权端口oauthtoken:令牌端口oauthconfirmaccess:用户确认授权提交端口oautherror:授权服务错误信息端口oauthchecktoken:用于资源服务器访问的令牌解析端口oauthtokenkey:提供公有秘钥端口,如果使用的是JWT令牌的话pathMapping可以映射成其他地址Overridepublicvoidconfigure(AuthorizationServerEndpointsConfigurerendpoints)throwsException{endpoints。pathMapping(oauthtoken,clonelitoken)。allowedTokenEndpointRequestMethods(HttpMethod。GET,HttpMethod。POST)允许的请求方式tokenStore默认内存存储,重启服务token就会失效。tokenStore(newInMemoryTokenStore())。reuseRefreshTokens(true)。tokenEnhancer()。accessTokenConverter(jwtAccessTokenConverter())用于配置密码式的授权方式,如果不设置,密码模式请求token是,token为null,TokenEndpoint会提示不支持password授权模式,这里配置就是parentAuthenticationManager。authenticationManager(authenticationManager())。tokenGranter(newTokenGranter(){OverridepublicOAuth2AccessTokengrant(Strings,TokenRequesttokenRequest){returnnull;}});}} 在上面AuthorizationServerSecurityConfigurer的配置中,可以看到我们设置了oauthchecktoken接口。checkTokenAccess(isAuthenticated()),即需要授权才能访问该接口。 而权限的配置就在下面AuthorizationServerSecurityConfiguration的方法中,同时有包括setSharedObject的ClientDetailsService,因此授权服务和资源服务不是同一个服务,也就没有EnableAuthorizationServer注解,就不会执行setSharedObject的操作,这就是为什么getSharedObject(ClientDetailsService。class)可能是null的原因。ConfigurationOrder(0)ClientDetailsServiceConfiguration提供clientDetailsService的beanImport({ClientDetailsServiceConfiguration。class,AuthorizationServerEndpointsConfiguration。class})publicclassAuthorizationServerSecurityConfigurationextendsWebSecurityConfigurerAdapter{。。。。。。AutowiredprivateClientDetailsServiceclientDetailsService;。。。。。。Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{AuthorizationServerSecurityConfigurerconfigurernewAuthorizationServerSecurityConfigurer();FrameworkEndpointHandlerMappinghandlerMappingendpoints。oauth2EndpointHandlerMapping();http。setSharedObject(FrameworkEndpointHandlerMapping。class,handlerMapping);configure(configurer);http。apply(configurer);StringtokenEndpointPathhandlerMapping。getServletPath(oauthtoken);StringtokenKeyPathhandlerMapping。getServletPath(oauthtokenkey);StringcheckTokenPathhandlerMapping。getServletPath(oauthchecktoken);if(!endpoints。getEndpointsConfigurer()。isUserDetailsServiceOverride()){UserDetailsServiceuserDetailsServicehttp。getSharedObject(UserDetailsService。class);endpoints。getEndpointsConfigurer()。userDetailsService(userDetailsService);}formatter:offhttp。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:onsetSharedObjectclientDetailsServicehttp。setSharedObject(ClientDetailsService。class,clientDetailsService);}。。。。。。}没有EnableAuthorizationServer注解,就不会执行AuthorizationServerSecurityConfiguration的方法Target(ElementType。TYPE)Retention(RetentionPolicy。RUNTIME)DocumentedImport({AuthorizationServerEndpointsConfiguration。class,AuthorizationServerSecurityConfiguration。class})publicinterfaceEnableAuthorizationServer{}5。2RemoteTokenServices 见名知意,这个需要调用其他服务验证accesstoken,显然能提供这个服务的应该就是授权服务了。授权服务提供了CheckTokenEndpoint用于验证accesstoken的接口如下,而其底层实现也是resourceServerTokenServices。oauthchecktoken 调用授权服务器的oauthchecktoken接口,我们上面提到需要授权服务器设置访问权限,这里用的是客户端模式。OverridepublicOAuth2AuthenticationloadAuthentication(StringaccessToken)throwsAuthenticationException,InvalidTokenException{MultiValueMapString,StringformDatanewLinkedMultiValueMapString,String();formData。add(tokenName,accessToken);HttpHeadersheadersnewHttpHeaders();N设置请求头,客户端凭证,访问授权服务器需要认证headers。set(Authorization,getAuthorizationHeader(clientId,clientSecret));N设置请求地址,并发送请求获取验证结果MapString,ObjectmappostForMap(checkTokenEndpointUrl,formData,headers);if(map。containsKey(error)){logger。debug(checktokenreturnederror:map。get(error));thrownewInvalidTokenException(accessToken);}Assert。state(map。containsKey(clientid),Clientidmustbepresentinresponsefromauthserver);N构造OAuth2Authentication对象returntokenConverter。extractAuthentication(map);}5。3LocalTokenServices(自定义) 当然如果上面两种校验accesstoken的实现不能满足项目需求,也可以自定义自己的ResourceServerTokenServices实现类,重写验证accesstoken的逻辑。 综上,整个令牌验证的流程就是ResourceServerSecurityConfigurer指定了OAuth2AuthenticationProcessingFilter过滤器,过滤器调用AuthenticationManager,AuthenticationManager又调用ResourceServerTokenServices实现令牌验证,其中具体实现我们可以通过资源服务配置进行修改。 到这里,令牌验证通过,Authentication的信息就会放到线程变量SecurityContext中,然后过滤器就会放行请求。 不过验证令牌后得到的权限信息还没有用到。publicinterfaceAuthenticationextendsPrincipal,Serializable{Collectionlt;?extendsGrantedAuthoritygetAuthorities();。。。。。。} 这就要提到EnableGlobalMethodSecurity系列的注解了,我们以后再说。