Spring Security Oauth2

oauth2理解
oauth2理解
oauth2参考

oauth2流程

基本概念

  • Resource Owner: 资源所有者,一般指用户。

  • Resource Server: 资源服务器。

  • Client:客户端 、第三方应用,一般指需要赋权的应用。

  • Authorization Serve:授权服务器。

  • client-id、secret:通过这个,授权服务器能够认识第三方应用

  • code:由授权服务器生成,然后发送给客户端,然后客户端通过code去请求access_token

  • access_token:由授权服务器返回给客户端,然后客户端通过这个去访问资源服务器的资源

简单梳理:用户(RO)在资源服务器(RS)上拥有资源,在访问第三方应用的时候,想使用资源服务器上的资源,这时候需要通过授权服务器(AS)赋予用户访问RS上的部分数据。

image01.png

image02.png

image03.png

上图中就是一个标准的OAuth2授权流程,先理一下这角色的对应关系。
我打开开发者头条,点击登录的时候选择了QQ登录这种方式,点击QQ登录跳转到一个提示界面,询问我是否继续登录,我点击确认之后重新跳转到开发者头条,这时候我在开发者头条的登陆状态是:已登录。

这个时候,通过chrome调试工具,观察一下network中请求的链接

点击QQ登录的时候请求变化如下

https://toutiao.io/auth/qq

https://graph.qq.com/oauth2.0/authorize?client_id=101185770&redirect_uri=https%3A%2F%2Ftoutiao.io%2Fauth%2Fqq%2Fcallback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942
上面的路径经过解码之后的寄过如下
https://graph.qq.com/oauth2.0/authorize?client_id=101185770&redirect_uri=https://toutiao.io/auth/qq/callback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942

https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101185770&redirect_uri=https%3A%2F%2Ftoutiao.io%2Fauth%2Fqq%2Fcallback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942
上面的路径经过解码之后的寄过如下,这个其实就是对用图二中的那个授权界面。
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101185770&redirect_uri=https://toutiao.io/auth/qq/callback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942

在授权界面点击确定登录时请求变化如下

https://graph.qq.com/oauth2.0/authorize ,下图是它的请求参数

image.png

https://toutiao.io/auth/qq/callback?code=258FA606DF3FD476174341A8AAF24CC6&state=a1a86a5f569d9fe3a9a89fb7acb66831ec6e36522453d9b9

image.png

之后,在头条服务器对应的 /auth/qq/callback 方法内,会使用 哪个code向qq授权服务器请求access_token

这时候各个角色对应的关系如下:

  • 我:RS资源所有者
  • 开发者头条:Client第三方应用
  • QQ资源服务器:资源服务器
  • QQ授权服务器:授权服务器

我自己理解的流程

oauth2

XML配置

主要介绍oauth2在项目中的XML配置方式。
先看一下依赖,这里面的依赖已经包括了 spring-security 对 cas、oauth2、ldap 支持所需要的依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.0.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.4.RELEASE</version>
</dependency>

配置文件

