我们知道 Spring Security 采用 IoC 和 AOP思想,基于 Servlet 过滤器实现的安全框架。
传送门:Spring Security过滤器链加载执行流程分析:https://blog.csdn.net/qq_42402854/article/details/122205790
OAuth2AuthenticationProcessingFilter是OAuth2受保护资源的身份验证过滤器。重点查看它。
一、OAuth2AuthenticationProcessingFilter
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);
private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private boolean stateless = true;
...
}
1、查看 doFilter方法
查看 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 {
// 1. 从标头中提取OAuth令牌
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设置属性
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
// 转为AbstractAuthenticationToken并设置Details
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
// 2. 认证管理器进行认证
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
// 发布认证成功
eventPublisher.publishAuthenticationSuccess(authResult);
// 4. 设置SecurityContext
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}// 5. 异常处理
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);
}
下面基于 JdbcTokenStore查看分析源码。
2、从标头中提取OAuth令牌
调用 BearerTokenExtractor的 extract方法,从标头中提取OAuth令牌。
最后封装返回 PreAuthenticatedAuthenticationToken对象
,它是一个预认证对象的Authentication,它的 principal属性包含了当前令牌。
@Override
public Authentication extract(HttpServletRequest request) {
String tokenValue = extractToken(request);
if (tokenValue != null) {
// 创建PreAuthenticatedAuthenticationToken
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
return authentication;
}
return null;
}
2.1 extractToken方法如下:
protected String extractToken(HttpServletRequest request) {
// 1. 检查消息头中的令牌
String token = extractHeaderToken(request);
// 2. 消息头没有检查请求参数中是否有access_token
if (token == null) {
logger.debug("Token not found in headers. Trying request parameters.");
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);
}
}
// 3.返回token
return token;
}
注意:
先从请求头中提取令牌,如果没有,再从请求参数中提取令牌。所以这也是通过令牌访问资源服务器的两种方式。
2.2 extractHeaderToken方法如下:
protected String extractHeaderToken(HttpServletRequest request) {
// 1.获取Authorization消息头内容 Bearer xxxtoken
Enumeration<String> headers = request.getHeaders("Authorization");
// 2. 循环消息头内容
while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
String value = headers.nextElement();
if
// 3. 如果已Bearer 开头((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
// 4. 截取令牌
String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
// Add this here for the auth details later. Would be better to change the signature of this method.
// 5. request对象添加属性
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
int commaIndex = authHeaderValue.indexOf(',');
if (commaIndex > 0) {
// 6.如果存在逗号,则重新截取
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
// 7. 返回token
return authHeaderValue;
}
}
return null;
}
3、调用认证管理器进行认证
查看认证管理器 OAuth2AuthenticationManager的 authenticate方法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Authentication预认证对象为null,抛出Invalid token (token not found)异常。
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
// 获取token
String token = (String) authentication.getPrincipal();
// 1. JdbcTokenStore这里调用DefaultTokenServices
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
// 2. 检查ClientDetails
checkClientDetails(auth);
// 填充认证信息并返回Authention
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
3.1 调用DefaultTokenServices
查看 DefaultTokenServices类的 loadAuthentication方法。
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
InvalidTokenException {
//1. 查库,通过令牌获取token_id
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
else if (accessToken.isExpired()) {
tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
//2. 查库,通过token_id获取授权信息
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
if (result == null) {
// in case of race condition
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
if (clientDetailsService != null) {
String clientId = result.getOAuth2Request().getClientId();
try {
clientDetailsService.loadClientByClientId(clientId);
}
catch (ClientRegistrationException e) {
throw new InvalidTokenException("Client not valid: " + clientId, e);
}
}
return result;
}
3.2 检查ClientDetails
查看 checkClientDetails方法
private void checkClientDetails(OAuth2Authentication auth) {
// 1. Oauth客户端如果为null ,不检查,配置了ClientDetailsService的实现类,会进行检查
if (clientDetailsService != null) {
ClientDetails client;
try {
// 2. 获取客户端
client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
}
catch (ClientRegistrationException e) {
throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
}
// 3. 获取配置的客户端授权范围
Set<String> allowed = client.getScope();
for (String scope : auth.getOAuth2Request().getScope()) {
// 4. 如果没有当前授权,则抛出异常OAuth2AccessDeniedException
if (!allowed.contains(scope)) {
throw new OAuth2AccessDeniedException(
"Invalid token contains disallowed scope (" + scope + ") for this client");
}
}
}
}
4、设置SecurityContext
认证管理器进行认证通过,将用户授权信息放到 SecurityContextHolder上下文中。
eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
5、异常处理
catch (OAuth2Exception failed) {
// 清理SecurityContext
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + failed);
}
// 发布认证失败
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
// 调用OAuth2AuthenticationEntryPoint返回ResponseEntity
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
return;
}
chain.doFilter(request, response);
}
通过查看源码,对OAuth2令牌的身份验证有了一定认识。
– 求知若饥,虚心若愚。