Spring Security源碼解析篇介紹了Spring Security的原理,複習下幾個概念
- Principle GrantedAuthority Authentication AbstractAuthenticationToken UsernamePasswordAuthenticationToken
- AuthenticationManager AuthenticationProvider DaoAuthenticationProvider ProviderManager
- UserDetail CredentialsContainer UserDetailsService UserCache
- WebSecurityConfigurerAdapter WebSecurity HttpSecuirty FilterChainProxy
接下來介紹oauth2的相關概念。oauth2是建立在spring security基礎上的一套規範,並非框架。
主要角色有
- 授權服務器 AuthorizationServer 配置client,tokenStore,authenticationManager等
- 資源服務器 ResourceServer 配置HttpSecurity,哪些uri需要驗證;配置ResourceServerSecurityConfigurer,如設置tokenService
- 客戶端 client 包含clientid,secret
- 用戶 user 包含username,password
一般來說,資源服務器和授權服務器都是一個機構提供的,授權服務器給客戶端授權,客戶端再去請求資源服務器(授權服務器和資源服務器可以是一個應用程序也可以是兩個應用程序)。下面例子中的QQ既是資源服務器,也是授權服務器。
用戶想登錄csdn(客戶端),發現還沒有註冊csdn(客戶端)的賬號,剛好看見有一個通過QQ登錄的方式,遂輸入QQ的賬戶密碼,點擊授權。此時QQ服務器(授權服務器)對csdn(客戶端)和用戶密碼進行驗證,驗證成功後給csdn(客戶端)發放令牌,csdn憑藉令牌去獲取用戶的QQ頭像(資源服務器)等信息。
目錄
1. 好基友一輩子 OAuth2Authentication和OAuth2AccessToken
2. TokenGranter、TokenStore、TokenExtractor
2.4 ResourceServerTokenServices
3. ClientDetails ClientDetailsService
3.3 ClientDetailsServiceBuilder
4. 資源服務器配置 ResourceServerConfigurerAdapter
5. 授權服務器配置 AuthorizationServerConfigurerAdapter
6.TokenEndPoint,AuthorizationEndPoint,CheckTokenEndPoint
1. 好基友一輩子 OAuth2Authentication和OAuth2AccessToken
1.1 OAuth2Authentication
OAuth2Authentication顧名思義是Authentication的子類,存儲用戶信息和客戶端信息,但多了2個屬性
private final OAuth2Request storedRequest; private final Authentication userAuthentication;
這樣OAuth2Authentication可以存儲2個Authentication,一個給client(必要),一個給user(只是有些授權方式需要)。除此之外同樣有principle,credentials,authorities,details,authenticated等屬性。
OAuth2Request 用於存儲request中的Authentication信息(grantType,responseType,resouceId,clientId,scope等),這裏就引出了OAuth2 中的三大request。
1.2 BaseRequest
BaseRequest是抽象類,有3個屬性:clienId、scope和requestParameters。
abstract class BaseRequest implements Serializable {
private String clientId;
private Set<String> scope = new HashSet<String>();
private Map<String, String> requestParameters = Collections
.unmodifiableMap(new HashMap<String, String>());
/** setter,getter */
}
繼承類見下圖,3個類都在OAuth2包中,這些request都會存在於OAuth2的驗證流程中,用於傳遞clientId,scope,requestParameters等屬性,與HttpServletRequest有本質區別!
1.2.1 AuthorizationRequest
向授權服務器AuthorizationEndPoint (/oauth/authorize)請求授權,AuthorizationRequest作爲載體存儲state,redirect_uri等參數,生命週期很短且不能長時間存儲信息,可用OAuth2Request代替存儲信息。
public class AuthorizationRequest extends BaseRequest implements Serializable {
// 用戶同意授權傳遞的參數,不可改變
private Map<String, String> approvalParameters = Collections.unmodifiableMap(new HashMap<String, String>());
// 客戶端發送出的狀態信息,從授權服務器返回的狀態應該不變纔對
private String state;
// 返回類型集合
private Set<String> responseTypes = new HashSet<String>();
// resource ids 可變
private Set<String> resourceIds = new HashSet<String>();
// 授權的權限
private Collection<? extends GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
// 終端用戶是否同意該request發送
private boolean approved = false;
// 重定向uri
private String redirectUri;
// 額外的屬性
private Map<String, Serializable> extensions = new HashMap<String, Serializable>();
// 持久化到OAuth2Request
public OAuth2Request createOAuth2Request() {
return new OAuth2Request(getRequestParameters(), getClientId(), getAuthorities(), isApproved(), getScope(), getResourceIds(), getRedirectUri(), getResponseTypes(), getExtensions());
}
// setter,getter
}
1.2.2 TokenRequest
向授權服務器TokenEndPoint(/oauth/token)發送請求獲得access_token時,tokenRequest作爲載體存儲請求中grantType等參數。常和tokenGranter.grant(grantType,tokenRequest)結合起來使用。
TokenRequest攜帶了新屬性grantType,和方法createOAuth2Request(用於持久化)
private String grantType;
public OAuth2Request createOAuth2Request(ClientDetails client) {
Map<String, String> requestParameters = getRequestParameters();
HashMap<String, String> modifiable = new HashMap<String, String>(requestParameters);
// Remove password if present to prevent leaks
modifiable.remove("password");
modifiable.remove("client_secret");
// Add grant type so it can be retrieved from OAuth2Request
modifiable.put("grant_type", grantType);
return new OAuth2Request(modifiable, client.getClientId(), client.getAuthorities(), true, this.getScope(),
client.getResourceIds(), null, null, null);
}
1.2.3 OAuth2Request
用來存儲TokenRequest或者AuthorizationRequest的信息,只有構造方法和getter方法,不提供setter方法。它作爲OAuth2Authentication的一個屬性(StoredRequest),存儲request中的authentication信息(grantType,approved,responseTypes)。
1.2.4 OAuth2RequestFactory
工廠類生成OAuth2Request、TokenRequest、AuthenticationRequest。
public interface OAuth2RequestFactory {
/**
* 從request請求參數中獲取clientId,scope,state
* clientDetailsService loadClientByClientId(clientId) 獲取clientDetails resourcesId Authorities
* 根據以上信息生成AuthenticationRequest
*/
AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters);
/**
* AuthorizationRequest request 有生成OAuth2Request的方法
* request.createOAuth2Request()
*/
OAuth2Request createOAuth2Request(AuthorizationRequest request);
OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest);
TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient);
TokenRequest createTokenRequest(AuthorizationRequest authorizationRequest, String grantType);
}
1.3 OAuth2AccessToken
OAuth2AccessToken是一個接口,提供安全令牌token的基本信息,不包含用戶信息,僅包含一些靜態屬性(scope,tokenType,expires_in等)和getter方法,如String getScope,OAuth2RefreshToken getRefreshToken,String getTokenType,String getValue()等。TokenGranter.grant()返回的值即OAuth2AccessToken。
OAuth2AccessToken和OAuth2Authentication是一對好基友,誰要先走誰是狗!!!
TokenStore同時存儲OAuth2AccessToken和OAuth2Authentication,也可根據OAuth2Authentication中的OAuth2Request信息可獲取對應的OAuth2AccessToken。
DefaultTokenServices有如下方法,都可以通過一個獲得另一個的值
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication)
OAuth2Authentication loadAuthentication(String accessTokenValue)
// 當tokenStore是jdbcTokenStore,表示從數據庫中根據OAuth2Authentication獲取OAuth2AccessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
DefaultOAuth2AccessToken是OAuth2AccessToken的實現類,多了構造方法,setter方法和OAuth2AccessToken valueOf(Map<String,Object> tokenParams)。經過json轉換後就是我們常見的access_token對象,如下所示。
{
"access_token": "1e95d081-0048-4397-a081-c76f7823fe54",
"token_type": "bearer",
"refresh_token": "7f6db28b-50dc-40a2-b381-3e356e30af2b",
"expires_in": 1799,
"scope": "read write"
}
OAuth2RefreshToken是接口,只有String getValue()方法。
DefaultOAuth2RefreshToken是OAuth2RefreshToken的實現類。
2. TokenGranter、TokenStore、TokenExtractor
2.1 TokenGranter(/oauth/token)
一般在用戶請求TokenEndPoints中的路徑/oauth/token時,根據請求參數中的grantType,username,password,client_id,client_secret等,調用TokenGranter給用戶分發OAuth2AccessToken。
根據grantType(password,authorization-code)和TokenRequest(requestParameters,clientId,grantType)授予人OAuth2AccessToken令牌。
public interface TokenGranter {
OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}
回憶下TokenRequest包含了基本信息clientId,scope,requestParameters,grantType等。根據tokenRequest獲取OAuth2Request,初始化獲得OAuth2Authentication,再去數據庫裏找oauth2accesstoken,如果有則直接返回,如果沒有則創建新的oauth2accesstoken,並且和OAuth2Authentication一起存入數據庫中。
2.1.1 AbstractTokenGranter
TokenGranter抽象繼承類AbstractTokenGranter,實現了grant方法,源碼如下。
執行順序爲根據tokenRequest====》clientId ====》clientDetails====》OAuth2Authentication(getOAuth2Authentication(client,tokenRequest))====》OAuth2AccessToken(tokenService.createAccessToken)
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
logger.debug("Getting access token for: " + clientId);
// getAccessToken 先獲得OAuth2Authentication,再創建OAuth2AccessToken
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
// AbstractTokenGranter的繼承類重寫了該方法
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, null);
}
實現AbstractTokenGranter的類有5種。其中如果用password的方式進行驗證,那麼TokenGranter類型是ResourceOwnerPasswordTokenGranter,該類中重寫了getOAuth2Authentication方法,裏面調用了authenticationManager.manage()方法。
用戶可自行定義granter類繼承AbstractTokenGranter,重寫getOAuth2Authentication()方法,並將該granter類添加至CompositeTokenGranter中。
TokenGranter的繼承類如下圖所示
2.1.2 CompositeTokenGranter
有繼承類CompositeTokenGranter,包含List<TokenGranter> tokenGranters屬性,grant方法是遍歷tokenGranters進行逐一grant,只要有一個有返回值就返回。
public class CompositeTokenGranter implements TokenGranter {
private final List<TokenGranter> tokenGranters;
public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
}
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 TokenStore(/oauth/token)
一般在TokenGranter執行grant方法完畢後,將OAuth2AccessToken和OAuth2Authentication存儲起來,方便以後根據其中一個查詢另外一個(如根據access_token查詢獲得OAuth2Authentication)。
存儲OAuth2AccessToken和OAuth2Authentication(比Authentication多了兩個屬性storedRequest,userAuthentication),存儲方法如下。還有各種read,remove方法
void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
實現類有5類,其中JdbcTokenStore是通過連接數據庫來存儲OAuth2AccessToken的,這也是我們一般存儲token的方法。條件是數據庫裏的表結構必須按照標準建立。(JwtTokenStore不存儲token和authentication,直接根據token解析獲得authentication)
oauth_access_token表結構如下,可見表裏存儲了OAuth2AccessToken和OAuth2Authentication兩個對象,值得注意的是token_id並不等於OAuth2AccessToken.getValue(),value經過MD5加密後纔是token_id。同理authentication_id 和 refresh_token也是經過加密轉換存儲的。
第一次獲得token,直接存入數據庫表裏。
如果重複post請求/oauth/token, JdbcTokenStore會先判斷表中是否已有該用戶的token,如果有先刪除,再添加。
2.3 TokenExtractor (OAuth2AuthentiactionProcessingFilter)
用戶攜帶token訪問資源,過濾器進行到OAuth2AuthentiactionProcessingFilter時,從HttpServletRequest中獲取access_token(可以從header或者params中獲取),拼接成PreAuthenticatedAuthenticationToken(Authentication子類)
Authentication extract(HttpServletRequest request);
BearerTokenExtractor是它的實現類,實現了從request中獲取Authentication的方法。
1.header中 Authentication:Bearer xxxxxxxx--xxx
2.request parameters中 access_token=xxxx-xxxx-xxxx
protected String extractToken(HttpServletRequest request) {
// 1.直接從header中提取key爲Authentication,value是以Bearer 開頭的header
// 如Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
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.");
// 2.如果header中不包含,則從param中獲取"access_token"對應的值
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;
}
2.4 ResourceServerTokenServices
兩個方法。用戶攜access_token訪問資源服務器時,資源服務器會將該字符串進行解析,獲得OAuth2Authentication和OAuth2AccessToken。
loadAuthentication根據字符串accessToken獲得OAuth2Authentication;
readAccessToken根據字符串accessToken獲得OAuth2AccessToken。
public interface ResourceServerTokenServices {
/**
* Load the credentials for the specified access token.
*
* @param accessToken The access token value.
* @return The authentication for the access token.
* @throws AuthenticationException If the access token is expired
* @throws InvalidTokenException if the token isn't valid
*/
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
/**
* Retrieve the full access token details from just the value.
*
* @param accessToken the token value
* @return the full access token with client id etc.
*/
OAuth2AccessToken readAccessToken(String accessToken);
}
有兩重要繼承類,DefaultTokenServices和RemoteTokenServices
2.4.1 DefaultTokenServices
實現了兩個接口AuthorizationServerTokenServices和ResourceServerTokenServices。常在granter().grant()方法中調用tokenServices.createAccessToken()方法獲得oauth2accesstoken。
其中重要方法createAccessToken(OAuth2Authentication oauth2)源碼如下
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 如果數據庫中已存了authentication和accesstoken,則直接提取
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;
}
}
// Only create a new refresh token if there wasn't an existing one
// associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in
// the case that the old access token
// expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has
// expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
// 第一次創建access_token,並且存儲到數據庫中
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;
}
2.4.2 RemoteTokenServices
當授權服務和資源服務不在一個應用程序的時候,資源服務可以把傳遞來的access_token遞交給授權服務的/oauth/check_token進行驗證,而資源服務自己無需去連接數據庫驗證access_token,這時就用到了RemoteTokenServices。
loadAuthentication方法,設置head表頭Authorization 存儲clientId和clientSecret信息,請求參數包含access_token字符串,向AuthServer的CheckTokenEndpoint (/oauth/check_token)發送請求,返回驗證結果map(包含clientId,grantType,scope,username等信息),拼接成OAuth2Authentication。
重要!!!
AuthServer需要配置checkTokenAcess,否則默認爲“denyAll()”,請求訪問/oauth/check_token會提示沒權限。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.realm(QQ_RESOURCE_ID).allowFormAuthenticationForClients();
// 訪問/oauth/check_token 需要client驗證
oauthServer.checkTokenAccess("isAuthenticated()");、
// 也可配置訪問/oauth/check_token無需驗證
// oauthServer.checkTokenAccess("permitAll()");
}
不支持readAccessToken方法。
public class RemoteTokenServices implements ResourceServerTokenServices {
protected final Log logger = LogFactory.getLog(getClass());
private RestOperations restTemplate;
private String checkTokenEndpointUrl;
private String clientId;
private String clientSecret;
private String tokenName = "token";
private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
@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")) {
logger.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
return tokenConverter.extractAuthentication(map);
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
}
3. ClientDetails ClientDetailsService
這兩個概念簡單的有些可怕,完全就是UserDetails和UserDetailsService的翻版。一個是對應user,一個是對應client。
client需要事先註冊到授權服務器,這樣授權服務器會根據client的授權請求獲取clientId,secret等信息,進行驗證後返回token。
3.1 ClientDetails
client的信息,存於授權服務器端,這樣只需要知道客戶端的clientId,就可以獲取到客戶端能訪問哪些資源,是否需要密碼,是否限制了scope,擁有的權限等等。
public interface ClientDetails extends Serializable {
String getClientId();
// client能訪問的資源id
Set<String> getResourceIds();
// 驗證client是否需要密碼
boolean isSecretRequired();
String getClientSecret();
// client是否限制了scope
boolean isScoped();
// scope集合
Set<String> getScope();
// 根據哪些grantType驗證通過client
Set<String> getAuthorizedGrantTypes();
// 註冊成功後跳轉的uri
Set<String> getRegisteredRedirectUri();
// client擁有的權限
Collection<GrantedAuthority> getAuthorities();
// client的token時效
Integer getAccessTokenValiditySeconds();
// client的refreshToken時效
Integer getRefreshTokenValiditySeconds();
// true:默認自動授權;false:需要用戶確定才能授權
boolean isAutoApprove(String scope);
// 額外的信息
Map<String, Object> getAdditionalInformation();
}
3.2 ClientDetailsService
根據clientId獲取clientDetails
public interface ClientDetailsService {
/**
* Load a client by the client id. This method must not return null.
*
* @param clientId The client id.
* @return The client details (never null).
* @throws ClientRegistrationException If the client account is locked, expired, disabled, or invalid for any other reason.
*/
ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException;
}
有兩個子類 InMemoryClientDetailsService(內存) 和 JdbcClientDetailsService(數據庫,OAUTH_CLIENT_DETAILS、OAUTH_CLIENT_TOKEN表等)。說白了就是一個是把ClientDetails存內存裏,一個存數據庫裏(oauth_client_details表)。
一般在AuthServer中配置ClientDetailsServiceConfigurer。
1、配置JdbcDetailsService
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 配置客戶端 a configurer that defines the client details service.
* Client details can be initialized, or you can just refer to an existing store.
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
2、配置InMemoryClientDetailsService
@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");
// @formatter:on
}
3.3 ClientDetailsServiceBuilder
創建InMemoryClientDetailsService或者JdbcClientDetailsService,有內部類ClientBuilder。
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;
}
}
}
4. 資源服務器配置 ResourceServerConfigurerAdapter
配置哪些路徑需要認證後才能訪問,哪些不需要。自然就聯想到了HttpSecurity(配置HttpSecurity就相當於配置了不同uri對應的filters)。
Spring Security中我們是這樣配置WebSecurityConfigurerAdapter的。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()//所有請求必須登陸後訪問
.and().httpBasic()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/index")
.failureUrl("/login?error")
.permitAll()//登錄界面,錯誤界面可以直接訪問
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login")
.permitAll().and().rememberMe();//註銷請求可直接訪問
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER", "ADMIN");
}
}
作爲資源服務器ResourceServerConfigurerAdapter,需要和@EnableResourceServer搭配,然後和上面一樣需配置HttpSecurity就好了。還能配置ResourceServerSecurityConfigurer,設置tokenService等
/**
* 配置資源服務器
*/
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.logout()
.logoutUrl("/oauth/logout")
.logoutSuccessHandler(customLogoutSuccessHandler)
.and()
.authorizeRequests()
// hello路徑允許直接訪問
.antMatchers("/hello/").permitAll()
// secure路徑需要驗證後才能訪問
.antMatchers("/secure/**").authenticated();
}
// 遠程連接authServer服務
@Autowired
public RemoteTokenServices remoteTokenServices;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(remoteTokenServices);
}
}
5. 授權服務器配置 AuthorizationServerConfigurerAdapter
註冊client信息,可以同時配置多個不同類型的client。
/**
* 配置認證服務器 @EnableAuthorizationServer自動註冊到spring context中
*/
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements EnvironmentAware {
private static final String ENV_OAUTH = "authentication.oauth.";
private static final String PROP_CLIENTID = "clientid";
private static final String PROP_SECRET = "secret";
private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";
private RelaxedPropertyResolver propertyResolver;
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
/**
* 可以設置tokenStore,tokenGranter,authenticationManager,requestFactory等接口使用什麼繼承類,但一般沿用默認的就好了
* 如果使用的是密碼方式授權,則必須設置authenticationManager
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
}
/**
* 註冊clients到授權服務器,這裏是註冊到內存中,且配置了scopes,authorities等信息
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(propertyResolver.getProperty(PROP_CLIENTID))
.scopes("read", "write")
.authorities(Authorities.ROLE_ADMIN.name(), Authorities.ROLE_USER.name())
.authorizedGrantTypes("password", "refresh_token")
.secret(propertyResolver.getProperty(PROP_SECRET))
// 給客戶端的token時效爲1800秒
.accessTokenValiditySeconds(propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer.class, 1800));
}
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
}
}
6.TokenEndPoint,AuthorizationEndPoint,CheckTokenEndPoint
6.1 TokenEndPoint
客戶端post請求"/oauth/token",驗證用戶信息並獲取OAuth2AccessToken,必須先經過client驗證。這一步的最終目的是存儲OAuth2AccessToken+OAuth2Authentication並返回OAuth2AccessToken。
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
...
// AuthorizationServerEndpointsConfigurer
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
6.2 AuthorizationEndPoint
這個一般只適用於authorization code模式,客戶端請求authorization server中的/oauth/authorize(請求前先得登錄oauth server獲得authentication),驗證client信息後根據redirect_uri請求重定向回client,同時帶上code值。client附帶code值再次向/oauth/token請求,返回accesstoken。
具體流程將在下章中介紹,這裏只引出相關概念。
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
// 生成code值並返回
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the session
// for approveOrDeny to use. That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
6.3 CheckTokenEndpoint
當採用RemoteTokenServices時,resouceServer無法自行驗證access_token字符串是否正確,遂遞交給另一個應用程序中的authserver裏CheckTokenEndpoint(/oauth/check_token)進行檢驗,檢驗結果返回給resourceServer。
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);
return response;
}
7.總結
複習下這章講到的知識點。
- 四大角色: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)