Oauth2源码分析(下)

二、密码模式源码

2.1 概述

访问/oauth/token会经过拦截器的顺序ClientCredentialsTokenEndpointFilterBasicAuthenticationFilterClientCredentialsTokenEndpointFilter从request parameters中抽取client信息(username,password,grant_type,client_id,client_secret)BasicAuthenticationFilter从header Authorization Basic XXXX中抽取client信息(client_id和client_secret)

流程:

TokenRequest包含了基本信息clientId,scope,requestParameters,grantType等。根据tokenRequest获取OAuth2Request,初始化获得OAuth2Authentication,再去数据库里找oauth2accesstoken,如果有则直接返回,如果没有则创建新的oauth2accesstoken,并且和OAuth2Authentication一起存入数据库中。

2.2 源码

摘要:

  • 四大角色:ResouceServer AuthorizationServer client user
  • OAuth2AccessToken OAuth2Authentiaction
  • OAuth2Request TokenRequest AuthorizationRequest
  • TokenGranter TokenStore TokenExtractor DefaultTokenServices RemoteTokenServices
  • ResourceServerConfigurerAdapter AuthorizationServerConfigurerAdapter
  • TokenEndPoint(/oauth/token) AuthorizationEndPoint(/oauth/authorize) CheckTokenEndpoint(/oauth/check_token)

TokenEndpoint类中定义了/oauth/token接口

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {

    private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();

    private Set<HttpMethod> allowedRequestMethods = new HashSet<HttpMethod>(Arrays.asList(HttpMethod.POST));

    @RequestMapping(value = "/oauth/token", method=RequestMethod.GET)
    public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        if (!allowedRequestMethods.contains(HttpMethod.GET)) {
            throw new HttpRequestMethodNotSupportedException("GET");
        }
        return postAccessToken(principal, parameters);
    }
    
    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException(
                    "There is no client authentication. Try adding an appropriate authentication filter.");
        }

        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        if (clientId != null && !clientId.equals("")) {
            // Only validate the client details if a client authenticated during this
            // request.
            if (!clientId.equals(tokenRequest.getClientId())) {
                // double check to make sure that the client ID in the token request is the same as that in the
                // authenticated client
                throw new InvalidClientException("Given client ID does not match authenticated client");
            }
        }
        if (authenticatedClient != null) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }
        if (!StringUtils.hasText(tokenRequest.getGrantType())) {
            throw new InvalidRequestException("Missing grant type");
        }
        if (tokenRequest.getGrantType().equals("implicit")) {
            throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
        }

        if (isAuthCodeRequest(parameters)) {
            // The scope was requested or determined during the authorization step
            if (!tokenRequest.getScope().isEmpty()) {
                logger.debug("Clearing scope of incoming token request");
                tokenRequest.setScope(Collections.<String> emptySet());
            }
        }

        if (isRefreshTokenRequest(parameters)) {
            // A refresh token has its own default scopes, so we should ignore any added by the factory here.
            tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
        }

        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }

        return getResponse(token);

    }
}

RemoteTokenServices :资源服务可以把传递来的access_token递交给授权服务的/oauth/check_token进行验证,而资源服务自己无需去连接数据库验证access_token,这时就用到了RemoteTokenServices。

2.2.1 Oauth的请求封装类

OAuth2Authentication和OAuth2AccessToken是一对好基友,谁要先走谁是狗!!!

2.2.1.1 OAuth2Authentication

OAuth2Authentication顾名思义是Authentication的子类,存储用户信息和客户端信息,但多了2个属性

private final OAuth2Request storedRequest; 
private final Authentication userAuthentication;

这样OAuth2Authentication可以存储2个Authentication,一个给client(必要),一个给user(只是有些授权方式需要)。除此之外同样有principle,credentials,authorities,details,authenticated等属性。

OAuth2Request 用于存储request中的Authentication信息(grantType,responseType,resouceId,clientId,scope等),这里就引出了OAuth2 中的三大request。

2.2.1.2 OAuth2AccessToken

OAuth2AccessToken是一个接口,提供安全令牌token的基本信息,不包含用户信息,仅包含一些静态属性(scope,tokenType,expires_in等)和getter方法。TokenGranter.grant()返回的值即OAuth2AccessToken

@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class)
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class)

public interface OAuth2AccessToken {

    public static String BEARER_TYPE = "Bearer";

    public static String OAUTH2_TYPE = "OAuth2";

    public static String ACCESS_TOKEN = "access_token";

    public static String TOKEN_TYPE = "token_type";

    public static String EXPIRES_IN = "expires_in";

    public static String REFRESH_TOKEN = "refresh_token";

    public static String SCOPE = "scope";


    Map<String, Object> getAdditionalInformation();

    Set<String> getScope();

    OAuth2RefreshToken getRefreshToken();

    String getTokenType();

    boolean isExpired();

    Date getExpiration();

    int getExpiresIn();

    String getValue();
    
}

TokenStore同时存储OAuth2AccessToken和OAuth2Authentication,也可根据OAuth2Authentication中的OAuth2Request信息可获取对应的OAuth2AccessToken

DefaultTokenServices有如下方法,都可以通过一个获得另一个的值

OAuth2AccessToken createAccessToken(OAuth2Authentication authentication)

OAuth2Authentication loadAuthentication(String accessTokenValue)

当tokenStore是jdbcTokenStore,表示从数据库中根据OAuth2Authentication获取OAuth2AccessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);

DefaultOAuth2AccessToken是OAuth2AccessToken的实现类,多了构造方法,setter方法和OAuth2AccessToken valueOf(Map<String,Object> tokenParams)。经过json转换后就是我们常见的access_token对象,如下所示。

{
"access_token": "1e95d081-0048-4397-a081-c76f7823fe54",
"token_type": "bearer",
"refresh_token": "7f6db28b-50dc-40a2-b381-3e356e30af2b",
"expires_in": 1799,
"scope": "read write"
}

