這幾天一直在研究oauth2協(xié)議,打算按照oauth2協(xié)議做一個認證服務,使用了spring security oauth2作為工具搭建了認證服務器和資源服務器。這篇博客還不會告知如何搭建這兩個服務器,我們先來簡單地了解一下oauth2的認證流程,當前主要會側重講授權碼模式。關心Spring security核心Filter創(chuàng)建和工作原理的,可以查看關于spring security中Filter的創(chuàng)建和工作原理
1.基本認證流程

2.spring security oauth2是如何實現(xiàn)整個認證流程的
這個問題確實有點難度,從網(wǎng)上查資料說是過濾器實現(xiàn)的,好吧,既然是過濾器實現(xiàn)的,那咱們開始從過濾器找線索。
首先小伙伴要知道Filter并不屬于spring,而是屬于tomcat,咱們可以從ApplicationFilterChain這個類入手,啟動認證服務器,啟動資源服務器,對資源服務器中ApplicationFilterChain的doFilter方法打上斷點,開始通過客戶端發(fā)送請求訪問資源服務器,請求會進資源服務器,重點內容在下面這個方法里面:
internalDoFilter(request,response);
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
上面的方法就是在調用過濾器鏈,通過debug可以觀察到有以下幾個過濾器:

仔細看一下,有兩個很明顯的過濾器,一個是OAuth2ClientContextFilter,springSecurityFilterChain,而且springSecurityFilterChain還是一個代理對象,這兩個過濾器肯定和spring security oauth2有關系。
OAuth2ClientContextFilter
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 記錄當前地址(currentUri)到HttpServletRequest
request.setAttribute(CURRENT_URI, calculateCurrentUri(request));
try {
// 調用下一個過濾器
chain.doFilter(servletRequest, servletResponse);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// 捕獲異常,根據(jù)對應的異常發(fā)起重定向請求
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
.getFirstThrowableOfType(
UserRedirectRequiredException.class, causeChain);
if (redirect != null) {
// 這個重定向會讓客戶端去請求認證服務器,
//也就是認證流程示意圖中第2步重定向的操作
//會重定向到認證服務器的/oauth/authorize
redirectUser(redirect, request, response);
} else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new NestedServletException("Unhandled exception", ex);
}
}
}
根據(jù)上面的注釋,發(fā)現(xiàn)其實這個過濾器做的事情,就是重定向到認證服務器。
springSecurityFilterChain
因為這個對象是一個代理對象,但是我們可以找到它的被代理類,從ApplicationFilterChain中debug,咱們可以找到FilterChainProxy,代碼一直在執(zhí)行內部類VirtualFilterChain的doFilter方法:
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}
重點放在additionalFilters這個對象上面:

