OAuth2.0權限驗證過程是咋樣的呢?

OAuth2.0最直觀配置可以看這裏

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

其中最重要的就是

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

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

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

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

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

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

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

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();
}
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;
}

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

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

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

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

@Slf4j
@Component
@AllArgsConstructor
public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
	private final ObjectMapper objectMapper;

	@Override
	@SneakyThrows
	public void commence(HttpServletRequest request, HttpServletResponse response,
						 AuthenticationException authException) {
		response.setCharacterEncoding(CommonConstants.UTF8);
		response.setContentType(CommonConstants.CONTENT_TYPE);
		R<String> result = new R<>();
		result.setCode(HttpStatus.HTTP_UNAUTHORIZED);
		if (authException != null) {
			result.setMsg("error");
			result.setData(authException.getMessage());
		}
		response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
		PrintWriter printWriter = response.getWriter();
		printWriter.append(objectMapper.writeValueAsString(result));
	}
}

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

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

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

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

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

發佈了73 篇原創文章 · 獲贊 3 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章