<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
    http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd
    http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd">

    <!-- 配置参考 http://blog.csdn.net/monkeyking1987/article/details/16828059 -->
    <!-- 配置参考 https://github.com/spring-projects/greenhouse/blob/master/src/main/java/com/springsource/greenhouse/config/security.xml -->

    <!-- 授权服务器的对外接口,没有验证通过的用户将看到 确认授权界面,确认授权界面 -->
    <http pattern="/oauth/authorize" create-session="ifRequired" use-expressions="false"
          authentication-manager-ref="oauth2AuthenticationManager">
        <csrf disabled="true"/>
        <intercept-url pattern="/oauth/authorize" method="GET" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <form-login login-page='/login?oauth'/>
    </http>


    <!-- client端 申请 access_token 时候的认证和权限设置,授权服务器提供 -->
    <http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="oauth2AuthenticationManager"
          entry-point-ref="oauth2AuthenticationEntryPoint">
        <csrf disabled="true"/>
        <intercept-url pattern="/oauth/token" access="isFullyAuthenticated()"/>
        <anonymous enabled="false"/>
        <http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
        <custom-filter ref="requestContextFilter" before="PRE_AUTH_FILTER"/>
        <custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>
        <access-denied-handler ref="oauth2AccessDeniedHandler"/>
    </http>

    <!--对外API Spring Security 配置-->
    <!--
    * pattern  URL匹配模式
    * create-session 是否创建后台session
    * entry-point-ref 入口点,访问过程中如果发生异常,会返回到入口点
    * access-decision-manager-ref 认证管理器
    -->
    <http pattern="/api/**"
          create-session="stateless"
          use-expressions="true"
          entry-point-ref="oauth2AuthenticationEntryPoint"
          access-decision-manager-ref="oauth2AccessDecisionManager">
        <csrf disabled="true"/>
        <!--拒绝匿名访问-->
        <anonymous enabled="true"/>
        <!-- 对外公开访问的API -->
        <intercept-url pattern="/api/public/**" access="permitAll"/>
        <!-- 设置访问权限控制 -->
        <intercept-url pattern="/api/**" access="isFullyAuthenticated()"/>
        <!--<intercept-url pattern="/api/**" access="hasAnyRole('ROLE_UNITY','SCOPE_READ')"/>-->
        <!-- oauth 资源过滤器,与resource server配置对应 -->
        <custom-filter ref="apiResourceServer" before="PRE_AUTH_FILTER"/>
        <!-- 访问次数过滤器 -->
        <!--<custom-filter ref="accessLimitFilter" after="FILTER_SECURITY_INTERCEPTOR"/>-->
        <!-- 访问拒绝处理器 -->
        <access-denied-handler ref="oauth2AccessDeniedHandler"/>
    </http>

    <!--内部API Spring Security 配置-->
    <http pattern="/i/api/**"
          create-session="stateless"
          use-expressions="true"
          entry-point-ref="oauth2AuthenticationEntryPoint"
          access-decision-manager-ref="oauth2AccessDecisionManager">
        <csrf disabled="true"/>
        <!--拒绝匿名访问-->
        <anonymous enabled="false"/>
        <!-- 设置访问权限控制 -->
        <!--<intercept-url pattern="/i/api/**" access="hasAnyRole('ROLE_UNITY','SCOPE_READ')"/>-->
        <!-- oauth 资源过滤器,与resource server配置对应 -->
        <custom-filter ref="iapiResourceServer" before="PRE_AUTH_FILTER"/>
        <!-- 访问拒绝处理器 -->
        <access-denied-handler ref="oauth2AccessDeniedHandler"/>
    </http>

    <!--透传API Spring Security 配置-->
    <http pattern="/r/api/**"
          create-session="stateless"
          use-expressions="true"
          entry-point-ref="oauth2AuthenticationEntryPoint"
          access-decision-manager-ref="oauth2AccessDecisionManager">
        <csrf disabled="true"/>
        <!--拒绝匿名访问-->
        <anonymous enabled="true"/>
        <!-- 设置访问权限控制 -->
        <intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
                       method="GET"/>
        <intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
                       method="POST"/>
        <intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
                       method="PUT"/>
        <intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
                       method="DELETE"/>
        <!-- oauth 资源过滤器,与resource server配置对应 -->
        <custom-filter ref="apiResourceServer" before="PRE_AUTH_FILTER"/>
        <!-- 访问拒绝处理器 -->
        <access-denied-handler ref="oauth2AccessDeniedHandler"/>
    </http>

    <!--访问限制过滤器-->
    <!--<beans:bean id="accessLimitFilter" class="com.hand.hap.security.ApiAccessLimitFilter" />-->
    <!--内部API Spring Security 配置-->
    <beans:bean id="oauth2AuthenticationEntryPoint"
                class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    </beans:bean>

    <!-- oauth2认证管理器,确定用户,角色及相应的权限 -->
    <beans:bean id="oauth2AccessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
        <!-- 投票器 -->
        <beans:constructor-arg>
            <beans:list>
                <!---->
                <beans:bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
                <beans:bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter"/>
                <beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
                <beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
                <beans:bean class="com.hand.hap.security.PermissionVoter"/>
            </beans:list>
        </beans:constructor-arg>
    </beans:bean>

    <!-- 外部API Resource -->
    <oauth2:resource-server id="apiResourceServer" resource-id="api-resource" token-services-ref="tokenServices"/>

    <!-- 内部API Resource -->
    <oauth2:resource-server id="iapiResourceServer" resource-id="iapi-resource" token-services-ref="tokenServices"/>

    <beans:bean id="oauth2AccessDeniedHandler"
                class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>

    <beans:bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
        <beans:property name="tokenStore" ref="tokenStore"/>
        <beans:property name="tokenEnhancer" ref="jwtAccessTokenConverter"/>
        <beans:property name="supportRefreshToken" value="true"/>
        <beans:property name="accessTokenValiditySeconds" value="3000"/>
        <beans:property name="clientDetailsService" ref="clientDetailsService"/>
    </beans:bean>

    <beans:bean id="tokenStore" class="com.hand.hap.security.CustomJwtTokenStore">
        <beans:constructor-arg ref="jwtAccessTokenConverter"/>
    </beans:bean>

    <!--<beans:bean id="jwtAccessTokenConverter" class="org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter">
        <beans:property name="signingKey" value="handhand"/>
    </beans:bean>-->

    <beans:bean id="jwtAccessTokenConverter" class="com.hand.hap.security.CustomJwtAccessTokenConverter">
        <beans:property name="signingKey" value="handhand"/>
    </beans:bean>

    <beans:bean id="requestContextFilter"
                class="com.hand.hap.security.AuthenticationRequestContextFilter"/>


    <!--<beans:bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.JdbcTokenStore">
        <beans:constructor-arg index="0" ref="dataSource"/>
    </beans:bean>-->


    <!--  ==============================  -->
    <!--  OAUTH 2 : AUTHORIZATION SERVER  -->
    <!--  ==============================  -->
    <oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
                                 authorization-request-manager-ref="OAuth2RequestFactory"
                                 user-approval-page="forward:/oauth_approval.html"
                                 user-approval-handler-ref="oauthUserApprovalHandler">
        <oauth2:authorization-code/>
        <oauth2:implicit/>
        <oauth2:refresh-token/>
        <oauth2:client-credentials/>
        <oauth2:password authentication-manager-ref="authenticationManager_oauth"/>
    </oauth2:authorization-server>

    <beans:bean id="clientCredentialsTokenEndpointFilter"
                class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
        <beans:property name="authenticationManager" ref="oauth2AuthenticationManager"/>
    </beans:bean>

    <authentication-manager id="oauth2AuthenticationManager">
        <authentication-provider ref="customClientAuthenticationProvider"/>
    </authentication-manager>

    <beans:bean id="customClientAuthenticationProvider"
                class="com.hand.hap.security.CustomAuthenticationProvider">
        <beans:property name="userDetailsService" ref="oauth2ClientDetailsUserService"/>
    </beans:bean>

    <beans:bean id="oauth2ClientDetailsUserService"
                class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
        <beans:constructor-arg ref="clientDetailsService"/>
    </beans:bean>

    <authentication-manager id="authenticationManager_oauth">
        <authentication-provider ref="customUserAuthenticationProvider"/>
    </authentication-manager>

    <beans:bean id="customUserAuthenticationProvider"
                class="com.hand.hap.security.CustomAuthenticationProvider">
        <beans:property name="userDetailsService" ref="customUserDetailsService"/>
        <beans:property name="passwordEncoder" ref="passwordManager"/>
    </beans:bean>

    <beans:bean id="OAuth2RequestFactory"
                class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory">
        <beans:constructor-arg ref="clientDetailsService"/>
    </beans:bean>

    <!-- Web Application clients -->
    <beans:bean id="clientDetailsService" class="com.hand.hap.security.CustomJdbcClientDetailsService">
    </beans:bean>

    <!--<oauth2:client-details-service  id="clientDetailsService" >
         <oauth2:client client-id="client"
                       secret="secret"
                       authorities="APP"
                       authorized-grant-types="authorization_code,refresh_token"
                       resource-ids="api-resource"
                       scope="read,write,trust"/>
         <oauth2:client client-id="client2"
                       secret="secret"
                       authorities="APP"
                       authorized-grant-types="password,refresh_token"
                       resource-ids="api-resource"
                       scope="read,write,trust"/>
         <oauth2:client client-id="client3"
                       secret="secret"
                       authorities="APP"
                       authorized-grant-types="client_credentials"
                       resource-ids="api-resource"
                       scope="read,write,trust"/>
         <oauth2:client client-id="client4"
                       secret="secret"
                       authorities="APP"
                       authorized-grant-types="implicit"
                       resource-ids="api-resource"
                       scope="read,write,trust"/>
     </oauth2:client-details-service>-->

    <beans:bean id="oauthUserApprovalHandler"
                class="org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler">
        <beans:property name="tokenStore" ref="tokenStore"/>
        <beans:property name="requestFactory" ref="OAuth2RequestFactory"/>
        <beans:property name="clientDetailsService" ref="clientDetailsService"/>
    </beans:bean>


</beans:beans>

先解释一下上面注释掉的一部分内容,<oauth2:client-details-service /> 用于配置 client客户端,上面注释的那些代码,其实就是配置了4个clien,可以看出,每个client的参数都对应着上面的属性,比如 授权模式、client-id、secret等参数。
现在不需要配置这个的原因,是因为这些配置已经动态配置在数据库里面了,对应的表名:sys_oauth_client_details ,可以看看表里面的内容,和配置文件里面的内容基本一致,不过更方便管理。

image.png

解读

认证服务器对外授权接口/oauth/authorize
    <!-- 授权服务器的对外接口,没有验证通过的用户将看到 确认授权界面,确认授权界面 -->
    <http pattern="/oauth/authorize" create-session="ifRequired" use-expressions="false"
          authentication-manager-ref="oauth2AuthenticationManager">
        <csrf disabled="true"/>
        <intercept-url pattern="/oauth/authorize" method="GET" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <form-login login-page='/login?oauth'/>
    </http>

pattern="/oauth/authorize" create-session="ifRequired"这个地址是有特殊用处的,要不然这里也不会单独配置。举一个例子来说明:当你在开发者头条点击QQ登录的时候,会跳转到QQ授权界面,这时候的处理流程: 用处 -->开发者头条后台-->QQ认证服务器对外的授权接口。这里的/oauth/authorize就相当于是QQ认证服务器对外的授权接口。

