本文將從基於spring security oauth2.0流程中的(1)獲取token的認證、受權以及(2)帶token去訪問受保護資源的流程進行源碼解析。前端
瞭解spring security 的應該知道oauth2.0協議提供了四種受權模式
spring
以client模式爲例,獲取token的地址爲:數據庫
http://localhost:8080/oauth/token?client_id=client_1&client_secret=123456&scope=select&grant_type=client_credentials
複製代碼
其餘三種的獲取token的url也爲/oauth/token,只是後面的參數不一樣。
可想而知,想要了解如何獲取token,/oauth/token即爲入口。 從這個入口開始分析,spring security oauth2內部是如何生成token的。
express
首先開啓debug信息:json
logging:
level:
org.springframework: DEBUG
複製代碼
在搭建好的認證服務中,調用上述client模式下獲取token的url,查看打印出來的結果。安全
截取關鍵的打印結果,能夠看出大概的流程,在請求到達/oauth/token以前通過了ClientCredentialsTokenEndpointFilter這個過濾器。 ClientCredentialsTokenEndpointFilter過濾器是做爲client模式獲取token的入口,其餘模式對應的入口過濾器會有不一樣,好比password模式的爲ResourceOwnerPasswordTokenGranter。
bash
ClientCredentialsTokenEndpointFilter的關鍵方法以下:服務器
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
...
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
...
clientId = clientId.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
return this.getAuthenticationManager().authenticate(authRequest);
}
複製代碼
從request中獲取到client_id與client_secret,Spring Security將獲取到的用戶名和密碼封裝成UsernamePasswordAuthenticationToken做爲身份標識(client模式下取的爲client_id對應username、client_secret對應password來使用)。
將上述產生的Authentication對象使用容器中的頂級身份管理器AuthenticationManager去進行身份認證。app
該接口最主要的做用是用來作驗證,這個接口只有一個方法:框架
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
複製代碼
其中authenticate()方法運行後可能會有三種狀況:
ProviderManager是上面的AuthenticationManager最多見的實現,它不本身處理驗證,而是將驗證委託給其所配置的AuthenticationProvider列表,而後會依次調用每個 AuthenticationProvider進行認證,這個過程當中只要有一個AuthenticationProvider驗證成功,就不會再繼續作更多驗證,會直接以該認證結果做爲ProviderManager的認證結果。
AuthenticationProvider列表在ProviderManager中以一個List(泛型:AuthenticationProvider)存在,循環該List去完成上述的認證過程,認證相關核心代碼以下:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...
private List<AuthenticationProvider> providers = Collections.emptyList();
...
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
...
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
複製代碼
AuthenticationProvider的經常使用實現類則是DaoAuthenticationProvider,認證中所使用到的用戶信息的獲取是經過類內部聚合了UserDetailsService接口,UserDetailsService接口是獲取用戶詳細信息的最終接口。
UserDetailsService做爲獲取用戶信息的接口,其中只有一個方法
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
複製代碼
該接口的實現類經常使用的分爲三個
綜上所述,整個認證流程可分爲以下幾步:
認證相關UML類圖
前面的兩個ClientCredentialsTokenEndpointFilter和DaoAuthenticationProvider能夠理解爲一些前置校驗,和身份封裝。 基本client模式,通過ClientCredentialsTokenEndpointFilter以後,身份信息已經獲得了AuthenticationManager的驗證。接着便到達了TokenEndpoint。
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
...
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
...
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
...
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
...
return getResponse(token);
}
private TokenGranter tokenGranter;
}
複製代碼
拿掉了一些校驗代碼以後,真正的/oauth/token端點暴露在了咱們眼前,其中方法參數中的Principal通過以前的過濾器,已經被填充了相關的信息,方法的內部是經過TokenGranter來實現頒發token。
UML類圖體現的TokenGranter接口的設計以下:
TokenGranter的設計思路是使用CompositeTokenGranter管理一個List列表,每一種grantType對應一個具體的真正受權者,在受權過程當中能夠發現CompositeTokenGranter 內部循環調用五種TokenGranter實現類的grant方法,而granter內部則是經過grantType來區分走哪一種受權類型。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;
}
}
複製代碼
五種受權類型分別是:
在客戶端(client)模式下token是如何產生的,則須要繼續看5種受權的抽象類:AbstractTokenGranter。
public abstract class AbstractTokenGranter implements TokenGranter {
protected final Log logger = LogFactory.getLog(getClass());
//與token相關的service,重點
private final AuthorizationServerTokenServices tokenServices;
private final ClientDetailsService clientDetailsService;
private final OAuth2RequestFactory requestFactory;
private final String grantType;
...
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
...
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
logger.debug("Getting access token for: " + clientId);
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, null);
}
...
}
複製代碼
能夠經過上述源碼看到token的生成是調用了AuthorizationServerTokenServices的createAccessToken方法,在這裏針對AuthorizationServerTokenServices進行解析。
public interface AuthorizationServerTokenServices {
//建立token
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
//刷新token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
throws AuthenticationException;
//獲取token
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
}
複製代碼
在該接口的惟一實現類DefaultTokenServices中,能夠看到token是如何生成的,而且能夠了解到框架對token進行哪些信息的關聯。
@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;
}
}
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}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;
}
複製代碼
根據上述可大致知道AuthorizationServerTokenServices類,他提供了建立token,刷新token,獲取token的實現。在建立token時,他會調用tokenStore對產生的token和相關信息存儲到對應的實現類中,能夠是Redis,數據庫,內存,jwt。
獲取到token後,會拿token去請求受限的資源,接下來分析下資源認證的流程是如何運轉的。
實現資源服務器,須要繼承ResourceServerConfigurerAdapter,咱們便從ResourceServerConfigurerAdapter做爲入口進行拓展。
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
@Override
public void configure(ResourceServerSecurityConfigurer resources <1> ) throws Exception {
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
複製代碼
咱們能夠看到其實現了ResourceServerConfigurer接口,內部關聯了ResourceServerSecurityConfigurer和HttpSecurity。前者與資源安全配置相關,後者與http安全配置相關,接下來繼續看ResourceServerSecurityConfigurer這個類。
核心的代碼以下:
public void configure(HttpSecurity http) throws Exception {
AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();// <1>注意點
resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);// <2>注意點
if (eventPublisher != null) {
resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
}
if (tokenExtractor != null) {
resourcesServerFilter.setTokenExtractor(tokenExtractor);//<3>注意點
}
resourcesServerFilter = postProcess(resourcesServerFilter);
resourcesServerFilter.setStateless(stateless);
// @formatter:off
http
.authorizeRequests().expressionHandler(expressionHandler)
.and()
.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)//<4>注意點
.authenticationEntryPoint(authenticationEntryPoint);
// @formatter:on
}
複製代碼
這段是oauth2與HttpSecurity相關的核心配置,其中不少注意點:
在訪問受限資源的時候咱們會在請求中攜帶accessToken,好比:
http://localhost:8080/user/getUserInfo?access_token=950a7cc9-5a8a-42c9-a693-40e817b1a4b0
複製代碼
攜帶它進行訪問,會進入OAuth2AuthenticationProcessingFilter之中,其核心代碼以下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain){
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
//經過tokenExtractor從ServletRequest中獲取出token
Authentication authentication = tokenExtractor.extract(request);
if (authentication == null) {
...
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
//認證身份
Authentication authResult = authenticationManager.authenticate(authentication);
...
eventPublisher.publishAuthenticationSuccess(authResult);
//將身份信息綁定到SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
catch (OAuth2Exception failed) {
...
return;
}
chain.doFilter(request, response);
}
複製代碼
整個filter中就是對資源訪問認證的關鍵代碼,其中涉及到了兩個關鍵的類TokenExtractor,AuthenticationManager。AuthenticationManager在上述文章裏也有提到,是頂級身份認證接口。對於TokenExtractor稍後描述。
攜帶access_token一定得通過身份認證,通過身份認證就須要使用到AuthenticationManager接口,可是資源訪問身份認證時,AuthenticationManager的實現類並非經常使用實現類ProviderManager,而是OAuth2AuthenticationManager。
這裏只須要記住的是:OAuth2AuthenticationManager是與token認證相關的,而不是與獲取token密切相關的。 其判別身份的關鍵代碼以下:public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
String token = (String) authentication.getPrincipal();
//這裏是關鍵,藉助tokenServices根據token加載身份信息
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
...
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
...
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
複製代碼
經過上述源碼能夠看到,根據token加載身份信息是使用了tokenServices(ResourceServerTokenServices),獲取到身份信息後進行認證。這裏說明下,tokenServices分爲兩類,一個是用在AuthenticationServer端(認證受權端):
public interface AuthorizationServerTokenServices {
//建立token
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
//刷新token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
throws AuthenticationException;
//獲取token
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
}
複製代碼
在ResourceServer端(資源服務端)有本身的tokenServices接口:
public interface ResourceServerTokenServices {
//根據accessToken加載客戶端信息
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
//根據accessToken獲取完整的訪問令牌詳細信息。
OAuth2AccessToken readAccessToken(String accessToken);
}
複製代碼
ResourceServerTokenServices的內部加載邏輯與AuthorizationServerTokenServices基本相似,此處不展開介紹。
這個接口只有一個實現類,主要用於從request裏獲取access_token。
public class BearerTokenExtractor implements TokenExtractor {
private final static Log logger = LogFactory.getLog(BearerTokenExtractor.class);
@Override
public Authentication extract(HttpServletRequest request) {
String tokenValue = extractToken(request);
if (tokenValue != null) {
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
return authentication;
}
return null;
}
protected String extractToken(HttpServletRequest request) {
// first check the header...
String token = extractHeaderToken(request);
// bearer type allows a request parameter as well
if (token == null) {
...
//從requestParameter中獲取token
}
return token;
}
/**
* Extract the OAuth bearer token from a header.
*/
protected String extractHeaderToken(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders("Authorization");
while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
...
//從Header中獲取token
}
return null;
}
}
複製代碼
它的做用在於分離出請求中包含的token。咱們可使用多種方式攜帶token。
http://localhost:8080/user/getUser
Header:
Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
複製代碼
http://localhost:8080/user/getUser?access_token=f732723d-af7f-41bb-bd06-2636ab2be135
複製代碼
http://localhost:8080/user/getUser
form param:
access_token=f732723d-af7f-41bb-bd06-2636ab2be135
複製代碼
這裏格外說下自定義異常處理,若是想要重寫異常機制,能夠直接替換掉相關的Handler,如權限相關的AccessDeniedHandler。具體的配置應該在@EnableResourceServer中被覆蓋。
好比下面的例子:
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
response.setHeader("Content-Type", "application/json;charset=UTF-8");
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("data", null);
jsonObject.put("code", "401");
jsonObject.put("msg", "權限不足");
response.getWriter().write(jsonObject.toJSONString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製代碼
建立一個異常類,去實現AccessDeniedHandler接口,重寫handle方法,加入本身想要的異常封裝。
@Component
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws ServletException {
Throwable cause = authException.getCause();
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", "401");
jsonObject.put("data", "");
response.setStatus(HttpStatus.OK.value());
response.setHeader("Content-Type", "application/json;charset=UTF-8");
try {
if(cause instanceof InvalidTokenException) {
jsonObject.put("msg", "token格式非法或失效");
response.getWriter().write(jsonObject.toJSONString());
}else{
jsonObject.put("msg", "token缺失");
response.getWriter().write(jsonObject.toJSONString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製代碼
建立異常類,去實現AuthenticationEntryPoint,重寫commence方法,自定義異常操做。
最後在資源服務器配置上,加入以下配置:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
@Autowired
private TokenStore tokenStore;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint;
@Override
public void configure(HttpSecurity http) throws Exception {
...
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
//加入自定義異常處理
resources.authenticationEntryPoint(authExceptionEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler);
}
}
複製代碼
這樣在請求資源時,出現如上異常時,會拋出自定義的異常格式,方便前端小夥伴統一解析返回結果。