2.2.1.3 BaseRequest及其继承类AuthorizationRequestTokenRequestOAuth2Request

BaseRequest是抽象类,有3个属性:clienId、scope和requestParameters。

abstract class BaseRequest implements Serializable {
    private String clientId;
 
    private Set<String> scope = new HashSet<String>();
 
    private Map<String, String> requestParameters = Collections
            .unmodifiableMap(new HashMap<String, String>());
 
       /**  setter,getter  */
}

其继承类有AuthorizationRequestTokenRequestOAuth2Request

  • AuthorizationRequest:向授权服务器AuthorizationEndPoint (/oauth/authorize)请求授权,AuthorizationRequest作为载体存储state,redirect_uri等参数,生命周期很短且不能长时间存储信息,可用OAuth2Request代替存储信息。

    public class AuthorizationRequest extends BaseRequest implements Serializable {
     
      // 用户同意授权传递的参数,不可改变
      private Map<String, String> approvalParameters = Collections.unmodifiableMap(new HashMap<String, String>());
     
      // 客户端发送出的状态信息,从授权服务器返回的状态应该不变才对
      private String state;
     
      // 返回类型集合
      private Set<String> responseTypes = new HashSet<String>();
     
      // resource ids  可变
      private Set<String> resourceIds = new HashSet<String>();
     
      // 授权的权限
      private Collection<? extends GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
     
      // 终端用户是否同意该request发送
      private boolean approved = false;
     
      // 重定向uri
      private String redirectUri;
     
      // 额外的属性
      private Map<String, Serializable> extensions = new HashMap<String, Serializable>();
     
     
        // 持久化到OAuth2Request
        public OAuth2Request createOAuth2Request() {
          return new OAuth2Request(getRequestParameters(), getClientId(), getAuthorities(), isApproved(), getScope(), getResourceIds(), getRedirectUri(), getResponseTypes(), getExtensions());
      }
     
        // setter,getter
    }
    
  • TokenRequest:向授权服务器TokenEndPoint(/oauth/token)发送请求获得access_token时,tokenRequest作为载体存储请求中grantType等参数。常和tokenGranter.grant(grantType,tokenRequest)结合起来使用。
    TokenRequest携带了新属性grantType,和方法createOAuth2Request(用于持久化)

    private String grantType;
    public OAuth2Request createOAuth2Request(ClientDetails client) {
          Map<String, String> requestParameters = getRequestParameters();
          HashMap<String, String> modifiable = new HashMap<String, String>(requestParameters);
          // Remove password if present to prevent leaks
          modifiable.remove("password");
          modifiable.remove("client_secret");
          // Add grant type so it can be retrieved from OAuth2Request
          modifiable.put("grant_type", grantType);
          return new OAuth2Request(modifiable, client.getClientId(), client.getAuthorities(), true, this.getScope(),
    }
    
  • OAuth2Request:用来存储TokenRequest或者AuthorizationRequest的信息,只有构造方法和getter方法,不提供setter方法。它作为OAuth2Authentication的一个属性(StoredRequest),存储request中的authentication信息(authorities,grantType,approved,responseTypes)。

    public class OAuth2Request extends BaseRequest implements Serializable {
    
      private static final long serialVersionUID = 1L;
    
      private Set<String> resourceIds = new HashSet<String>();
    
      private Collection<? extends GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
    
      private boolean approved = false;
    
      private TokenRequest refresh = null;
    
      private String redirectUri;
    
      private Set<String> responseTypes = new HashSet<String>();
    
      private Map<String, Serializable> extensions = new HashMap<String, Serializable>();
    
      public OAuth2Request(Map<String, String> requestParameters, String clientId,Collection<? extends GrantedAuthority> authorities, boolean approved, Set<String> scope,Set<String> resourceIds, String redirectUri, Set<String> responseTypes,Map<String, Serializable> extensionProperties) {
          setClientId(clientId);
          setRequestParameters(requestParameters);
          setScope(scope);
          if (resourceIds != null) {
              this.resourceIds = new HashSet<String>(resourceIds);
          }
          if (authorities != null) {
              this.authorities = new HashSet<GrantedAuthority>(authorities);
          }
          this.approved = approved;
          if (responseTypes != null) {
              this.responseTypes = new HashSet<String>(responseTypes);
          }
          this.redirectUri = redirectUri;
          if (extensionProperties != null) {
              this.extensions = extensionProperties;
          }
      }
    
      protected OAuth2Request(OAuth2Request other) {
          this(other.getRequestParameters(), other.getClientId(), other.getAuthorities(), other.isApproved(), other
                  .getScope(), other.getResourceIds(), other.getRedirectUri(), other.getResponseTypes(), other
                  .getExtensions());
      }
    
      protected OAuth2Request(String clientId) {
          setClientId(clientId);
      }
    
      protected OAuth2Request() {
          super();
      }
    
      public String getRedirectUri() {
          return redirectUri;
      }
    
      public Set<String> getResponseTypes() {
          return responseTypes;
      }
    
      public Collection<? extends GrantedAuthority> getAuthorities() {
          return authorities;
      }
    
      public boolean isApproved() {
          return approved;
      }
    
      public Set<String> getResourceIds() {
          return resourceIds;
      }
    
      public Map<String, Serializable> getExtensions() {
          return extensions;
      }
    
      public OAuth2Request createOAuth2Request(Map<String, String> parameters) {
          return new OAuth2Request(parameters, getClientId(), authorities, approved, getScope(), resourceIds,
                  redirectUri, responseTypes, extensions);
      }
    
      public OAuth2Request narrowScope(Set<String> scope) {
          OAuth2Request request = new OAuth2Request(getRequestParameters(), getClientId(), authorities, approved, scope,
                  resourceIds, redirectUri, responseTypes, extensions);
          request.refresh = this.refresh;
          return request;
      }
    
      public OAuth2Request refresh(TokenRequest tokenRequest) {
          OAuth2Request request = new OAuth2Request(getRequestParameters(), getClientId(), authorities, approved,
                  getScope(), resourceIds, redirectUri, responseTypes, extensions);
          request.refresh = tokenRequest;
          return request;
      }
    
      public boolean isRefresh() {
          return refresh != null;
      }
    
      public TokenRequest getRefreshTokenRequest() {
          return refresh;
      }
    
      public String getGrantType() {
          if (getRequestParameters().containsKey(OAuth2Utils.GRANT_TYPE)) {
              return getRequestParameters().get(OAuth2Utils.GRANT_TYPE);
          }
          if (getRequestParameters().containsKey(OAuth2Utils.RESPONSE_TYPE)) {
              String response = getRequestParameters().get(OAuth2Utils.RESPONSE_TYPE);
              if (response.contains("token")) {
                  return "implicit";
              }
          }
          return null;
      }
    

2.2.1.4 OAuth2RefreshToken

OAuth2RefreshToken是接口,只有String getValue()方法。DefaultOAuth2RefreshToken是OAuth2RefreshToken的实现类。

public interface OAuth2RefreshToken {

    /**
     * The value of the token.
     * 
     * @return The value of the token.
     */
    @JsonValue
    String getValue();

}

2.2.1.5 OAuth2RequestFactory接口

工厂类用于生成OAuth2Request、TokenRequest、AuthenticationRequest。

public interface OAuth2RequestFactory {
 
    /**
            * 从request请求参数中获取clientId,scope,state
            * clientDetailsService  loadClientByClientId(clientId) 获取clientDetails resourcesId Authorities
            * 根据以上信息生成AuthenticationRequest
            */
    AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters);
 
    /**
     *  AuthorizationRequest request  有生成OAuth2Request的方法
     *  request.createOAuth2Request()
     */
    OAuth2Request createOAuth2Request(AuthorizationRequest request);
 
 
    OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest);
 
 
    TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient);
 
 
    TokenRequest createTokenRequest(AuthorizationRequest authorizationRequest, String grantType);
 
}