create-session="ifRequired" 表示为每个登录成功的用户会新建一个Session。

create-session="stateless" 表示对登录成功的用户不会创建Session了,你的application也不会允许新建session,而且Spring Security会跳过所有的 filter chain:HttpSessionSecurityContextRepository, SessionManagementFilter, RequestCacheFilter。也就是说每个请求都是无状态的独立的,需要被再次认证re-authentication。开销显然是增大了,因为每次请求都必须在服务器端重新认证并建立用户角色和权限的上下文。

oauth2AuthenticationManager 在 spring-security基本原理章节已经介绍过了,权限管理器,负责权限控制。对应的还有资源管理器,负责资源控制,在这个配置文件中,资源管理器对应 oauth2AccessDecisionManager

    <authentication-manager id="oauth2AuthenticationManager">
        <authentication-provider ref="customClientAuthenticationProvider"/>
    </authentication-manager>

    <beans:bean id="customClientAuthenticationProvider"
                class="com.hand.hap.security.CustomAuthenticationProvider">
        <beans:property name="userDetailsService" ref="oauth2ClientDetailsUserService"/>
    </beans:bean>

    <beans:bean id="oauth2ClientDetailsUserService"
                class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
        <beans:constructor-arg ref="clientDetailsService"/>
    </beans:bean>

    <!-- Web Application clients -->
    <beans:bean id="clientDetailsService" class="com.hand.hap.security.CustomJdbcClientDetailsService">

以上就是oauth2AuthenticationManager的配置,和之前讲的流程一样:manager-->provider-->userService,这些其实是属于 spring-security的概念。这里表示 访问 /oauth/authorize 会对应 oauth2AuthenticationManager 里面权限控制逻辑。其实不难发现,自己写的逻辑就是在 com.hand.hap.security.CustomJdbcClientDetailsService 这个类中,其对应源码如下:

package com.hand.hap.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hand.hap.security.oauth.dto.Oauth2ClientDetails;
import com.hand.hap.security.oauth.service.IOauth2ClientDetailsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.util.StringUtils;

import java.util.Map;

/**
 * @author Qixiangyu
 * @author njq.niu@hand-china.com
 * @date 2017/4/18.
 */
public class CustomJdbcClientDetailsService implements ClientDetailsService {

    private static final Logger logger = LoggerFactory.getLogger(CustomJdbcClientDetailsService.class);

    /**
     * 警告: 仅用于内部登录生成 AccessToken 使用!
     */
    public static final String DEFAULT_CLIENT_ID = "HAP_INNER_CLIENT_ID";
    public static final String DEFAULT_CLIENT_SECRET = "2d6be1aa-5e3b-4b03-b8c9-d553ea276a05";

    private static final String DEFAULT_SCOPE = "default";
    private static final String DEFAULT_RESOURCE_ID = "api-resource";
    private static final String DEFAULT_AUTHORIZED_GRANT_TYPES ="password,refresh_token";
    private static final String DEFAULT_AUTHORITIES ="authorities";

    private static final ThreadLocal<ClientDetails> CLIENT_DETAILS = new ThreadLocal<>();

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private IOauth2ClientDetailsService clientDetailsService;

    public ClientDetails loadInnerClient(){
        BaseClientDetails baseClientDetails = new BaseClientDetails(DEFAULT_CLIENT_ID,DEFAULT_RESOURCE_ID,DEFAULT_SCOPE,DEFAULT_AUTHORIZED_GRANT_TYPES,DEFAULT_AUTHORITIES, null);
        baseClientDetails.setClientSecret(DEFAULT_CLIENT_SECRET);
        // 默认设置1天
        baseClientDetails.setAccessTokenValiditySeconds(86400);
        return baseClientDetails;
    }

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        if(DEFAULT_CLIENT_ID.equalsIgnoreCase(clientId)){
            return loadInnerClient();
        }
        if(CLIENT_DETAILS.get() != null){
            return CLIENT_DETAILS.get();
        }