上圖就是spring security相關的Filters,也可以加入自定義Filter。
WebAsyncManagerIntegrationFilter
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 從請求中封裝一個WebAsyncManager
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
// 檢查是否存在一個SecurityContextCallableProcessingInterceptor
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
//不存在的話,就創(chuàng)建一個設置進去
if (securityProcessingInterceptor == null) {
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
new SecurityContextCallableProcessingInterceptor());
}
// 調用下一個過濾器
filterChain.doFilter(request, response);
}
這個過濾器的功能就是注冊一個SecurityContextCallableProcessingInterceptor,暫時不深究這個攔截器。
SecurityContextPersistenceFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
// 將request和response封裝
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
// 從session中取出SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 將SecurityContext放到SecurityContextHolder中,方便后續(xù)過濾器使用
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 執(zhí)行后續(xù)過濾器
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
// 執(zhí)行完后續(xù)過濾器后,取出SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// 清除SecurityContextHolder
SecurityContextHolder.clearContext();
// 將SecurityContextHolder重新設置回session中,
// 因為在執(zhí)行后續(xù)過濾器的時候,有可能發(fā)生了變化
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
上述注釋可以說明,其實SecurityContextPersistenceFilter就是做了SecurityContext的更新操作。
HeaderWriterFilter
/**
* Filter implementation to add headers to the current response. Can be useful to add
* certain headers which enable browser protection. Like X-Frame-Options, X-XSS-Protection
* and X-Content-Type-Options.
*
* @author Marten Deinum
* @author Josh Cummings
* @since 3.2
*
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,
response, this.headerWriters);
HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request,
headerWriterResponse);
try {
filterChain.doFilter(headerWriterRequest, headerWriterResponse);
}
finally {
headerWriterResponse.writeHeaders();
}
}
看上面注釋
Filter implementation to add headers to the current response
給當前響應添加headers,起到保護瀏覽器訪問的作用。
CsrfFilter
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}
CsrfFilter 主要是通過驗證 CSRF Token 來驗證,判斷是否受到了跨站點攻擊;處理流程簡單描述如下,
每當用戶登錄系統(tǒng)某個頁面的時候,通過系統(tǒng)后臺隨機生成一個 CSRF Token,通過 response 返回給客戶端;客戶端在發(fā)送 POST 表單提交的時候,需要將該 CSRF Token 作為隱藏字段(一般將該表單字段命名為 _csrf)提交到系統(tǒng)后臺進行處理;系統(tǒng)后臺會在當前的 session 中一直保存該 CSRF Token,這樣,當后臺收到前端所提交的 CSRF Token 以后,將會與當前 session 中緩存的 CSRF Token 進行比對,若兩者相同,則驗證通過,若兩者不相等,則驗證失敗,拒絕訪問;Spring Security 正式通過這樣的邏輯來避免 CSRF 攻擊的
LogoutFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判斷是否需要登出
if (requiresLogout(request, response)) {
// 獲取Authentication
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}
// 處理Authentication,一般就是直接清除
this.handler.logout(request, response, auth);
// 調用登出成功處理器,一般是頁面跳轉,也可自定義
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
// 執(zhí)行后續(xù)過濾器
chain.doFilter(request, response);
}
上述注釋表明,這個過濾器專門處理登出請求的。
OAuth2ClientAuthenticationProcessingFilter
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
// 向認證服務器發(fā)送請求獲取token
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
try {
//通過token獲取Authentication(這是一個解析token的過程)
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
// 發(fā)布成功獲取Authentication事件
publish(new AuthenticationSuccessEvent(result));
return result;
}
catch (InvalidTokenException e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
}
OAuth2ClientAuthenticationProcessingFilter是專門處理認證流程第6步的一個實現(xiàn),通過code向認證服務器獲取token
RequestCacheAwareFilter
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
這個filter的用途官方解釋是
用于用戶登錄成功后,重新恢復因為登錄被打斷的請求
這個解釋也有幾點需要說明
被打算的請求:簡單點說就是出現(xiàn)了AuthenticationException、AccessDeniedException兩類異常
重新恢復:既然能夠恢復,那肯定請求信息被保存到cache中了
SecurityContextHolderAwareRequestFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
(HttpServletResponse) res), res);
}
一行代碼,就是對請求做了一個包裝。
AnonymousAuthenticationFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 當前面的過濾器都沒有設置Authentication的時候,這里會給一個匿名的Authentication
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
// 繼續(xù)執(zhí)行后面的過濾器
chain.doFilter(req, res);
}
這個就是用來兜底的過濾器,反正只要沒有Authentication,最終都會給一個Authentication。
SessionManagementFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 判斷當前session中是否有SPRING_SECURITY_CONTEXT屬性
if (!securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
// 判斷authentication是否是一個匿名的authentication
if (authentication != null && !trustResolver.isAnonymous(authentication)) {
// 說明用戶已經認證成功來,需要保存authentication到session中
try {
sessionAuthenticationStrategy.onAuthentication(authentication,
request, response);
}
catch (SessionAuthenticationException e) {
// The session strategy can reject the authentication
logger.debug(
"SessionAuthenticationStrategy rejected the authentication object",
e);
// 出異常就清除,并調用失敗處理器
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, e);
return;
}
//把SecurityContext設置到當前session中
securityContextRepository.saveContext(SecurityContextHolder.getContext(),
request, response);
}
else {
// No security context or authentication present. Check for a session
// timeout
if (request.getRequestedSessionId() != null
&& !request.isRequestedSessionIdValid()) {
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " is invalid.");
}
if (invalidSessionStrategy != null) {
invalidSessionStrategy
.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}
這個過濾器看名字就知道是管理session的了。
ExceptionTranslationFilter
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) {
// 獲取后續(xù)過濾器拋出的異常
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
// 看看能不能拿到AuthenticationException
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
// 如果不是AuthenticationException,就看看是不是AccessDeniedException
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);
}
// 真正處理AuthenticationException或AccessDeniedException
// 如果捕獲到的異常是AuthenticationException,就重新執(zhí)行認證
// 如果捕獲到的異常是AccessDeniedException,再進一步執(zhí)行下面的判斷
// 如果當前的認證形式是Anonymous或者RememberMe,則重新執(zhí)行認證
// 否則就是當前認證用戶沒有權限訪問被請求資源,調用accessDeniedHandler.handle方法
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);
}
}
}
好吧,這個也好理解,就是專門處理異常的過濾器。
FilterSecurityInterceptor
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// OncePerRequestFilter子類會執(zhí)行這里
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 執(zhí)行父類beforeInvocation,類似于aop中的before
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 執(zhí)行過濾器鏈
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
// 這方法就是獲取ConfigAttribute和Authentication
// 通過ConfigAttribute和Authentication判斷當前請求是否允許正常通過
// 不允許的話,就拋出對應的異常
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
這個過濾器就是根據(jù)配置和權限決定請求是否正常通過,這是一個最終的決斷器。
認證過程

1.當請求/client/user進資源服務器,最終請求到達AnonymousAuthenticationFilter,就拿到一個匿名Authentication,通過FilterSecurityInterceptor決斷后拋出AccessDeniedException,ExceptionTranslationFilter根據(jù)異常會重定向到資源服務器登錄頁面
2.當請求資源服務器登錄頁面請求進來后,到達OAuth2ClientAuthenticationProcessingFilter,
AuthorizationCodeAccessTokenProvider
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
throw getRedirectForAuthorization(resource, request);
}
當沒有從登錄請求中拿到code和state參數(shù)時,拋出UserRedirectRequiredException,該異常會向上拋出被OAuth2ClientContextFilter捕獲,OAuth2ClientContextFilter針對該異常準備好URL和請求參數(shù),并告知客戶端向認證服務器重定向。
3.當認證服務器接收到/oauth/auhorize請求的時候,最終還是給了一個AnonymousAuthentication,領到了一個AccessDeniedException,被告知要重定向到認證服務器的登錄頁面。
4.然后就乖乖地請求認證服務器的登錄頁面啦,被DefaultLoginPageGeneratingFilter截獲,用戶看到了登錄頁面。用戶輸入用戶名密碼,提交表單給認證服務器,被UsernamePasswordAuthenticationFilter截獲,用戶登錄成功后認證服務器攜帶code告知客戶端重定向到資源服務器登錄請求。
5.客戶端重定向到指定URL后,資源服務器通過OAuth2ClientAuthenticationProcessingFilter獲取code。
6.資源服務器通過OAuth2ClientAuthenticationProcessingFilter獲取code后,再發(fā)送請求到認證服務器獲取token。認證服務器接收到/oauth/token請求,最終在TokenEndpoint生成token。
7.資源服務器拿到token后,說明用戶認證通過,資源服務器告知用戶重定向到第一次請求。
8.客戶端根據(jù)指示重定向到第一次發(fā)起的請求,資源服務器此時已經持有用戶認證信息,就能正常提供服務。
以上就是我對于spring security oauth2認證過程的一個簡單分析,大部分分析內容都是針對資源服務器,認證服務器略有不同,小伙伴可以利用這個方法針對認證服務器做分析。