本文将从基于spring security oauth2.0流程中的(1)获取token的认证、受权以及(2)带token去访问受保护资源的流程进行源码解析。前端
了解spring security 的应该知道oauth2.0协议提供了四种受权模式
spring
以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的关键方法以下:服务器
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
该接口最主要的做用是用来作验证,这个接口只有一个方法:框架
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
复制代码
其中authenticate()方法运行后可能会有三种状况:
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做为获取用户信息的接口,其中只有一个方法
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
复制代码
该接口的实现类经常使用的分为三个
综上所述,整个认证流程可分为以下几步:
认证相关UML类图
前面的两个ClientCredentialsTokenEndpointFilter和DaoAuthenticationProvider能够理解为一些前置校验,和身份封装。 基本client模式,通过ClientCredentialsTokenEndpointFilter以后,身份信息已经获得了AuthenticationManager的验证。接着便到达了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。
UML类图体现的TokenGranter接口的设计以下:
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;
}
}
复制代码
五种受权类型分别是:
在客户端(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);
}
...
}
复制代码
能够经过上述源码看到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做为入口进行拓展。
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这个类。
核心的代码以下:
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相关的核心配置,其中不少注意点:
在访问受限资源的时候咱们会在请求中携带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稍后描述。
携带access_token一定得通过身份认证,通过身份认证就须要使用到AuthenticationManager接口,可是资源访问身份认证时,AuthenticationManager的实现类并非经常使用实现类ProviderManager,而是OAuth2AuthenticationManager。
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基本相似,此处不展开介绍。
这个接口只有一个实现类,主要用于从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。
http://localhost:8080/user/getUser
Header:
Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
复制代码
http://localhost:8080/user/getUser?access_token=f732723d-af7f-41bb-bd06-2636ab2be135
复制代码
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方法,加入本身想要的异常封装。
@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);
}
}
复制代码
这样在请求资源时,出现如上异常时,会抛出自定义的异常格式,方便前端小伙伴统一解析返回结果。