        Oauth2ClientDetails oauth2ClientDetails = clientDetailsService.selectByClientId(clientId);
        if (oauth2ClientDetails == null) {
            throw new NoSuchClientException("No client with requested id: " + clientId);
        }
        BaseClientDetails details = new BaseClientDetails(oauth2ClientDetails.getClientId(),
                oauth2ClientDetails.getResourceIds(), oauth2ClientDetails.getScope(),
                oauth2ClientDetails.getAuthorizedGrantTypes(), oauth2ClientDetails.getAuthorities(),
                oauth2ClientDetails.getRedirectUri());
        details.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(oauth2ClientDetails.getAutoApprove()));
        details.setClientSecret(oauth2ClientDetails.getClientSecret());
        if(oauth2ClientDetails.getRefreshTokenValidity() != null) {
            details.setRefreshTokenValiditySeconds(oauth2ClientDetails.getRefreshTokenValidity().intValue());
        }
        if(oauth2ClientDetails.getAccessTokenValidity() != null) {
            details.setAccessTokenValiditySeconds(oauth2ClientDetails.getAccessTokenValidity().intValue());
        }
        if (oauth2ClientDetails.getAdditionalInformation() != null) {
            try {
                Map<String, Object> additionalInformation = objectMapper.readValue(oauth2ClientDetails.getAdditionalInformation(), Map.class);
                details.setAdditionalInformation(additionalInformation);
            }
            catch (Exception e) {
                logger.warn("Could not decode JSON for additional information: " + details, e);
            }
        }
        CLIENT_DETAILS.set(details);
        return details;
    }

    public static void clearInfo(){
        if(CLIENT_DETAILS != null) {
            CLIENT_DETAILS.remove();
        }
    }
}

主要就是 loadClientByClientId 方法,这里 从数据库获取client信息,然后 封装成 BaseClientDetails 对象,最后交由spring -security管理。

认证服务器对获取token_access接口 /oauth/authorize
    <!-- client端 申请 access_token 时候的认证和权限设置,授权服务器提供 -->
    <http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="oauth2AuthenticationManager"
          entry-point-ref="oauth2AuthenticationEntryPoint">
        <csrf disabled="true"/>
        <intercept-url pattern="/oauth/token" access="isFullyAuthenticated()"/>
        <anonymous enabled="false"/>
        <http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
        <custom-filter ref="requestContextFilter" before="PRE_AUTH_FILTER"/>
        <custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>
        <access-denied-handler ref="oauth2AccessDeniedHandler"/>
    </http>

tpattern="/oauth/token" create-session="stateless" /oauth/token 是用来 client 获取access_token的,注意其中的 <intercept-url pattern="/oauth/token" access="isFullyAuthenticated()"/>,表示访问 /oauth/token 接口需要 isFullyAuthenticatd()的权限。还有 entry-point-ref="oauth2AuthenticationEntryPoint" ,表示入口点,访问过程中如果发生异常,会返回到入口点。

接口访问权限
    <!--对外API Spring Security 配置-->
    <!--
    * pattern  URL匹配模式
    * create-session 是否创建后台session
    * entry-point-ref 入口点,访问过程中如果发生异常,会返回到入口点
    * access-decision-manager-ref 认证管理器
    -->
    <http pattern="/api/**"
          create-session="stateless"
          use-expressions="true"
          entry-point-ref="oauth2AuthenticationEntryPoint"
          access-decision-manager-ref="oauth2AccessDecisionManager">
        <csrf disabled="true"/>
        <!--拒绝匿名访问-->
        <anonymous enabled="true"/>
        <!-- 对外公开访问的API -->
        <intercept-url pattern="/api/public/**" access="permitAll"/>
        <!-- 设置访问权限控制 -->
        <intercept-url pattern="/api/**" access="isFullyAuthenticated()"/>
        <!--<intercept-url pattern="/api/**" access="hasAnyRole('ROLE_UNITY','SCOPE_READ')"/>-->
        <!-- oauth 资源过滤器,与resource server配置对应 -->
        <custom-filter ref="apiResourceServer" before="PRE_AUTH_FILTER"/>
        <!-- 访问次数过滤器 -->
        <!--<custom-filter ref="accessLimitFilter" after="FILTER_SECURITY_INTERCEPTOR"/>-->
        <!-- 访问拒绝处理器 -->
        <access-denied-handler ref="oauth2AccessDeniedHandler"/>
    </http>