2.2.2 TokenGranter、TokenStore、TokenExtractor

2.2.2.1 TokenGranter(/oauth/token)

一般在用户请求TokenEndPoints中的路径/oauth/token时,根据请求参数中的grantType,username,password,client_id,client_secret等,调用TokenGranter给用户分发OAuth2AccessToken。

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

根据grantType(password,authorization-code)和TokenRequest(requestParameters,clientId,grantType)授予人OAuth2AccessToken令牌。

public interface TokenGranter {
    OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}

回忆下TokenRequest包含了基本信息clientId,scope,requestParameters,grantType等。根据tokenRequest获取OAuth2Request,初始化获得OAuth2Authentication,再去数据库里找Oauth2AccessToken,如果有则直接返回,如果没有则创建新的Oauth2AccessToken,并且和OAuth2Authentication一起存入数据库中。

AbstractTokenGranter(授予OAuth2AccessToken)

TokenGranter抽象继承类AbstractTokenGranter,实现了grant方法。

执行顺序为根据tokenRequest====》clientId ====》clientDetails====》OAuth2Authentication(getOAuth2Authentication(client,tokenRequest))====》OAuth2AccessToken(tokenService.createAccessToken)

通过clientId获取ClientDetails,判断客户端是否有当前正在发起请求的授权模式,调用OAuth2RequestFactory的createOAuth2Request方法传入TokenRequest参数获得OAuth2Request,通过createAccessToken方法将获取的OAuth2Request作为参数获得OAuth2AccessToken。

public abstract class AbstractTokenGranter implements TokenGranter {
    
    protected final Log logger = LogFactory.getLog(getClass());

    private final AuthorizationServerTokenServices tokenServices;

    private final ClientDetailsService clientDetailsService;
    
    private final OAuth2RequestFactory requestFactory;
    
    private final String grantType;

    protected AbstractTokenGranter(AuthorizationServerTokenServices tokenServices,
            ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        this.clientDetailsService = clientDetailsService;
        this.grantType = grantType;
        this.tokenServices = tokenServices;
        this.requestFactory = requestFactory;
    }

    //通过grant方法进行认证,获取OAuth2AccessToken
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        if (!this.grantType.equals(grantType)) {
            return null;
        }
        //通过ClientDetails获取到client进行认证
        String clientId = tokenRequest.getClientId();
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        validateGrantType(grantType, client);

        if (logger.isDebugEnabled()) {
            logger.debug("Getting access token for: " + clientId);
        }

        return getAccessToken(client, tokenRequest);

    }

    //通过OAuth2Authentication获取到OAuth2AccessToken
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }
    //通过TokenRequest获取到OAuth2Request,通过OAuth2Request获取到OAuth2Authentication
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, null);
    }

    //判断客户端是否拥有指定的授权类型,没有则抛出异常
    protected void validateGrantType(String grantType, ClientDetails clientDetails) {
        Collection<String> authorizedGrantTypes = clientDetails.getAuthorizedGrantTypes();
        if (authorizedGrantTypes != null && !authorizedGrantTypes.isEmpty()
                && !authorizedGrantTypes.contains(grantType)) {
            throw new InvalidClientException("Unauthorized grant type: " + grantType);
        }
    }

    protected AuthorizationServerTokenServices getTokenServices() {
        return tokenServices;
    }
    
    protected OAuth2RequestFactory getRequestFactory() {
        return requestFactory;
    }

}

实现AbstractTokenGranter的类有5种。

21580557-7e210a361f9f6ee8.png

其中如果用password的方式进行验证,那么TokenGranter类型是ResourceOwnerPasswordTokenGranter,该类中重写了getOAuth2Authentication方法,里面调用了authenticationManager.manage()方法。

用户可自行定义granter类继承AbstractTokenGranter,重写**getOAuth2Authentication()**方法,并将该granter类添加至CompositeTokenGranter中。

