OAuth2.0 權限驗證過程是咋樣的,帶你來解析

我們知道,spring 中有很多內置的過濾器,我們一個普通的請求,會經過下圖這些過濾器的處理

OAuth2.0 權限驗證過程是咋樣的,帶你來解析

其中最重要的就是

org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#doFilter

<code>public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {

final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;

try {

Authentication authentication = tokenExtractor.extract(request);

if (authentication == null) {
if (stateless && isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
}
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);

if (debug) {
logger.debug("Authentication success: " + authResult);
}

eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);

}
}
catch (OAuth2Exception failed) {
SecurityContextHolder.clearContext();

if (debug) {
logger.debug("Authentication request failed: " + failed);
}
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));

return;
}

chain.doFilter(request, response);
}
/<code>

這裡會通過 org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor#extract 來獲取請求 header 中的 token 信息(如 Bearer 2c563440-bdd8-48a0-afca-f4ceb18c20ae)

<code>public Authentication extract(HttpServletRequest request) {
String tokenValue = extractToken(request);
if (tokenValue != null) {
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
return authentication;
}
return null;
}
/<code>

如果能獲取並正確解析,則按照已授權一路通行無阻,在具體的業務代碼中還能獲取到當前登錄用戶信息。

如果沒有獲取到 token 或解析失敗,則按照未授權路線,判斷當前訪問的 url 是否允許匿名訪問(可以看 ,怎麼配置匿名訪問資源),如果不允許則拋出 AccessDenied 異常,這個過程是通過下面的代碼實現的。

org.springframework.security.web.access.expression.WebExpressionVoter#vote

<code>public void decide(Authentication authentication, Object object,
Collection<configattribute> configAttributes) throws AccessDeniedException {
int deny = 0;

for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);

if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}

switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;

case AccessDecisionVoter.ACCESS_DENIED:
deny++;

break;

default:
break;
}
}

if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}

// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
/<configattribute>/<code>
<code>public int vote(Authentication authentication, FilterInvocation fi,
Collection<configattribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;

WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

if (weca == null) {
return ACCESS_ABSTAIN;
}

EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,

fi);
ctx = weca.postProcess(ctx, fi);

return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
/<configattribute>/<code>
OAuth2.0 權限驗證過程是咋樣的,帶你來解析

OAuth2.0 權限驗證過程是咋樣的,帶你來解析

等到 ExceptionTranslationFilter 過濾器中時,如果發現有異常,直接調用 AuthenticationEntryPoint#commence 來處理,這裡也很重要,給了我們一個可以自己處理異常的機會。

org.springframework.security.web.access.ExceptionTranslationFilter#sendStartAuthentication

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

就像這裡的 ResourceAuthExceptionEntryPoint 是我們自定義的處理類

<code>@Slf4j
@Component
@AllArgsConstructor
public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
\tprivate final ObjectMapper objectMapper;

\t@Override
\t@SneakyThrows
\tpublic void commence(HttpServletRequest request, HttpServletResponse response,
\t\t\t\t\t\t AuthenticationException authException) {
\t\tresponse.setCharacterEncoding(CommonConstants.UTF8);
\t\tresponse.setContentType(CommonConstants.CONTENT_TYPE);
\t\tR<string> result = new R<>();
\t\tresult.setCode(HttpStatus.HTTP_UNAUTHORIZED);
\t\tif (authException != null) {
\t\t\tresult.setMsg("error");
\t\t\tresult.setData(authException.getMessage());
\t\t}
\t\tresponse.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
\t\tPrintWriter printWriter = response.getWriter();
\t\tprintWriter.append(objectMapper.writeValueAsString(result));
\t}
}
/<string>/<code>

可以返回一個自定義的 json 對象(一般前後端分離的項目中,都是 ajax 交互,所以 json 對象是最通用的數據結構)

<code>{
"code": 401,
"msg": "error",
"data": "Full authentication is required to access this resource"
}

/<code>

這樣在終端獲取到 401 的 code 時,可以通過全局攔截的配置(比如 axios),控制頁面跳轉到登錄頁

<code>// HTTPresponse攔截
axios.interceptors.response.use(res => {
const status = Number(res.status) || 200
const message = res.data.msg || errorCode[status] || errorCode['default']
if (status === 401) {
store.dispatch('FedLogOut').then(() => {
router.push({path: '/login'})
})
return
}
}, error => {
return Promise.reject(new Error(error))
})
/<code>

好了,到這裡,我們把一次頁面請求的權限驗證過程解析完了,能更清晰的認識 OAuth 的授權和驗證原理了。


分享到:


相關文章: