Oauth2源碼分析(下)

二喧务、密碼模式源碼

2.1 概述

訪問(wèn)/oauth/token會(huì)經(jīng)過(guò)攔截器的順序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等勒葱。根據(jù)tokenRequest獲取OAuth2Request浪汪,初始化獲得OAuth2Authentication,再去數(shù)據(jù)庫(kù)里找oauth2accesstoken凛虽,如果有則直接返回死遭,如果沒(méi)有則創(chuàng)建新的oauth2accesstoken,并且和OAuth2Authentication一起存入數(shù)據(jù)庫(kù)中凯旋。

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類(lèi)中定義了/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 :資源服務(wù)可以把傳遞來(lái)的access_token遞交給授權(quán)服務(wù)的/oauth/check_token進(jìn)行驗(yàn)證呀潭,而資源服務(wù)自己無(wú)需去連接數(shù)據(jù)庫(kù)驗(yàn)證access_token,這時(shí)就用到了RemoteTokenServices至非。

2.2.1 Oauth的請(qǐng)求封裝類(lèi)

OAuth2Authentication和OAuth2AccessToken是一對(duì)好基友钠署,誰(shuí)要先走誰(shuí)是狗!;耐帧谐鼎!

2.2.1.1 OAuth2Authentication

OAuth2Authentication顧名思義是Authentication的子類(lèi),存儲(chǔ)用戶信息和客戶端信息趣惠,但多了2個(gè)屬性

private final OAuth2Request storedRequest; 
private final Authentication userAuthentication;

這樣OAuth2Authentication可以存儲(chǔ)2個(gè)Authentication狸棍,一個(gè)給client(必要),一個(gè)給user(只是有些授權(quán)方式需要)味悄。除此之外同樣有principle草戈,credentials,authorities侍瑟,details猾瘸,authenticated等屬性。

OAuth2Request 用于存儲(chǔ)request中的Authentication信息(grantType,responseType,resouceId,clientId,scope等),這里就引出了OAuth2 中的三大request牵触。

2.2.1.2 OAuth2AccessToken

OAuth2AccessToken是一個(gè)接口,提供安全令牌token的基本信息咐低,不包含用戶信息揽思,僅包含一些靜態(tài)屬性(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同時(shí)存儲(chǔ)OAuth2AccessToken和OAuth2Authentication钉汗,也可根據(jù)OAuth2Authentication中的OAuth2Request信息可獲取對(duì)應(yīng)的OAuth2AccessToken

DefaultTokenServices有如下方法鲤屡,都可以通過(guò)一個(gè)獲得另一個(gè)的值 损痰。

OAuth2AccessToken createAccessToken(OAuth2Authentication authentication)

OAuth2Authentication loadAuthentication(String accessTokenValue)

當(dāng)tokenStore是jdbcTokenStore,表示從數(shù)據(jù)庫(kù)中根據(jù)OAuth2Authentication獲取OAuth2AccessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);

DefaultOAuth2AccessToken是OAuth2AccessToken的實(shí)現(xiàn)類(lèi)酒来,多了構(gòu)造方法卢未,setter方法和OAuth2AccessToken valueOf(Map<String,Object> tokenParams)。經(jīng)過(guò)json轉(zhuǎn)換后就是我們常見(jiàn)的access_token對(duì)象堰汉,如下所示辽社。

{
"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及其繼承類(lèi)AuthorizationRequestTokenRequest翘鸭、OAuth2Request

BaseRequest是抽象類(lèi)滴铅,有3個(gè)屬性: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  */
}

