spring security oauth2.0 流程解析[19.06.19]

前言

本文将从基于spring security oauth2.0流程中的(1)获取token的认证、受权以及(2)带token去访问受保护资源的流程进行源码解析。前端

身份认证流程

了解spring security 的应该知道oauth2.0协议提供了四种受权模式
spring

  • 简化模式(implicit)
  • 客户端模式(client_credentials)
  • 受权码模式(authorization_code)
  • 密码模式(password)

以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

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

AuthenticationManager

该接口最主要的做用是用来作验证,这个接口只有一个方法:框架

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

}
复制代码

其中authenticate()方法运行后可能会有三种状况:

  • 验证成功,返回一个带有用户信息的Authentication。
  • 验证失败,抛出一个AuthenticationException异常。
  • 没法判断,返回null。

ProviderManager

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以及其实现类

UserDetailsService做为获取用户信息的接口,其中只有一个方法

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
复制代码

该接口的实现类经常使用的分为三个

  • InMemoryUserDetailsManager (内存中保存的用户信息)
  • JdbcDaoImpl(数据库中保存用户信息)
  • ClientDetailsUserDetailsService (client模式是不存在“用户”概念,该实现类是将client客户端的信息(client_id,client_secret)适配成用户的信息(username,password),这样认证流程就不须要进行修改)

认证总结

综上所述,整个认证流程可分为以下几步:

  • 用户使用用户名和密码进行登陆。
  • Spring Security将获取到的用户名和密码封装成一个Authentication接口的实现类,好比经常使用的UsernamePasswordAuthenticationToken。
  • 将上述产生的Authentication对象传递给AuthenticationManager的实现类ProviderManager进行认证。
  • ProviderManager依次调用各个AuthenticationProvider进行认证,认证成功后返回一个封装了用户权限等信息的Authentication对象。
  • 将AuthenticationManager返回的Authentication对象赋予给当前的SecurityContext。

认证相关UML类图

受权颁发token流程

前面的两个ClientCredentialsTokenEndpointFilter和DaoAuthenticationProvider能够理解为一些前置校验,和身份封装。 基本client模式,通过ClientCredentialsTokenEndpointFilter以后,身份信息已经获得了AuthenticationManager的验证。接着便到达了TokenEndpoint。

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。

TokenGranter

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;
    }
}
复制代码

五种受权类型分别是:

  • ResourceOwnerPasswordTokenGranter ==> password密码模式
  • AuthorizationCodeTokenGranter ==> authorization_code受权码模式
  • ClientCredentialsTokenGranter ==> client_credentials客户端模式
  • ImplicitTokenGranter ==> implicit简化模式
  • RefreshTokenGranter ==>refresh_token 刷新token专用

AbstractTokenGranter

在客户端(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);
    }

    ...
}
复制代码

AuthorizationServerTokenServices

能够经过上述源码看到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做为入口进行拓展。

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这个类。

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相关的核心配置,其中不少注意点:

  • <1> 建立OAuth2AuthenticationProcessingFilter,一会进行拓展解析。
  • <2> 为OAuth2AuthenticationProcessingFilter提供了固定的顶级身份认证接口AuthenticationManager,使用的实现类是OAuth2AuthenticationManager。
  • <3> 设置了TokenExtractor默认的实现类—-BearerTokenExtractor,该类大致用于能够从请求中不一样位置获取token,好比:header、url拼接等,一会进行拓展。
  • <4> 异常处理器,能够重写达到自定义异常的目的。(这里强烈建议自定义异常,保证接口返参的格式是统一的,方便前端人员进行统一解析返回结果。)

OAuth2AuthenticationProcessingFilter

在访问受限资源的时候咱们会在请求中携带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稍后描述。

身份管理器–OAuth2AuthenticationManager

携带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基本相似,此处不展开介绍。

TokenExtractor

这个接口只有一个实现类,主要用于从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。

  • 在Header中携带
http://localhost:8080/user/getUser
Header:
Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
复制代码
  • 拼接在url中做为requestParam
http://localhost:8080/user/getUser?access_token=f732723d-af7f-41bb-bd06-2636ab2be135
复制代码
  • 在form-data表单中携带
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方法,加入本身想要的异常封装。

自定义无效token 或token不存在异常类重写

@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);
        }
}
复制代码

这样在请求资源时,出现如上异常时,会抛出自定义的异常格式,方便前端小伙伴统一解析返回结果。

相关文章
相关标签/搜索