以 /api/** 开头的url,需要通过资源控制器oauth2AccessDecisionManager的认证,要具有 isFullyAuthenticated()的权限;以/api/public/**表示所有人都可以访问。配置文件中其它的 那些 内部API也是类似。

授权服务器

    <!--  ==============================  -->
    <!--  OAUTH 2 : AUTHORIZATION SERVER  -->
    <!--  ==============================  -->
    <oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
                                 authorization-request-manager-ref="OAuth2RequestFactory"
                                 user-approval-page="forward:/oauth_approval.html"
                                 user-approval-handler-ref="oauthUserApprovalHandler">
        <oauth2:authorization-code/>
        <oauth2:implicit/>
        <oauth2:refresh-token/>
        <oauth2:client-credentials/>
        <oauth2:password authentication-manager-ref="authenticationManager_oauth"/>
    </oauth2:authorization-server>

请求access_token原理

本章节主要介绍的是和 "请求access_token" 相关,即获取token的流程

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 是一个抽象类,在 spring security中,位于过滤链中的第 5 个。实际运行的时候,根据不同的场景,调用不同的实现类,其主要实现类如下

image.png

可能会好奇,spring securit 怎么就知道在什么时候调用哪个子类,它是怎么区分不同的场景的?追源码到 FilterChainProxy 类下,在其中的doFilterInternal方法中:

    private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);

        List<Filter> filters = getFilters(fwRequest);

        if (filters == null || filters.size() == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                        + (filters == null ? " has no matching filters"
                                : " has an empty filter list"));
            }

            fwRequest.reset();

            chain.doFilter(fwRequest, fwResponse);

            return;
        }

        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }

注意其中的 getFilters(fwRequest); 方法:

image.png

根据不同的请求路径,获取到不同的过滤器数组,这部分是在项目启动时初始化进去的。

可以看出,有关于 token的请求,都会被 ClientCredentialsTokenEndpointFilter 处理。

然后 通过这个进行身份认证

return this.getAuthenticationManager().authenticate(authRequest);

ProviderManager 实现了 AuthenticationManager接口, ProviderManager 拥有 一组 AuthenticationProvider 对象 (List<AuthenticationProvider> providers),AuthenticationProvider 是一个接口,对应一系列实现。然后遍历 providers,对每个 AuthenticationProvider 进行身份认证。默认是有两个 AuthenticationProvider 的, 如图:

image.png

进行校验
DaoAuthenticationProvider 继承了 AbstractUserDetailsAuthenticationProvider, 所以很多是功能都是在 AbstractUserDetailsAuthenticationProvider 抽象类中是实现的。

对于这个步骤,也是根据不同的场景调用不用的 UserDetailsService 实现类查询用户。

image.png

获取token的时候,调用的时候,实现类是 ClientDetailsUserDetailsService ,在 loadUserByUsername 方法里面 调用了 ClientDetailsService 实现类的 的 loadClientByClientId 方法, 就是根据client_id找到客户端信息


image.png

返回的用户信息如下


image.png

获取用户信息之后,紧接着对 用户信息进行校验,就是检查一下用户信息是否被锁住,是否失效啥的。additionalAuthenticationChecks 是对 密码进行校验
image.png

然后又有个校验,校验密码是否过期
image.png

都校验通过之后,然后返回一个 UsernamePasswordAuthenticationToken 对象


image.png

image.png

校验完成之后,将一些信息拷贝到 校验后的结果中,这里是将 authentication 中的 details 放到 result 中
image.png

至此一个 AuthenticationProvider 的校验已经完成,ProviderManager 中的 private List<AuthenticationProvider> providers 属性中 有多少个 AuthenticationProvider ,就重复多少次 这样的校验。
当所有的 AuthenticationProvider 都执行完之后,然后就抹去密码

image.png

然后就根据实际情况,调用验证成功的逻辑。
然后将 result 返回到 AbstractAuthenticationProcessingFilter中,如果没问题,就调用session 策略进行处理吧


image.png

不过这个处理不太清楚是什么场景下用到

image.png

至此,ClientCredentialsTokenEndpointFilter (也就是AbstractAuthenticationProcessingFilter )处理完毕,接着执行下面的过滤器。

所有过滤器执行完毕之后, 会请求 TokenEndpoint 类中的 方法,这里会校验 scope等一些信息。

使用access_token原理

主要介绍的是 和 "使用access_token" 相关,即通过 access_token 请求资源的时候,内部的执行流程。

ResourceServerSecurityConfigurer

在配置 资源服务器的时候, 使用了一个 @EnableResourceServer 注解,如下:

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.resourceId(API_RESOURCE_ID).stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)

                    // 配置order访问控制,必须认证过后才可以访问
                    .and().anonymous()
                    .and().requestMatchers().anyRequest()
                    .and().authorizeRequests().antMatchers("/order/**").authenticated();
        }
    }

可以看出,和资源相关的配置类:ResourceServerSecurityConfigurer,其主要配置如下:

    @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);
        }
        if (authenticationDetailsSource != null) {
            resourcesServerFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
        }
        resourcesServerFilter = postProcess(resourcesServerFilter);
        resourcesServerFilter.setStateless(stateless);

        // @formatter:off
        http
            .authorizeRequests().expressionHandler(expressionHandler)
        .and()
            .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);
        // @formatter:on
    }


    private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
        OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
        if (authenticationManager != null) {
            if (authenticationManager instanceof OAuth2AuthenticationManager) {
                oauthAuthenticationManager = (OAuth2AuthenticationManager) authenticationManager;
            }
            else {
                return authenticationManager;
            }
        }
        oauthAuthenticationManager.setResourceId(resourceId);
        oauthAuthenticationManager.setTokenServices(resourceTokenServices(http));
        oauthAuthenticationManager.setClientDetailsService(clientDetails());
        return oauthAuthenticationManager;
    }

该配置主要功能:

  • new OAuth2AuthenticationManager()
  • new OAuth2AuthenticationProcessingFilter()
  • setAuthenticationManager(oauthAuthenticationManager)
    即 创建OAuth2AuthenticationManager, 创建 OAuth2AuthenticationProcessingFilter, 然后将 OAuth2AuthenticationProcessingFilter 和 OAuth2AuthenticationManager相关联。它并没有将OAuth2AuthenticationManager添加到spring的容器中,不然可能会影响spring security的普通认证流程(非oauth2请求),只有被OAuth2AuthenticationProcessingFilter拦截到的oauth2相关请求才被特殊的身份认证器处理。

OAuth2AuthenticationProcessingFilter

当请求 http://localhost:7070/order/1?access_token=e50acb5a-c9b0-4e39-a8f6-221264ea25d7 的时候,需要进入 OAuth2AuthenticationProcessingFilter 过滤器,但是这里有一个前提,需要在配置文件中设置 OAuth2AuthenticationProcessingFilter 的 Order。

security:
  oauth2:
    resource:
      filter-order: 3

如果不设置Order OAuth2AuthenticationProcessingFilter 不会生效,生效的是 UsernamePasswordAuthenticationFilter,即会走标准的 spring-security认证流程。
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;

    ......

    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);
    }
}

当我们通过一个access_token请求资源的时候,可以看看过滤器链

image.png

首先提取 access_token

Authentication authentication = tokenExtractor.extract(request);

这一部分功能是通过 TokenExtractor 接口的实现类 BearerTokenExtractor 实现的。BearerTokenExtractor的核心代码如下

public Authentication extract(HttpServletRequest request) {
        String tokenValue = this.extractToken(request);
        if (tokenValue != null) {
            PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
            return authentication;
        } else {
            return null;
        }
    }

    protected String extractToken(HttpServletRequest request) {
        String token = this.extractHeaderToken(request);
        if (token == null) {
            logger.debug("Token not found in headers. Trying request parameters.");
            token = request.getParameter("access_token");
            if (token == null) {
                logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
            } else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer");
            }
        }

        return token;
    }

    protected String extractHeaderToken(HttpServletRequest request) {
        Enumeration headers = request.getHeaders("Authorization");

        String value;
        do {
            if (!headers.hasMoreElements()) {
                return null;
            }

            value = (String)headers.nextElement();
        } while(!value.toLowerCase().startsWith("Bearer".toLowerCase()));

        String authHeaderValue = value.substring("Bearer".length()).trim();
        request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim());
        int commaIndex = authHeaderValue.indexOf(44);
        if (commaIndex > 0) {
            authHeaderValue = authHeaderValue.substring(0, commaIndex);
        }

        return authHeaderValue;
    }

代码逻辑很简单,先是从 请求头中的 Authorization 获取 access_token ,如果没有找到,再从请求参数中获取 access_token:request.getParameter("access_token")。获取access_token之后再封装成一个 PreAuthenticatedAuthenticationToken 对象返回。

获取token之后,再校验身份

image.png

这里的authenticationManager, 不再是之前的ProvideManager,而是 OAuth2AuthenticationManager。

image.png

其核心代码如下:

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;

    }

首先根据token获取身份信息。

OAuth2Authentication auth = tokenServices.loadAuthentication(token);

这里的tokenServices是 对应 private ResourceServerTokenServices tokenServices;
其默认实现类DefaultTokenServices

image.png

对应的方法如下


image.png

先从tokenStore里面获取获取client_id,然后 clientDetailsService 通过client_id 获取 身份信息
clientDetailsService.loadClientByClientId(clientId);

获取身份信息之后,再进行身份校验

checkClientDetails(auth);

    private void checkClientDetails(OAuth2Authentication auth) {
        if (clientDetailsService != null) {
            ClientDetails client;
            try {
                client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
            }
            catch (ClientRegistrationException e) {
                throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
            }
            Set<String> allowed = client.getScope();
            for (String scope : auth.getOAuth2Request().getScope()) {
                if (!allowed.contains(scope)) {
                    throw new OAuth2AccessDeniedException(
                            "Invalid token contains disallowed scope (" + scope + ") for this client");
                }
            }
        }
    }

主要就是校验一下scope是否匹配,校验通过后,设置身份为已经校验成功,然后返回已经校验通过的身份信息。

token存储

主要介绍token的存储

TokenStore

spring-security-oauth2中access_token存储主要交由 TokenStore 接口管理,其实现类如下

image.png

主要的常用方法:读取、存储等

RedisTokenStore

spring:
    datasource:
      url: jdbc:mysql://localhost:3306/vue?characterEncoding=utf8&autoReconnect=true&useSSL=false
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver

    redis:
      host: 127.0.0.1
      database: 15

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    RedisConnectionFactory redisConnectionFactory;

......
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
            endpoints
                    .tokenStore(new RedisTokenStore(redisConnectionFactory))
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService)
                    // 允许 GET、POST 请求获取 token,即访问端点:oauth/token
                    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        }
}

JwtTokenStore

当我们使用JWT的时候,需要使用JwtTokenStore,同时需要再pom.xml文件中添加依赖

        <!-- 使用 JwtTokenStore 方式存儲令牌 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>

JwtTokenStore需要使用 JwtAccessTokenConverter。

JwtAccessTokenConverter

JwtAccessTokenConverter用于将令牌数据存储在令牌中,它有两个实现类:

  • JwtAccessTokenConverter:用于令牌 JWT 编码与解码
  • DefaultAccessTokenConverter:AccessTokenConverter 的默认实现。官方的解释如下:

Helper that translates between JWT encoded token values and OAuth authentication
information (in both directions). Also acts as a {@link TokenEnhancer} when tokens are
granted.

TokenEnhancer

可用于自定义令牌策略,在令牌被 AuthorizationServerTokenServices 的实现存储之前增强令牌的策略,它有两个实现类:

  • JwtAccessTokenConverter:用于令牌 JWT 编码与解码。
  • TokenEnhancerChain:一个令牌链,可以存放多个令牌,并循环的遍历令牌并将结果传递给下一个令牌。

OAuth2AccessToken

OAuth2 Token(令牌)实体类,包含了令牌、类型(Bearer)、失效时间等。

最后的配置如下:

    @Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

        @Autowired
        @Qualifier("authenticationManagerBean")
        AuthenticationManager authenticationManager;

//        @Autowired
//        RedisConnectionFactory redisConnectionFactory;

        @Autowired
        PasswordEncoder passwordEncoder;

        @Autowired
        UserDetailsService userDetailsService;

         @Override
        public void configure(ClientDetailsServiceConfigurer clientConfigure) throws Exception {
           clientConfigure.withClientDetails(new CustomClientDetailsService());
        /**
         * 使用jwt方式存储token
         *
         * @return
         */
        @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter());
        }


        /**
         * 使用 JWT 令牌
         *
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
                @Override
                public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                    String userName = authentication.getUserAuthentication().getName();

                    /** 自定义一些token属性 ***/
                    final Map<String, Object> additionalInformation = new HashMap<>(5);
                    additionalInformation.put("userName", userName);
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                    OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
                    return enhancedToken;
                }
            };

            // 用于加密的 密钥
            converter.setSigningKey("123");
            return converter;
        }

        @Bean
        @Primary
        public AuthorizationServerTokenServices defaultTokenServices() {
            final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
            defaultTokenServices.setTokenStore(tokenStore());
            return defaultTokenServices;
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

            /**
             * 使用RedisTokenStore 存储 access_token
             */
//            TokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);


            /**
             * JWT方式存储token
             */
            TokenStore tokenStore = tokenStore();
            endpoints.tokenStore(tokenStore)
                    .tokenServices(defaultTokenServices())
                    .accessTokenConverter(jwtAccessTokenConverter())
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService)
                    // 允许 GET、POST 请求获取 token,即访问端点:oauth/token
                    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        }


        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
            // 允许表单认证
            oauthServer.realm(API_RESOURCE_ID).allowFormAuthenticationForClients();

//            oauthServer.realm(API_RESOURCE_ID)
//                    //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
//                    .tokenKeyAccess("permitAll()")
//                    //url:/oauth/check_token allow check token
//                    .checkTokenAccess("isAuthenticated()")
//                    .checkTokenAccess("hasAuthority('ME')")
//
//                    //允许表单认证
//                    .allowFormAuthenticationForClients();
        }

    }

JWT可以添加一些其它用户信息,比如上面就添加了一个 userName 属性,在浏览器上访问token,其返回结果如下


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下: 以时间复杂度为O...
    高广超阅读 12,815评论 8 167
  • android拍照、相册选取的方法,权限获取用的RxPermissions。 拍照 相册
    白色相簿阅读 1,059评论 0 13
  • 说到后厨管理,不同的厨师长有不同的管理理念,有的主张人性管理,有的主张狼性管理。但不管是哪一种管理理念,都有其好的...
    琥珀工坊阅读 661评论 0 0