其繼承類(lèi)有AuthorizationRequest汉匙、TokenRequestOAuth2Request生蚁。

  • AuthorizationRequest:向授權(quán)服務(wù)器AuthorizationEndPoint (/oauth/authorize)請(qǐng)求授權(quán)噩翠,AuthorizationRequest作為載體存儲(chǔ)state,redirect_uri等參數(shù),生命周期很短且不能長(zhǎng)時(shí)間存儲(chǔ)信息守伸,可用OAuth2Request代替存儲(chǔ)信息绎秒。

    public class AuthorizationRequest extends BaseRequest implements Serializable {
     
      // 用戶同意授權(quán)傳遞的參數(shù),不可改變
      private Map<String, String> approvalParameters = Collections.unmodifiableMap(new HashMap<String, String>());
     
      // 客戶端發(fā)送出的狀態(tài)信息尼摹,從授權(quán)服務(wù)器返回的狀態(tài)應(yīng)該不變才對(duì)
      private String state;
     
      // 返回類(lèi)型集合
      private Set<String> responseTypes = new HashSet<String>();
     
      // resource ids  可變
      private Set<String> resourceIds = new HashSet<String>();
     
      // 授權(quán)的權(quán)限
      private Collection<? extends GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
     
      // 終端用戶是否同意該request發(fā)送
      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:向授權(quán)服務(wù)器TokenEndPoint(/oauth/token)發(fā)送請(qǐng)求獲得access_token時(shí)见芹,tokenRequest作為載體存儲(chǔ)請(qǐng)求中g(shù)rantType等參數(shù)。常和tokenGranter.grant(grantType,tokenRequest)結(jié)合起來(lái)使用蠢涝。
    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:用來(lái)存儲(chǔ)TokenRequest或者AuthorizationRequest的信息,只有構(gòu)造方法和getter方法和二,不提供setter方法徘铝。它作為OAuth2Authentication的一個(gè)屬性(StoredRequest),存儲(chǔ)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的實(shí)現(xiàn)類(lèi)。

public interface OAuth2RefreshToken {

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

}

2.2.1.5 OAuth2RequestFactory接口

工廠類(lèi)用于生成OAuth2Request淹魄、TokenRequest郁惜、AuthenticationRequest。

public interface OAuth2RequestFactory {
 
    /**
            * 從request請(qǐng)求參數(shù)中獲取clientId,scope,state
            * clientDetailsService  loadClientByClientId(clientId) 獲取clientDetails resourcesId Authorities
            * 根據(jù)以上信息生成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)

一般在用戶請(qǐng)求TokenEndPoints中的路徑/oauth/token時(shí),根據(jù)請(qǐng)求參數(shù)中的grantType,username,password缤沦,client_id,client_secret等虎韵,調(diào)用TokenGranter給用戶分發(fā)OAuth2AccessToken。

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

根據(jù)grantType(password,authorization-code)和TokenRequest(requestParameters,clientId,grantType)授予人OAuth2AccessToken令牌缸废。

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

回憶下TokenRequest包含了基本信息clientId,scope,requestParameters,grantType等包蓝。根據(jù)tokenRequest獲取OAuth2Request,初始化獲得OAuth2Authentication呆奕,再去數(shù)據(jù)庫(kù)里找Oauth2AccessToken养晋,如果有則直接返回,如果沒(méi)有則創(chuàng)建新的Oauth2AccessToken梁钾,并且和OAuth2Authentication一起存入數(shù)據(jù)庫(kù)中。

AbstractTokenGranter(授予OAuth2AccessToken)

TokenGranter抽象繼承類(lèi)AbstractTokenGranter拇勃,實(shí)現(xiàn)了grant方法瓣赂。

執(zhí)行順序?yàn)楦鶕?jù)tokenRequest====》clientId ====》clientDetails====》OAuth2Authentication(getOAuth2Authentication(client,tokenRequest))====》OAuth2AccessToken(tokenService.createAccessToken)

通過(guò)clientId獲取ClientDetails苫纤,判斷客戶端是否有當(dāng)前正在發(fā)起請(qǐng)求的授權(quán)模式祝高,調(diào)用OAuth2RequestFactory的createOAuth2Request方法傳入TokenRequest參數(shù)獲得OAuth2Request颓屑,通過(guò)createAccessToken方法將獲取的OAuth2Request作為參數(shù)獲得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;
    }

    //通過(guò)grant方法進(jìn)行認(rèn)證,獲取OAuth2AccessToken
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        if (!this.grantType.equals(grantType)) {
            return null;
        }
        //通過(guò)ClientDetails獲取到client進(jìn)行認(rèn)證
        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);

    }

    //通過(guò)OAuth2Authentication獲取到OAuth2AccessToken
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }
    //通過(guò)TokenRequest獲取到OAuth2Request瓶摆,通過(guò)OAuth2Request獲取到OAuth2Authentication
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, null);
    }

    //判斷客戶端是否擁有指定的授權(quán)類(lèi)型群井,沒(méi)有則拋出異常
    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;
    }

}

實(shí)現(xiàn)AbstractTokenGranter的類(lèi)有5種书斜。

21580557-7e210a361f9f6ee8.png

其中如果用password的方式進(jìn)行驗(yàn)證,那么TokenGranter類(lèi)型是ResourceOwnerPasswordTokenGranter自晰,該類(lèi)中重寫(xiě)了getOAuth2Authentication方法枪向,里面調(diào)用了authenticationManager.manage()方法。

用戶可自行定義granter類(lèi)繼承AbstractTokenGranter傍衡,重寫(xiě)**getOAuth2Authentication()**方法深员,并將該granter類(lèi)添加至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;
    }

    //重寫(xiě)了父類(lèi)的方法蛙埂,增加authenticate方法對(duì)賬號(hào)密碼進(jìn)行驗(yàn)證倦畅。
    @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有繼承類(lèi)CompositeTokenGranter,包含List<TokenGranter> tokenGranters屬性绣的,grant方法是遍歷tokenGranters進(jìn)行逐一grant叠赐,只要有一個(gè)有返回值就返回。

public class CompositeTokenGranter implements TokenGranter {

    private final List<TokenGranter> tokenGranters;

    public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
        this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
    }
    
    //對(duì)所有tokenGranters繼承類(lèi)進(jìn)行遍歷
    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執(zhí)行g(shù)rant方法完畢后屡江,TokenStore將OAuth2AccessToken和OAuth2Authentication存儲(chǔ)起來(lái)芭概,方便以后根據(jù)其中一個(gè)查詢另外一個(gè)(如根據(jù)access_token查詢獲得OAuth2Authentication)。

存儲(chǔ)OAuth2AccessTokenOAuth2Authentication(比Authentication多了兩個(gè)屬性storedRequest惩嘉,userAuthentication)罢洲,存儲(chǔ)方法如下。還有各種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的實(shí)現(xiàn)類(lèi)有5類(lèi),其中JdbcTokenStore是通過(guò)連接數(shù)據(jù)庫(kù)來(lái)存儲(chǔ)OAuth2AccessToken的臊诊,這也是我們一般存儲(chǔ)token的方法鸽粉。條件是數(shù)據(jù)庫(kù)里的表結(jié)構(gòu)必須按照標(biāo)準(zhǔn)建立。

21580557-4ce2c9dbee0ea3bb.png

JdbcTokenStore:oauth_access_token表結(jié)構(gòu)如下抓艳,可見(jiàn)表里存儲(chǔ)了OAuth2AccessToken和OAuth2Authentication兩個(gè)對(duì)象触机,值得注意的是token_id并不等于OAuth2AccessToken.getValue(),value經(jīng)過(guò)MD5加密后才是token_id玷或。同理authentication_id 和 refresh_token也是經(jīng)過(guò)加密轉(zhuǎn)換存儲(chǔ)的儡首。第一次獲得token,直接存入數(shù)據(jù)庫(kù)表里偏友。如果重復(fù)post請(qǐng)求/oauth/token蔬胯, JdbcTokenStore會(huì)先判斷表中是否已有該用戶的token,如果有先刪除位他,再添加氛濒。

21580557-20289ba5cc4ca997.png

JwtTokenStore:不存儲(chǔ)token和authentication,直接根據(jù)token解析獲得authentication产场。

2.2.2.3 TokenExtractor (OAuth2AuthenticationProcessingFilter)

用戶攜帶token訪問(wèn)資源,過(guò)濾器進(jìn)行到OAuth2AuthenticationProcessingFilter時(shí)舞竿,從HttpServletRequest中獲取Authorization或access_token(可以從header或者params中獲取)京景,拼接成PreAuthenticatedAuthenticationToken(Authentication子類(lèi))

BearerTokenExtractor是它的實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)了從request中獲取Authentication的方法骗奖。

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

如果都不存在确徙,則不是Oauth2的認(rèn)證方式。

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

    //從請(qǐng)求參數(shù)中獲取access_token=xxxx-xxxx-xxxx执桌,并在請(qǐng)求頭中添加token類(lèi)型鄙皇;
    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;
    }

    //從請(qǐng)求頭中獲取Authentication:Bearer xxxxxxxx--xxx,并在請(qǐng)求頭中添加token類(lèi)型仰挣。
    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

兩個(gè)方法育苟。用戶攜access_token訪問(wèn)資源服務(wù)器時(shí),資源服務(wù)器會(huì)將該字符串進(jìn)行解析椎木,獲得OAuth2Authentication和OAuth2AccessToken。

loadAuthentication根據(jù)字符串a(chǎn)ccessToken獲得OAuth2Authentication;

readAccessToken根據(jù)字符串a(chǎn)ccessToken獲得OAuth2AccessToken博烂。

public interface ResourceServerTokenServices {
    //根據(jù)字符串a(chǎn)ccessToken獲得OAuth2Authentication
    OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
    //根據(jù)字符串a(chǎn)ccessToken獲得OAuth2AccessToken
    OAuth2AccessToken readAccessToken(String accessToken);

}
DefaultTokenServices

實(shí)現(xiàn)了兩個(gè)接口AuthorizationServerTokenServices和ResourceServerTokenServices香椎。常在granter().grant()方法中調(diào)用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沒(méi)有關(guān)聯(lián)的refresh_token的情況下才能創(chuàng)建refresh_token禽篱,如果有的話會(huì)重復(fù)利用
        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }
        // 如果refresh_token過(guò)期了需要重新發(fā)布
        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

當(dāng)授權(quán)服務(wù)和資源服務(wù)不在一個(gè)應(yīng)用程序的時(shí)候淤刃,資源服務(wù)可以把傳遞來(lái)的access_token遞交給授權(quán)服務(wù)的/oauth/check_token進(jìn)行驗(yàn)證讯壶,而資源服務(wù)自己無(wú)需去連接數(shù)據(jù)庫(kù)驗(yàn)證access_token,這時(shí)就用到了RemoteTokenServices。

loadAuthentication方法舆逃,設(shè)置head表頭Authorization 存儲(chǔ)clientId和clientSecret信息,請(qǐng)求參數(shù)包含access_token字符串沸停,向AuthServer的CheckTokenEndpoint (/oauth/check_token)發(fā)送請(qǐng)求辆影,返回驗(yàn)證結(jié)果map(包含clientId,grantType,scope,username等信息),拼接成OAuth2Authentication后添。

AuthServer需要配置checkTokenAccess笨枯,否則默認(rèn)為“denyAll()”,請(qǐng)求訪問(wèn)/oauth/check_token會(huì)提示沒(méi)權(quán)限遇西。

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
            oauthServer.realm(QQ_RESOURCE_ID).allowFormAuthenticationForClients();
 
            // 訪問(wèn)/oauth/check_token 需要client驗(yàn)證
            oauthServer.checkTokenAccess("isAuthenticated()");馅精、
            // 也可配置訪問(wèn)/oauth/check_token無(wú)需驗(yàn)證
            // 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客戶端相關(guān)類(lèi) ClientDetails ClientDetailsService

就是UserDetails和UserDetailsService的翻版粱檀。一個(gè)是對(duì)應(yīng)user洲敢,一個(gè)是對(duì)應(yīng)client。

client需要事先注冊(cè)到授權(quán)服務(wù)器茄蚯,這樣授權(quán)服務(wù)器會(huì)根據(jù)client的授權(quán)請(qǐng)求獲取clientId压彭,secret等信息睦优,進(jìn)行驗(yàn)證后返回token。

2.2.3.1 ClientDetails

client的信息哮塞,存于授權(quán)服務(wù)器端刨秆,這樣只需要知道客戶端的clientId,就可以獲取到客戶端能訪問(wèn)哪些資源忆畅,是否需要密碼衡未,是否限制了scope,擁有的權(quán)限等等家凯。

public interface ClientDetails extends Serializable {
 
    String getClientId();
 
    // client能訪問(wèn)的資源id
    Set<String> getResourceIds();
 
    // 驗(yàn)證client是否需要密碼
    boolean isSecretRequired();
 
    
    String getClientSecret();
 
    // client是否限制了scope
    boolean isScoped();
 
    // scope集合
    Set<String> getScope();
 
    // 根據(jù)哪些grantType驗(yàn)證通過(guò)client
    Set<String> getAuthorizedGrantTypes();
 
    // 注冊(cè)成功后跳轉(zhuǎn)的uri
    Set<String> getRegisteredRedirectUri();
 
    // client擁有的權(quán)限
    Collection<GrantedAuthority> getAuthorities();
 
    // client的token時(shí)效
    Integer getAccessTokenValiditySeconds();
 
    // client的refreshToken時(shí)效
    Integer getRefreshTokenValiditySeconds();
    
    // true:默認(rèn)自動(dòng)授權(quán)缓醋;false:需要用戶確定才能授權(quán)
    boolean isAutoApprove(String scope);
 
    // 額外的信息
    Map<String, Object> getAdditionalInformation();
 
}

2.2.3.2 ClientDetailsService

只有一個(gè)loadClientByClientId方法,根據(jù)clientId獲取clientDetails對(duì)象绊诲。

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;

}

有兩個(gè)子類(lèi)

  • InMemoryClientDetailsService(內(nèi)存):把ClientDetails存內(nèi)存
  • JdbcClientDetailsService:存數(shù)據(jù)庫(kù)里(oauth_client_details表)

在AuthorizationServerConfigurerAdapter類(lèi)中的configure方法中配置客戶端信息存儲(chǔ)方式:

//存儲(chǔ)在數(shù)據(jù)庫(kù)中:
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }
    
//或存儲(chǔ)在內(nèi)存中:
    @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

創(chuàng)建InMemoryClientDetailsService或者JdbcClientDetailsService送粱,有內(nèi)部類(lèi)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 資源服務(wù)器配置 ResourceServerConfigurerAdapter

配置哪些路徑需要認(rèn)證后才能訪問(wèn)掂之,哪些不需要抗俄。自然就聯(lián)想到了HttpSecurity(配置HttpSecurity就相當(dāng)于配置了不同uri對(duì)應(yīng)的filters)。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()//所有請(qǐng)求必須登陸后訪問(wèn)
                .and().httpBasic()
                .and()
                    .formLogin()
                    .loginPage("/login")
                    .defaultSuccessUrl("/index")
                    .failureUrl("/login?error")
                    .permitAll()//登錄界面世舰,錯(cuò)誤界面可以直接訪問(wèn)
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login")
                .permitAll().and().rememberMe();//注銷(xiāo)請(qǐng)求可直接訪問(wèn)
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER", "ADMIN");
    }
}

作為資源服務(wù)器ResourceServerConfigurerAdapter动雹,需要和@EnableResourceServer搭配,然后和上面一樣需配置HttpSecurity就好了跟压。還能配置ResourceServerSecurityConfigurer胰蝠,設(shè)置tokenService等。

/**
 * 配置資源服務(wù)器
*/
@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路徑允許直接訪問(wèn)
            .antMatchers("/hello/").permitAll()
            // secure路徑需要驗(yàn)證后才能訪問(wèn)
            .antMatchers("/secure/**").authenticated();
    }
 
 
    // 遠(yuǎn)程連接authServer服務(wù)
    @Autowired
    public RemoteTokenServices remoteTokenServices;
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenServices(remoteTokenServices);
    }
}