public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "password";

    private final AuthenticationManager authenticationManager;

    public ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager,
            AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
            ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    //重写了父类的方法,增加authenticate方法对账号密码进行验证。
    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
        String username = parameters.get("username");
        String password = parameters.get("password");
        // Protect from downstream leaks of password
        parameters.remove("password");

        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            userAuth = authenticationManager.authenticate(userAuth);
        }
        catch (AccountStatusException ase) {
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        }
        catch (BadCredentialsException e) {
            // If the username/password are wrong the spec says we should send 400/invalid grant
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }
        
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);      
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}
CompositeTokenGranter

TokenGranter有继承类CompositeTokenGranter,包含List<TokenGranter> tokenGranters属性,grant方法是遍历tokenGranters进行逐一grant,只要有一个有返回值就返回。

public class CompositeTokenGranter implements TokenGranter {

    private final List<TokenGranter> tokenGranters;

    public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
        this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
    }
    
    //对所有tokenGranters继承类进行遍历
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if (grant!=null) {
                return grant;
            }
        }
        return null;
    }
    
    public void addTokenGranter(TokenGranter tokenGranter) {
        if (tokenGranter == null) {
            throw new IllegalArgumentException("Token granter is null");
        }
        tokenGranters.add(tokenGranter);
    }

}

2.2.2.2 TokenStore

一般在TokenGranter执行grant方法完毕后,TokenStore将OAuth2AccessToken和OAuth2Authentication存储起来,方便以后根据其中一个查询另外一个(如根据access_token查询获得OAuth2Authentication)。

存储OAuth2AccessTokenOAuth2Authentication(比Authentication多了两个属性storedRequest,userAuthentication),存储方法如下。还有各种read,remove方法。

public interface TokenStore {

    void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);

    OAuth2Authentication readAuthentication(OAuth2AccessToken token);
    
    OAuth2Authentication readAuthentication(String token);

    OAuth2AccessToken readAccessToken(String tokenValue);

    void removeAccessToken(OAuth2AccessToken token);

    void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);

    OAuth2RefreshToken readRefreshToken(String tokenValue);

    OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);

    void removeRefreshToken(OAuth2RefreshToken token);

    void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);

    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

    Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);

    Collection<OAuth2AccessToken> findTokensByClientId(String clientId);

}

TokenStore的实现类有5类,其中JdbcTokenStore是通过连接数据库来存储OAuth2AccessToken的,这也是我们一般存储token的方法。条件是数据库里的表结构必须按照标准建立。

21580557-4ce2c9dbee0ea3bb.png

JdbcTokenStore:oauth_access_token表结构如下,可见表里存储了OAuth2AccessToken和OAuth2Authentication两个对象,值得注意的是token_id并不等于OAuth2AccessToken.getValue(),value经过MD5加密后才是token_id。同理authentication_id 和 refresh_token也是经过加密转换存储的。第一次获得token,直接存入数据库表里。如果重复post请求/oauth/token, JdbcTokenStore会先判断表中是否已有该用户的token,如果有先删除,再添加。

21580557-20289ba5cc4ca997.png

JwtTokenStore:不存储token和authentication,直接根据token解析获得authentication。

2.2.2.3 TokenExtractor (OAuth2AuthenticationProcessingFilter)

用户携带token访问资源,过滤器进行到OAuth2AuthenticationProcessingFilter时,从HttpServletRequest中获取Authorization或access_token(可以从header或者params中获取),拼接成PreAuthenticatedAuthenticationToken(Authentication子类)

BearerTokenExtractor是它的实现类,实现了从request中获取Authentication的方法。

  1. header中 Authentication:Bearer xxxxxxxx--xxx
  2. request parameters中 access_token=xxxx-xxxx-xxxx

如果都不存在,则不是Oauth2的认证方式。

public class BearerTokenExtractor implements TokenExtractor {

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

    //从HttpServletRequest中获取access_token
    @Override
    public Authentication extract(HttpServletRequest request) {
        String tokenValue = extractToken(request);
        if (tokenValue != null) {
            PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
            return authentication;
        }
        return null;
    }

    //从请求参数中获取access_token=xxxx-xxxx-xxxx,并在请求头中添加token类型;
    protected String extractToken(HttpServletRequest request) {
        // first check the header...
        String token = extractHeaderToken(request);

        // bearer type allows a request parameter as well
        if (token == null) {
            logger.debug("Token not found in headers. Trying request parameters.");
            token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
            if (token == null) {
                logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
            }
            else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
            }
        }

        return token;
    }

    //从请求头中获取Authentication:Bearer xxxxxxxx--xxx,并在请求头中添加token类型。
    protected String extractHeaderToken(HttpServletRequest request) {
        Enumeration<String> headers = request.getHeaders("Authorization");
        while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
            String value = headers.nextElement();
            if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
                String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
                // Add this here for the auth details later. Would be better to change the signature of this method.
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
                        value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
                int commaIndex = authHeaderValue.indexOf(',');
                if (commaIndex > 0) {
                    authHeaderValue = authHeaderValue.substring(0, commaIndex);
                }
                return authHeaderValue;
            }
        }

        return null;
    }

}

2.2.2.4 ResourceServerTokenServices

两个方法。用户携access_token访问资源服务器时,资源服务器会将该字符串进行解析,获得OAuth2Authentication和OAuth2AccessToken。

loadAuthentication根据字符串accessToken获得OAuth2Authentication;

readAccessToken根据字符串accessToken获得OAuth2AccessToken。

public interface ResourceServerTokenServices {
    //根据字符串accessToken获得OAuth2Authentication
    OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
    //根据字符串accessToken获得OAuth2AccessToken
    OAuth2AccessToken readAccessToken(String accessToken);

}
DefaultTokenServices

实现了两个接口AuthorizationServerTokenServices和ResourceServerTokenServices。常在granter().grant()方法中调用tokenServices.createAccessToken()方法获得oauth2accesstoken。

OAuth2AccessToken

public interface AuthorizationServerTokenServices {

    OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

    OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
            throws AuthenticationException;

    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
    
}
21580557-e10ad7024f80873a.png

其中重要方法createAccessToken(OAuth2Authentication oauth2)源码如下

    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if (existingAccessToken != null) {
            if (existingAccessToken.isExpired()) {
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    // The token store could remove the refresh token when the
                    // access token is removed, but we want to
                    // be sure...
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
            else {
                // Re-store the access token in case the authentication has changed
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        // 在access_token没有关联的refresh_token的情况下才能创建refresh_token,如果有的话会重复利用
        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }
        // 如果refresh_token过期了需要重新发布
        else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = createRefreshToken(authentication);
            }
        }
        
        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        // In case it was modified
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;

    }
RemoteTokenServices

当授权服务和资源服务不在一个应用程序的时候,资源服务可以把传递来的access_token递交给授权服务的/oauth/check_token进行验证,而资源服务自己无需去连接数据库验证access_token,这时就用到了RemoteTokenServices。

loadAuthentication方法,设置head表头Authorization 存储clientId和clientSecret信息,请求参数包含access_token字符串,向AuthServer的CheckTokenEndpoint (/oauth/check_token)发送请求,返回验证结果map(包含clientId,grantType,scope,username等信息),拼接成OAuth2Authentication。

AuthServer需要配置checkTokenAccess,否则默认为“denyAll()”,请求访问/oauth/check_token会提示没权限。

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
            oauthServer.realm(QQ_RESOURCE_ID).allowFormAuthenticationForClients();
 
            // 访问/oauth/check_token 需要client验证
            oauthServer.checkTokenAccess("isAuthenticated()");、
            // 也可配置访问/oauth/check_token无需验证
            // oauthServer.checkTokenAccess("permitAll()");
        }

不支持readAccessToken方法。

public class RemoteTokenServices implements ResourceServerTokenServices {

    protected final Log logger = LogFactory.getLog(getClass());

    private RestOperations restTemplate;

    private String checkTokenEndpointUrl;

    private String clientId;

    private String clientSecret;

    private String tokenName = "token";

    private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();

    public RemoteTokenServices() {
        restTemplate = new RestTemplate();
        ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            // Ignore 400
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode() != 400) {
                    super.handleError(response);
                }
            }
        });
    }

    public void setRestTemplate(RestOperations restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
        this.checkTokenEndpointUrl = checkTokenEndpointUrl;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public void setClientSecret(String clientSecret) {
        this.clientSecret = clientSecret;
    }

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

    public void setTokenName(String tokenName) {
        this.tokenName = tokenName;
    }

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

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token");
    }

    private String getAuthorizationHeader(String clientId, String clientSecret) {

        if(clientId == null || clientSecret == null) {
            logger.warn("Null Client ID or Client Secret detected. Endpoint that requires authentication will reject request with 401 error.");
        }

        String creds = String.format("%s:%s", clientId, clientSecret);
        try {
            return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
        }
        catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("Could not convert String");
        }
    }

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

}

2.2.3 Client客户端相关类 ClientDetails ClientDetailsService

就是UserDetails和UserDetailsService的翻版。一个是对应user,一个是对应client。

client需要事先注册到授权服务器,这样授权服务器会根据client的授权请求获取clientId,secret等信息,进行验证后返回token。

2.2.3.1 ClientDetails

client的信息,存于授权服务器端,这样只需要知道客户端的clientId,就可以获取到客户端能访问哪些资源,是否需要密码,是否限制了scope,拥有的权限等等。

public interface ClientDetails extends Serializable {
 
    String getClientId();
 
    // client能访问的资源id
    Set<String> getResourceIds();
 
    // 验证client是否需要密码
    boolean isSecretRequired();
 
    
    String getClientSecret();
 
    // client是否限制了scope
    boolean isScoped();
 
    // scope集合
    Set<String> getScope();
 
    // 根据哪些grantType验证通过client
    Set<String> getAuthorizedGrantTypes();
 
    // 注册成功后跳转的uri
    Set<String> getRegisteredRedirectUri();
 
    // client拥有的权限
    Collection<GrantedAuthority> getAuthorities();
 
    // client的token时效
    Integer getAccessTokenValiditySeconds();
 
    // client的refreshToken时效
    Integer getRefreshTokenValiditySeconds();
    
    // true:默认自动授权;false:需要用户确定才能授权
    boolean isAutoApprove(String scope);
 
    // 额外的信息
    Map<String, Object> getAdditionalInformation();
 
}

2.2.3.2 ClientDetailsService

只有一个loadClientByClientId方法,根据clientId获取clientDetails对象。

public interface ClientDetailsService {

  /**
   * Load a client by the client id. This method must not return null.
   *
   * @param clientId The client id.
   * @return The client details (never null).
   * @throws ClientRegistrationException If the client account is locked, expired, disabled, or invalid for any other reason.
   */
  ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException;

}

有两个子类

  • InMemoryClientDetailsService(内存):把ClientDetails存内存
  • JdbcClientDetailsService:存数据库里(oauth_client_details表)

在AuthorizationServerConfigurerAdapter类中的configure方法中配置客户端信息存储方式:

//存储在数据库中:
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }
    
//或存储在内存中:
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
 
        // @formatter:off
        clients.inMemory().withClient("aiqiyi")
              .resourceIds(QQ_RESOURCE_ID)
              .authorizedGrantTypes("authorization_code", "refresh_token", "implicit")
              .authorities("ROLE_CLIENT")
              // , "get_fanslist"
              .scopes("get_fanslist")
              .secret("secret")
              .redirectUris("http://localhost:8081/aiqiyi/qq/redirect")
              .autoApprove(true)
              .autoApprove("get_user_info")
              .and()
              .withClient("youku")
              .resourceIds(QQ_RESOURCE_ID)
              .authorizedGrantTypes("authorization_code", "refresh_token", "implicit")
              .authorities("ROLE_CLIENT")
              .scopes("get_user_info", "get_fanslist")
              .secret("secret")
              .redirectUris("http://localhost:8082/youku/qq/redirect");
    }

