二喧务、密碼模式源碼
2.1 概述
訪問(wèn)/oauth/token
會(huì)經(jīng)過(guò)攔截器的順序ClientCredentialsTokenEndpointFilter
和BasicAuthenticationFilter
,ClientCredentialsTokenEndpointFilter
從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)AuthorizationRequest、TokenRequest翘鸭、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汉匙、TokenRequest、OAuth2Request生蚁。
-
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種书斜。
其中如果用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ǔ)OAuth2AccessToken和OAuth2Authentication(比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)建立。
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,如果有先刪除位他,再添加氛濒。
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的方法骗奖。
- header中 Authentication:Bearer xxxxxxxx--xxx
- 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);
}
其中重要方法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所示:
- 紅框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);
}
}
- 如果拋出的異常是AuthenticationException猴贰,則執(zhí)行方法sendStartAuthentication
- 如果拋出的異常是AccessDeniedException,且從SecurityContextHolder.getContext().getAuthentication()得到的是AnonymousAuthenticationToken或者RememberMeAuthenticationToken河狐,那么執(zhí)行sendStartAuthentication
- 如果上面的第二點(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):
-
自定義類(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"); } } }
-
在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)流程:
FilterSecurityInterceptor的類(lèi)關(guān)系圖如下角撞。它使用AuthenticationManager做認(rèn)證(用戶是否已登錄)呛伴,使用AccessDecisionManager做驗(yàn)證(用戶是否有權(quán)限)。
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)系圖如下:
AccessDecisionManager負(fù)責(zé)驗(yàn)證用戶是否有操作權(quán)限庶弃,它也是采用組合模式。security自帶的AccessDecisionManager實(shí)現(xiàn)類(lèi)有三種:AffirmativeBased只要有一個(gè)認(rèn)證處理器認(rèn)證通過(guò)就表示成功德澈;ConsensusBased采用的是多數(shù)原則歇攻;UnanimousBased采用一票否決制。