2.2.5 授權(quán)服務(wù)器配置 AuthorizationServerConfigurerAdapter

注冊(cè)client信息震蒋,可以同時(shí)配置多個(gè)不同類(lèi)型的client茸塞。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    //token存儲(chǔ)方式
    @Resource
    private TokenStore tokenStore;
    //JWT令牌配置
    @Resource
    private JwtAccessTokenConverter accessTokenConverter;

    //客戶端詳情服務(wù)
    @Autowired
    private ClientDetailsService clientDetailsService;

    //認(rèn)證管理器
    @Autowired
    private AuthenticationManager authenticationManager;


    /**
     * 將客戶端信息存儲(chǔ)到數(shù)據(jù)庫(kù)
     *
     * @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()//使用內(nèi)存存儲(chǔ)
//                .withClient("c1") //客戶端id
//                .secret(bCryptPasswordEncoder.encode("abc123"))//設(shè)置密碼
//                .resourceIds("res1")//可訪問(wèn)的資源列表
//                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")//該client允許的授權(quán)類(lèi)型
//                .scopes("all")//允許的授權(quán)范圍
//                .autoApprove(false)//false跳轉(zhuǎn)到授權(quán)頁(yè)面,true不跳轉(zhuǎn)
//                .redirectUris("http://www.baidu.com");//設(shè)置回調(diào)地址
    }


    /**
     * 令牌管理服務(wù)
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService); //客戶端詳情服務(wù)
        services.setSupportRefreshToken(true); //支持刷新令牌
        services.setTokenStore(tokenStore); //令牌的存儲(chǔ)策略
        //令牌增強(qiáng),設(shè)置JWT令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);

        services.setAccessTokenValiditySeconds(7200); //令牌默認(rèn)有效時(shí)間2小時(shí)
        services.setRefreshTokenValiditySeconds(259200); //刷新令牌默認(rèn)有效期3天
        return services;
    }

    /**
     * 設(shè)置授權(quán)碼模式的授權(quán)碼如何存取查剖,暫時(shí)采用內(nèi)存方式
     *
     * @return
     */