2.2.3.3 ClientDetailsServiceBuilder

创建InMemoryClientDetailsService或者JdbcClientDetailsService,有内部类ClientDetailsServiceBuilder。

public class ClientDetailsServiceBuilder<B extends ClientDetailsServiceBuilder<B>> extends
        SecurityConfigurerAdapter<ClientDetailsService, B> implements SecurityBuilder<ClientDetailsService> {
 
    private List<ClientBuilder> clientBuilders = new ArrayList<ClientBuilder>();
 
    public InMemoryClientDetailsServiceBuilder inMemory() throws Exception {
        return new InMemoryClientDetailsServiceBuilder();
    }
 
    public JdbcClientDetailsServiceBuilder jdbc() throws Exception {
        return new JdbcClientDetailsServiceBuilder();
    }
 
    @SuppressWarnings("rawtypes")
    public ClientDetailsServiceBuilder<?> clients(final ClientDetailsService clientDetailsService) throws Exception {
        return new ClientDetailsServiceBuilder() {
            @Override
            public ClientDetailsService build() throws Exception {
                return clientDetailsService;
            }
        };
    }
 
    // clients.inMemory().withClient("clientId").scopes().secret()...
    public ClientBuilder withClient(String clientId) {
        ClientBuilder clientBuilder = new ClientBuilder(clientId);
        this.clientBuilders.add(clientBuilder);
        return clientBuilder;
    }
 
    @Override
    public ClientDetailsService build() throws Exception {
        for (ClientBuilder clientDetailsBldr : clientBuilders) {
            addClient(clientDetailsBldr.clientId, clientDetailsBldr.build());
        }
        return performBuild();
    }
 
    protected void addClient(String clientId, ClientDetails build) {
    }
 
    protected ClientDetailsService performBuild() {
        throw new UnsupportedOperationException("Cannot build client services (maybe use inMemory() or jdbc()).");
    }
 
    public final class ClientBuilder {
         // ...
         public ClientDetailsServiceBuilder<B> and() {
            return ClientDetailsServiceBuilder.this;
        }
    }
}

2.2.4 资源服务器配置 ResourceServerConfigurerAdapter

配置哪些路径需要认证后才能访问,哪些不需要。自然就联想到了HttpSecurity(配置HttpSecurity就相当于配置了不同uri对应的filters)。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()//所有请求必须登陆后访问
                .and().httpBasic()
                .and()
                    .formLogin()
                    .loginPage("/login")
                    .defaultSuccessUrl("/index")
                    .failureUrl("/login?error")
                    .permitAll()//登录界面,错误界面可以直接访问
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login")
                .permitAll().and().rememberMe();//注销请求可直接访问
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER", "ADMIN");
    }
}

作为资源服务器ResourceServerConfigurerAdapter,需要和@EnableResourceServer搭配,然后和上面一样需配置HttpSecurity就好了。还能配置ResourceServerSecurityConfigurer,设置tokenService等。

/**
 * 配置资源服务器
*/
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
 
    @Autowired
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    @Autowired
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;

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

        http
            .exceptionHandling()
            .authenticationEntryPoint(customAuthenticationEntryPoint)
            .and()
            .logout()
            .logoutUrl("/oauth/logout")
            .logoutSuccessHandler(customLogoutSuccessHandler)
            .and()
            .authorizeRequests()
            // hello路径允许直接访问
            .antMatchers("/hello/").permitAll()
            // secure路径需要验证后才能访问
            .antMatchers("/secure/**").authenticated();
    }
 
 
    // 远程连接authServer服务
    @Autowired
    public RemoteTokenServices remoteTokenServices;
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenServices(remoteTokenServices);
    }
}

2.2.5 授权服务器配置 AuthorizationServerConfigurerAdapter

注册client信息,可以同时配置多个不同类型的client。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    //token存储方式
    @Resource
    private TokenStore tokenStore;
    //JWT令牌配置
    @Resource
    private JwtAccessTokenConverter accessTokenConverter;

    //客户端详情服务
    @Autowired
    private ClientDetailsService clientDetailsService;

    //认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;


    /**
     * 将客户端信息存储到数据库
     *
     * @param dataSource
     * @return
     */
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService)clientDetailsService).setPasswordEncoder(bCryptPasswordEncoder);
        return clientDetailsService;
    }

    /**
     * 客户端配置
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
//        clients.inMemory()//使用内存存储
//                .withClient("c1") //客户端id
//                .secret(bCryptPasswordEncoder.encode("abc123"))//设置密码
//                .resourceIds("res1")//可访问的资源列表
//                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")//该client允许的授权类型
//                .scopes("all")//允许的授权范围
//                .autoApprove(false)//false跳转到授权页面,true不跳转
//                .redirectUris("http://www.baidu.com");//设置回调地址
    }


    /**
     * 令牌管理服务
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService); //客户端详情服务
        services.setSupportRefreshToken(true); //支持刷新令牌
        services.setTokenStore(tokenStore); //令牌的存储策略
        //令牌增强,设置JWT令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);

        services.setAccessTokenValiditySeconds(7200); //令牌默认有效时间2小时
        services.setRefreshTokenValiditySeconds(259200); //刷新令牌默认有效期3天
        return services;
    }

    /**
     * 设置授权码模式的授权码如何存取,暂时采用内存方式
     *
     * @return
     */