//    @Bean
//    public AuthorizationCodeServices authorizationCodeServices(){
//        return new InMemoryAuthorizationCodeServices();
//    }

    @Resource
    private AuthorizationCodeServices authorizationCodeServices;

    /**
     * 授權(quán)碼存儲(chǔ)到數(shù)據(jù)庫(kù)
     * @param dataSource
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource){
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    /**
     * 令牌訪問(wèn)端點(diǎn)配置
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)//認(rèn)證管理器
                .authorizationCodeServices(authorizationCodeServices)//授權(quán)碼服務(wù)
                .tokenServices(tokenServices()) //令牌管理服務(wù)(設(shè)置令牌存儲(chǔ)方式和令牌類(lèi)型JWT)
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    /**
     * 對(duì)授權(quán)端點(diǎn)接口的安全約束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") // /auth/token_key是公開(kāi)的
                .checkTokenAccess("permitAll()") // /auth/check_token是公開(kāi)的
                .allowFormAuthenticationForClients(); //允許表單認(rèn)證(申請(qǐng)令牌)
    }

}

2.2.6 TokenEndPoint钾虐,AuthorizationEndPoint,CheckTokenEndPoint

2.2.6.1 TokenEndPoint

客戶端post請(qǐng)求"/oauth/token"笋庄,驗(yàn)證用戶信息并獲取OAuth2AccessToken禾唁,必須先經(jīng)過(guò)client驗(yàn)證。這一步的最終目的是存儲(chǔ)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

這個(gè)一般只適用于authorization code模式荡短,客戶端請(qǐng)求authorization server中的/oauth/authorize(請(qǐng)求前先得登錄oauth server獲得authentication),驗(yàn)證client信息后根據(jù)redirect_uri請(qǐng)求重定向回client哆键,同時(shí)帶上code值掘托。client附帶code值再次向/oauth/token請(qǐng)求,返回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

當(dāng)采用RemoteTokenServices時(shí)闪盔,resouceServer無(wú)法自行驗(yàn)證access_token字符串是否正確弯院,遂遞交給另一個(gè)應(yīng)用程序中的authserver里CheckTokenEndpoint(/oauth/check_token)進(jìn)行檢驗(yàn),檢驗(yàn)結(jié)果返回給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 概述

異常處理規(guī)則:

  • 規(guī)則1. 如果異常是 AuthenticationException,使用 AuthenticationEntryPoint 處理
  • 規(guī)則2. 如果異常是 AccessDeniedException 且用戶是匿名用戶异赫,使用 AuthenticationEntryPoint 處理
  • 規(guī)則3. 如果異常是 AccessDeniedException 且用戶不是匿名用戶椅挣,如果否則交給 AccessDeniedHandler 處理。

3.2 源碼

3.2.1 ExceptionTranslationFilter

ExceptionTranslationFilter的doFilter

ExceptionTranslationFilter是個(gè)異常過(guò)濾器塔拳,用來(lái)處理在認(rèn)證授權(quán)過(guò)程中拋出的異常鼠证,在過(guò)濾器鏈中處于倒數(shù)第三的位置(這個(gè)filter后面分為是FilterSecurityInterceptor、SwitchUserFilter)靠抑,所以ExceptionTranslationFilter只能捕獲到后面兩個(gè)過(guò)濾器所拋出的異常量九。

ExceptionTranslationFilter后面的過(guò)濾器是FilterSecurityInterceptor。先上一張圖颂碧,如下圖1所示:

21580557-0fd084a033d2b022.png
  • 紅框1中的荠列,是調(diào)用Filter鏈中的后續(xù)Filter。
  • 如果圖1中的操作拋出異常载城,就會(huì)來(lái)到紅框2處弯予,判斷拋出的異常是否是AuthenticationException。
  • 如果拋出的異常不是AuthenticationException个曙,即紅框2的結(jié)果為null,那么就到紅框3處受楼,判斷是否是AccessDeniedException垦搬。
  • 如果拋出的異常是AuthenticationException或者時(shí)AccessDeniedException,那么執(zhí)行紅框4處的代碼艳汽。

ExceptionTranslationFilter的handleSpringSecurityException方法

下面來(lái)看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);   //保存當(dāng)前請(qǐng)求
        logger.debug("Calling Authentication entry point.");
        authenticationEntryPoint.commence(request, response, reason);
    }

}
  1. 如果拋出的異常是AuthenticationException猴贰,則執(zhí)行方法sendStartAuthentication
  2. 如果拋出的異常是AccessDeniedException,且從SecurityContextHolder.getContext().getAuthentication()得到的是AnonymousAuthenticationToken或者RememberMeAuthenticationToken河狐,那么執(zhí)行sendStartAuthentication
  3. 如果上面的第二點(diǎn)不滿足米绕,則執(zhí)行accessDeniedHandler的handle方法

在HttpSessionRequestCache 中會(huì)將本次請(qǐng)求的信息保存到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方法

調(diào)用sendStartAuthentication方法實(shí)現(xiàn)對(duì)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方法中完成對(duì)請(qǐng)求的重定向

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

自定義未登錄異常

如果未登錄,不希望跳轉(zhuǎn)到/login而是直接拋異巢鲆眨或跳轉(zhuǎn)到指定路徑栅干,可以通過(guò)以下兩步來(lái)實(shí)現(xiàn):

  1. 自定義類(lèi)實(shí)現(xiàn)AuthenticationEntryPoint接口,重寫(xiě)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,"未認(rèn)證的用戶:" + authException.getMessage());
                new DefaultRedirectStrategy().sendRedirect(request, response, "http://www.jd.com");
            }
        }
    
    }
    
  2. 在WebSecurityConfigurerAdapter繼承類(lèi)中指定異常處理類(lèi)為自定義類(lèi)碱鳞。

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    //跨域請(qǐng)求偽造防御失效
                    .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應(yīng)用中,spring security是一個(gè)filter踱蛀。而在filter內(nèi)部窿给,它又自建了一個(gè)filter chain(如果不用命名空間贵白,也可以自定義)。spring security按順序?qū)γ總€(gè)filter進(jìn)行處理崩泡。各filter之間有較大的差異性禁荒。與權(quán)限驗(yàn)證關(guān)系最密切的是FilterSecurityInterceptor。

FilterSecurityInterceptor認(rèn)證及驗(yàn)權(quán)流程:

21580557-91f104e63676e03d.png

FilterSecurityInterceptor的類(lèi)關(guān)系圖如下角撞。它使用AuthenticationManager做認(rèn)證(用戶是否已登錄)呛伴,使用AccessDecisionManager做驗(yàn)證(用戶是否有權(quán)限)。

21580557-c3d28217250cf5ee.png

ProviderManager是默認(rèn)的AuthenticationManager實(shí)現(xiàn)類(lèi)靴寂,它不直接進(jìn)行認(rèn)證磷蜀。而是采用組合模式,將認(rèn)證工作委托給AuthenticationProvider百炬。一般情況下褐隆,一組AuthenticationProvider有一個(gè)認(rèn)證成功,就被視為認(rèn)證成功剖踊。ProviderManager關(guān)系圖如下:

21580557-18e1a04bf1e402e4.png

AccessDecisionManager負(fù)責(zé)驗(yàn)證用戶是否有操作權(quán)限庶弃,它也是采用組合模式。security自帶的AccessDecisionManager實(shí)現(xiàn)類(lèi)有三種:AffirmativeBased只要有一個(gè)認(rèn)證處理器認(rèn)證通過(guò)就表示成功德澈;ConsensusBased采用的是多數(shù)原則歇攻;UnanimousBased采用一票否決制。

21580557-eb6101754772e091.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載梆造,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者缴守。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市镇辉,隨后出現(xiàn)的幾起案子屡穗,更是在濱河造成了極大的恐慌,老刑警劉巖忽肛,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件村砂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡屹逛,警方通過(guò)查閱死者的電腦和手機(jī)础废,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)罕模,“玉大人评腺,你說(shuō)我怎么就攤上這事∈缯疲” “怎么了歇僧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我诈悍,道長(zhǎng)祸轮,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任侥钳,我火速辦了婚禮适袜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舷夺。我一直安慰自己苦酱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布给猾。 她就那樣靜靜地躺著疫萤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪敢伸。 梳的紋絲不亂的頭發(fā)上扯饶,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音池颈,去河邊找鬼尾序。 笑死,一個(gè)胖子當(dāng)著我的面吹牛躯砰,可吹牛的內(nèi)容都是我干的每币。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼琢歇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼兰怠!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起李茫,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤揭保,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后涌矢,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡快骗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年娜庇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片方篮。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡名秀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出藕溅,到底是詐尸還是另有隱情匕得,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站汁掠,受9級(jí)特大地震影響略吨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜考阱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一翠忠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乞榨,春花似錦秽之、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鹦倚,卻和暖如春河质,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背申鱼。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工愤诱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捐友。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓淫半,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親匣砖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子科吭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容