//    @Bean
//    public AuthorizationCodeServices authorizationCodeServices(){
//        return new InMemoryAuthorizationCodeServices();
//    }

    @Resource
    private AuthorizationCodeServices authorizationCodeServices;

    /**
     * 授权码存储到数据库
     * @param dataSource
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource){
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    /**
     * 令牌访问端点配置
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)//认证管理器
                .authorizationCodeServices(authorizationCodeServices)//授权码服务
                .tokenServices(tokenServices()) //令牌管理服务(设置令牌存储方式和令牌类型JWT)
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    /**
     * 对授权端点接口的安全约束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") // /auth/token_key是公开的
                .checkTokenAccess("permitAll()") // /auth/check_token是公开的
                .allowFormAuthenticationForClients(); //允许表单认证(申请令牌)
    }

}

2.2.6 TokenEndPoint,AuthorizationEndPoint,CheckTokenEndPoint

2.2.6.1 TokenEndPoint

客户端post请求"/oauth/token",验证用户信息并获取OAuth2AccessToken,必须先经过client验证。这一步的最终目的是存储OAuth2AccessToken+OAuth2Authentication并返回OAuth2AccessToken。

    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken>     postAccessToken(Principal principal,   @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
 
        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException(
                    "There is no client authentication. Try adding an appropriate authentication filter.");
        }
 
        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
 
        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
 
        ...
        // AuthorizationServerEndpointsConfigurer
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }
 
        return getResponse(token);
    }

2.2.6.2 AuthorizationEndPoint

这个一般只适用于authorization code模式,客户端请求authorization server中的/oauth/authorize(请求前先得登录oauth server获得authentication),验证client信息后根据redirect_uri请求重定向回client,同时带上code值。client附带code值再次向/oauth/token请求,返回accesstoken。

    @RequestMapping(value = "/oauth/authorize")
    public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
            SessionStatus sessionStatus, Principal principal) {
 
        // Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
        // query off of the authorization request instead of referring back to the parameters map. The contents of the
        // parameters map will be stored without change in the AuthorizationRequest object once it is created.
        AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
 
        Set<String> responseTypes = authorizationRequest.getResponseTypes();
 
        if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
            throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
        }
 
        if (authorizationRequest.getClientId() == null) {
            throw new InvalidClientException("A client id must be provided");
        }
 
        try {
 
            if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
                throw new InsufficientAuthenticationException(
                        "User must be authenticated with Spring Security before authorization can be completed.");
            }
 
            ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
 
            // The resolved redirect URI is either the redirect_uri from the parameters or the one from
            // clientDetails. Either way we need to store it on the AuthorizationRequest.
            String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
            String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
            if (!StringUtils.hasText(resolvedRedirect)) {
                throw new RedirectMismatchException(
                        "A redirectUri must be either supplied or preconfigured in the ClientDetails");
            }
            authorizationRequest.setRedirectUri(resolvedRedirect);
 
            // We intentionally only validate the parameters requested by the client (ignoring any data that may have
            // been added to the request by the manager).
            oauth2RequestValidator.validateScope(authorizationRequest, client);
 
            // Some systems may allow for approval decisions to be remembered or approved by default. Check for
            // such logic here, and set the approved flag on the authorization request accordingly.
            authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
                    (Authentication) principal);
            // TODO: is this call necessary?
            boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
            authorizationRequest.setApproved(approved);
 
            // Validation is all done, so we can check for auto approval...
            if (authorizationRequest.isApproved()) {
                if (responseTypes.contains("token")) {
                    return getImplicitGrantResponse(authorizationRequest);
                }
                if (responseTypes.contains("code")) {
                                 // 生成code值并返回
                    return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                            (Authentication) principal));
                }
            }
 
            // Place auth request into the model so that it is stored in the session
            // for approveOrDeny to use. That way we make sure that auth request comes from the session,
            // so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
            model.put("authorizationRequest", authorizationRequest);
 
            return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
 
        }
        catch (RuntimeException e) {
            sessionStatus.setComplete();
            throw e;
        }
 
    }

2.2.6.3 CheckTokenEndpoint

当采用RemoteTokenServices时,resouceServer无法自行验证access_token字符串是否正确,遂递交给另一个应用程序中的authserver里CheckTokenEndpoint(/oauth/check_token)进行检验,检验结果返回给resourceServer。

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

三、异常处理源码

3.1 概述

异常处理规则:

  • 规则1. 如果异常是 AuthenticationException,使用 AuthenticationEntryPoint 处理
  • 规则2. 如果异常是 AccessDeniedException 且用户是匿名用户,使用 AuthenticationEntryPoint 处理
  • 规则3. 如果异常是 AccessDeniedException 且用户不是匿名用户,如果否则交给 AccessDeniedHandler 处理。

3.2 源码

3.2.1 ExceptionTranslationFilter

ExceptionTranslationFilter的doFilter

ExceptionTranslationFilter是个异常过滤器,用来处理在认证授权过程中抛出的异常,在过滤器链中处于倒数第三的位置(这个filter后面分为是FilterSecurityInterceptor、SwitchUserFilter),所以ExceptionTranslationFilter只能捕获到后面两个过滤器所抛出的异常。

ExceptionTranslationFilter后面的过滤器是FilterSecurityInterceptor。先上一张图,如下图1所示:

21580557-0fd084a033d2b022.png
  • 红框1中的,是调用Filter链中的后续Filter。
  • 如果图1中的操作抛出异常,就会来到红框2处,判断抛出的异常是否是AuthenticationException。
  • 如果抛出的异常不是AuthenticationException,即红框2的结果为null,那么就到红框3处,判断是否是AccessDeniedException。
  • 如果抛出的异常是AuthenticationException或者时AccessDeniedException,那么执行红框4处的代码。

ExceptionTranslationFilter的handleSpringSecurityException方法

下面来看handleSpringSecurityException的方法体

public class ExceptionTranslationFilter extends GenericFilterBean {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            chain.doFilter(request, response);

            logger.debug("Chain processed normally");
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
            RuntimeException ase = (AuthenticationException) throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);

            if (ase == null) {
                ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                        AccessDeniedException.class, causeChain);
            }

            if (ase != null) {
                if (response.isCommitted()) {
                    throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
                }
                handleSpringSecurityException(request, response, chain, ase);
            }
            else {
                // Rethrow ServletExceptions and RuntimeExceptions as-is
                if (ex instanceof ServletException) {
                    throw (ServletException) ex;
                }
                else if (ex instanceof RuntimeException) {
                    throw (RuntimeException) ex;
                }

                // Wrap other Exceptions. This shouldn't actually happen
                // as we've already covered all the possibilities for doFilter
                throw new RuntimeException(ex);
            }
        }
    }

    private void handleSpringSecurityException(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, RuntimeException exception)
            throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
            logger.debug(
                    "Authentication exception occurred; redirecting to authentication entry point",
                    exception);

            sendStartAuthentication(request, response, chain,
                    (AuthenticationException) exception);
        }
        else if (exception instanceof AccessDeniedException) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
                logger.debug(
                        "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
                        exception);

                sendStartAuthentication(
                        request,
                        response,
                        chain,
                        new InsufficientAuthenticationException(
                            messages.getMessage(
                                "ExceptionTranslationFilter.insufficientAuthentication",
                                "Full authentication is required to access this resource")));
            }
            else {
                logger.debug(
                        "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
                        exception);

                accessDeniedHandler.handle(request, response,
                        (AccessDeniedException) exception);
            }
        }
    }

    protected void sendStartAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain,
            AuthenticationException reason) throws ServletException, IOException {
        // SEC-112: Clear the SecurityContextHolder's Authentication, as the
        // existing Authentication is no longer considered valid
        SecurityContextHolder.getContext().setAuthentication(null);
        requestCache.saveRequest(request, response);   //保存当前请求
        logger.debug("Calling Authentication entry point.");
        authenticationEntryPoint.commence(request, response, reason);
    }

}
  1. 如果抛出的异常是AuthenticationException,则执行方法sendStartAuthentication
  2. 如果抛出的异常是AccessDeniedException,且从SecurityContextHolder.getContext().getAuthentication()得到的是AnonymousAuthenticationToken或者RememberMeAuthenticationToken,那么执行sendStartAuthentication
  3. 如果上面的第二点不满足,则执行accessDeniedHandler的handle方法

在HttpSessionRequestCache 中会将本次请求的信息保存到session中

public class HttpSessionRequestCache implements RequestCache {
    /**
     * Stores the current request, provided the configuration properties allow it.
     */
    public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
        if (requestMatcher.matches(request)) {
            DefaultSavedRequest savedRequest = new DefaultSavedRequest(request,
                    portResolver);

            if (createSessionAllowed || request.getSession(false) != null) {
                // Store the HTTP request itself. Used by
                // AbstractAuthenticationProcessingFilter
                // for redirection after successful authentication (SEC-29)
                request.getSession().setAttribute(this.sessionAttrName, savedRequest);
                logger.debug("DefaultSavedRequest added to Session: " + savedRequest);
            }
        }
        else {
            logger.debug("Request not saved as configured RequestMatcher did not match");
        }
    }
}
    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
        this.accessDeniedHandler = accessDeniedHandler;
    }

ExceptionTranslationFilter的sendStartAuthentication方法

调用sendStartAuthentication方法实现对request的缓存和重定向

    protected void sendStartAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain,
            AuthenticationException reason) throws ServletException, IOException {
        // SEC-112: Clear the SecurityContextHolder's Authentication, as the
        // existing Authentication is no longer considered valid
        SecurityContextHolder.getContext().setAuthentication(null);
        requestCache.saveRequest(request, response);
        logger.debug("Calling Authentication entry point.");
        authenticationEntryPoint.commence(request, response, reason);
    }

在commence方法中完成对请求的重定向

    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {

        String redirectUrl = null;

        if (useForward) {

            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be
                // used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response,
                        authException);

                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }

                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        }
        else {
            // redirect to login page. Use https if forceHttps true

            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

        }

        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }

自定义未登录异常

如果未登录,不希望跳转到/login而是直接抛异常或跳转到指定路径,可以通过以下两步来实现:

  1. 自定义类实现AuthenticationEntryPoint接口,重写commence方法。

    @Configuration
    public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            if (!response.isCommitted()) {
    //            response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,"未认证的用户:" + authException.getMessage());
                new DefaultRedirectStrategy().sendRedirect(request, response, "http://www.jd.com");
            }
        }
    
    }
    
  2. 在WebSecurityConfigurerAdapter继承类中指定异常处理类为自定义类。

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    //跨域请求伪造防御失效
                    .csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/r/r1").hasAnyAuthority("p1")
                    .antMatchers("/uaa/publicKey", "/login**", "/isExpired**", "/mobile/**", "/check/**", "/user/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(new MyAuthenticationEntryPoint());
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            super.configure(auth);
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            super.configure(web);
        }
    
    }
    

3.2.2 FilterSecurityInterceptor

在web应用中,spring security是一个filter。而在filter内部,它又自建了一个filter chain(如果不用命名空间,也可以自定义)。spring security按顺序对每个filter进行处理。各filter之间有较大的差异性。与权限验证关系最密切的是FilterSecurityInterceptor。

FilterSecurityInterceptor认证及验权流程:

21580557-91f104e63676e03d.png

FilterSecurityInterceptor的类关系图如下。它使用AuthenticationManager做认证(用户是否已登录),使用AccessDecisionManager做验证(用户是否有权限)。

21580557-c3d28217250cf5ee.png

ProviderManager是默认的AuthenticationManager实现类,它不直接进行认证。而是采用组合模式,将认证工作委托给AuthenticationProvider。一般情况下,一组AuthenticationProvider有一个认证成功,就被视为认证成功。ProviderManager关系图如下:

21580557-18e1a04bf1e402e4.png

AccessDecisionManager负责验证用户是否有操作权限,它也是采用组合模式。security自带的AccessDecisionManager实现类有三种:AffirmativeBased只要有一个认证处理器认证通过就表示成功;ConsensusBased采用的是多数原则;UnanimousBased采用一票否决制。

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

推荐阅读更多精彩内容