客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式:
- 密码模式(resource owner password credentials): 用户向客户端提供自己的用户名和密码。客户端使用这些信息,向服务商提供商索要授权。(一般不用)
- 客户端模式(client credentials): 指客户端以自己的名义,而不是以用户的名义,向服务提供商进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求服务提供商提供服务,其实不存在授权问题。
- 授权码模式(authorization code): 授权码模式,是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与服务提供商的认证服务器进行互动。
- 简化模式(implicit): 不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了授权码这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
资源拥有着(ResourceOwner): 资源的拥有人,想要分享某些资源给第三方应用
客户应用: 通常是一个Web或者无线应用,需要访问用户的受保护资源
资源服务器ResourceServer: Web站点或者Web service API,用户的受保护数据存在此处
授权服务器AuthenticationServer:客户应用成功认证并获得授权之后,向客户应用颁发访问令牌Access Token
客户凭证: 客户的clientId和密码用于认证客户,相当于给不同机构分配机构ID
令牌: 授权服务器在接收到客户请求后,颁发的访问令牌
作用域: 客户请求访问令牌时,由资源拥有者额外指定的细分权限(permission)
授权码(Authorization Code Token): 仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌
刷新令牌(Refresh Token): 用于去授权服务器获取一个新的访问令牌
访问令牌(Access Token): 用于去代表一个用户或服务直接去访问受保护的资源.
Bearer Token: 不管谁拿到Token,都可以访问资源
Proof of Possession(PoP Token): 可以校验client是否对Token有名确的拥有权
spring-security-oauth三种存储策略:
- InMemoryTokenStore 令牌存储在服务器内存中,因此存在授权服务器重新启动时丢失令牌的风险。
- JwtTokenStore 所有授权和访问授权数据都被编码到令牌本身中,并且此类令牌不会在任何地方持久化。此类令牌使用解码器进行即时验证,并且依赖于JwtAccessTokenConverter 。
- JdbcTokenStore 令牌数据存储在关系数据库中。使用此令牌存储,可以安全地重新启动授权服务器。令牌也可以在服务器之间轻松共享,并且可以被吊销。注意,要使用JdbcTokenStore,我们将在类路径中需要“spring-jdbc”依赖项。
虽然两者有时候可能存在于同一个应用程序中, 但 Spring Security OAuth 中你可以把它们各自放在不同应用上, 而且你可以有多个资源服务, 它们共享同一个中央授权服务.所有获取令牌的请求都将在 Spring MVC controller endpoints 中处理, 并且访问受保护的资源服务的处理流程将会放在标准的 Spring Security 请求过滤器中.
下面是配置一个授权服务必须要实现的 endpoints:
AuthorizationEndpoint: 用来作为请求者获得权限的服务, 默认 URL 是 /oauth/authorize
TokenEndpoint: 用来作为请求者获取令牌的服务, 默认 URL 是 /oauth/token
下面是配置一个资源服务必须要实现的过滤器:
OAuth2AuthenticationProcessingFilter: 用来作为认证令牌的一个处理流程过滤器. 只有当它放行后, 请求才能访问被保护的资源
流程
协议流程
1 | +--------+ +---------------+ |
刷新Token
1 | +--------+ +---------------+ |
授权码
1 | +----------+ |
隐式
1 | +----------+ |
密码
1 | +----------+ |
客户端
1 | +---------+ +---------------+ |
Authorization Server - 授权服务配置
用 @EnableAuthorizationServer 来配置 OAuth 2.0 授权服务.接下来介绍几个配置类, 它们是由 Spring 创建的独立的配置对象, 会被 Spring 传入 AuthorizationServerConfigurer 中:
- ClientDetailsServiceConfigurer: 用来配置客户端详情服务, 客户端详情信息在这里初始化, 你能够把客户端详情硬编码在这里或是通过数据库来存取.
- AuthorizationServerEndpointsConfigurer: 用来配置授权以及令牌的访问端点和令牌服务;
- AuthorizationServerSecurityConfigurer: 用来配置令牌端点的安全约束.(以上配置可以选择继承 AuthorizationServerConfigurerAdapter 并且覆盖其中的三个configure 方法来配置)
ClientDetailsServiceConfigurer
void configure(ClientDetailsServiceConfigurer clients) throw Exception
ClientDetailsServiceConfigurer, AuthorizationServerConfigurer 的一个回调配置项. 能够使用内存或者 JDBC 来实现客户端详情服务 (ClientDetailsService).
AuthorizationServerEndpointsConfigurer
void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
AuthorizationServerEndpointsConfigurer, AuthorizationServerConfigurer 的一个回调配置项. 用于配置授权以及令牌的访问端点和令牌服务.
AuthorizationServerSecurityConfigurer
void configure(AuthorizationServerSecurityConfigurer security) throws Exception
AuthorizationServerSecurityConfigurer, AuthorizationServerConfigurer 的一个回调配置项. “授权服务器” 安全配置, 实际上就是 /oauth/token 端点
AuthorizationServerTokenServices - 管理令牌
AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理, 需要注意:
- 当一个令牌被创建了, 你必须保存, 这样当一个客户端使用这个令牌对资源服务请求的时候才可以应用到这个令牌
- 当一个令牌是有效的时候, 它可以被用来加载身份信息, 里面包含了这个令牌的相关权限
InMemoryTokenStore JdbcTokenStore JwtTokenStore
公钥公布在 /oauth/token_key 这个 URL 连接中, 默认的访问安全规则是 denyAll()
Grant Type - 配置授权类型
授权是使用 AuthorizationEndpoint 和这个端点来控制的, 你能够使用 AuthorizationServerEndpointsConfigurer 这个对象的实例来配置
Endpoint URL - 配置授权端点
AuthorizationServerEndpointsConfigurer 这个配置对象 (AuthorizationServerConfigurer 的一个回调配置项) 有一个叫做 pathMapping() 的方法用来配置端点 URL 链接, 它有两个参数:第一个参数 String 类型的, 这个端点URL的默认链接; 第二个参数: String 类型的, 你要进行替代的URL链接.
以上的参数都将以 “/” 字符为开始的字符串, 框架的默认 URL 链接如下列表, 可以作为这个 pathMapping() 方法的第一个参数:
/oauth/authorize: 授权端点, 这个 URL 应该被 Spring Security 保护起来只供授权用户访问;也就是授权码模式认证授权接口
/oauth/token: 令牌端点,获取 token 的接口
/oauth/confirm_access: 用户确认授权提交端点;
/oauth/error: 授权服务错误信息端点;
/oauth/check_token: 用于资源服务访问的令牌解析端点; 当授权服务器和资源服务器分开部署的时候, 资源服务器需要访问这个地址验证令牌,也就是检查 token 合法性接口
/oauth/token_key: 提供公有密匙的端点, 如果使用 JWT 令牌的话;
参数说明
access_token : 就是之后请求需要带上的 token
token_type:为 bearer,这是 access token 最常用的一种形式
refresh_token:之后可以用这个值来换取新的 token,而不用输入账号密码
expires_in:token 的过期时间(秒)
Resource Server - 资源服务配置
一个资源服务 (可以和授权服务在同一个应用中, 当然也可以分离开成为两个不同的应用程序) 提供一些受token令牌保护的资源, Spring OAuth 提供者是通过 Spring Security authentication filter 即验证过滤器来实现的保护, 你可以通过 @EnableResourceServer 注解到一个 @Configuration 配置类上, 并且必须使用 ResourceServerConfigurer 这个配置对象来进行配置 (可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法, 参数就是这个对象的实例). 下面是一些可以配置的属性:
tokenServices: ResourceServerTokenServices 类的实例, 用来实现令牌服务.
resourceId: 这个资源服务的ID, 这个属性是可选的, 但是推荐设置并在授权服务中进行验证.
其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌.
请求匹配器, 用来设置需要进行保护的资源路径, 默认的情况下是受保护资源服务的全部路径.
受保护资源的访问规则, 默认的规则是简单的身份验证 (plain authenticated).
其他的自定义权限保护规则通过 HttpSecurity 来进行配置.
@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链,
使用授权服务的 /oauth/check_token 端点你需要将这个端点暴露出去, 以便资源服务可以进行访问
ResourceServerSecurityConfigurer
configure(ResourceServerSecurityConfigurer resources): 为资源服务器配置特定属性, 如 resource id.
org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurer#configure(ResourceServerSecurityConfigurer)
ResourceServerTokenServices
ResourceServerTokenServices 是组成授权服务的另一半, 如果你的授权服务和资源服务在同一个应用程序上的话, 你可以使用 DefaultTokenServices, 这样你就不用考虑关于实现所有必要的接口的一致性问题, 这通常是很困难的. 如果你的资源服务器是分离开的, 那么你就必须要确保匹配授权服务提供的 ResourceServerTokenServices, 它知道如何对令牌进行解码
DefaultTokenServices: 在授权服务器上, 你通常可以使用 DefaultTokenServices 并且选择一些主要的表达式通过 TokenStore (后端存储或者本地编码).
RemoteTokenServices: 允许资源服务器通过 HTTP 请求来解码令牌 (也就是授权服务的 /oauth/check_token 端点). 如果你的资源服务没有太大的访问量的话, 那么使用 RemoteTokenServices 将会很方便 (所有受保护的资源请求都将请求一次授权服务用以检验 token 值), 或者你可以通过缓存来保存每一个 token 验证的结果
resource-id
可以为每一个资源服务器 (可能是一个微服务实例) 设置一个 resource id. 在给客户端授权的时候, 可以设置这个客户端可以访问哪些微服务实例. 如果没有设置就是对所有的 resource 都有访问权限.
UserDetailsService
UserDetailsService 接口从数据库中获取用户信息, 并通过实现 AuthenticationProvider 接口编写自己的校验逻辑, 从而完成 SpringSecurity 身份校验.
UserDetailsService
UserDetailsService 是加载用户指定数据的核心接口.
loadUserByUsername
UserDetailsService 只有 loadUserByUsername 一个接口方法, 用于通过用户名获取用户数据. 返回 UserDetails 对象, 表示用户的核心信息 (用户名, 用户密码, 权限等信息).
Authentication
一旦请求被 AuthenticationManager.authenticate(Authentication) 方法处理了, Authentication 就标示为一个认证请求的 Token. 当信息被认证了, Authentication 会被线程安全的 SecurityContext 持有, 后者可以通过 SecurityContextHolder 获取. 本文中我们会用到 Authentication 其中之一: UsernamePasswordAuthenticationToken (被设计成用于描述用户名和密码的简单实现).
AuthenticationProvider
定义了用于处理认证逻辑的接口标准, 我们可以实现这个类以便实现自己的认证逻辑.
SecurityConfig
在 WebSecurityConfigurerAdapter 的实现 - 配置类中指明启用我们自定义的 AuthenticationProvider
JWTAuthorizationFilter
这个过滤器用于对携带 access-token 的请求执行权限检查. 所以, 除了注册端点之外的所有请求都应该被它过滤.
1 | public class JWTAuthorizationFilter extends OncePerRequestFilter { |
微服务中一般使用流程
token可以通过存储在数据库中每次请求进行校验,或者用jwt程序直接交易(优点性能高,缺点没办直接法注销token)
OAuth2.0 jwt 通过tokenEnhancer增强jwttoken,增加额外信息,配置是否允许跨域请求等。
- 登录成功后生成token,把用户的权限信息存储到redis中(key=permission:账号:角色 value=权限内容)。
- 请求发送到网关(网关中配置了一些白名单,黑名单等策略),网关判断token是否存在等,如果存在网关获取请求头中Token调用Auth服务校验Token是否有效(token存储在redis中或者数据库中),或者直接使用jwt算法计算校验。(如果一个账号同时只允许一个用户登录,还可以把token放在redis里面(auth:token),在登录成功后把旧的auth:token删除,插入新的auth:token,每次请求都校验auth:token是否存在)
- 功能功能服务进行权限判断的时候,通过请求头中的token获取到用户信息,通过用户信息去redis中获取到具体的权限信息,然后通过自定义注解拦截器(基于:
HandlerInterceptorAdapter
,或者@Aspect
切面)完成对接口的权限校验。
权限更简单的实现方案,直接使用uuid随机码生成token存储在redis中,通过切面进行权限校验,auth2都可以不需要,如果需要各种强大的功能SSO功能才需要考虑OAuth2.0。
SpringSecurity
自定义权限校验,开启@EnableGlobalMethodSecurity(prePostEnabled = true)
注解,@PreAuthorize
可以用了。 定义@Service("auth")
定义鉴权方法 hasPermi等,定义异常类。可以在Controller层通过@PreAuthorize("@auth.hasPermi('sys:config:info')")
用户成功登陆处理,只需要实现AuthenticationSuccessHandler接口即可
1 | /** |
OAuth2.0 对权限信息的存储
1 | /** |
流程大概是:
OAuth2AuthenticationProcessingFilter拦截请求调用 OAuth2AuthenticationManager 进行权限校验 调用RemoteTokenServices获取用户详细权限信息(调用this.postForMap(this.checkTokenEndpointUrl, formData, headers) restTemplate /oauth/check_token,资源服务loadAuthentication(调用DefaultTokenServices如果使用的是redisStore里面已经存储了权限信息),调用DefaultAccessTokenConverter再调用DefaultUserAuthenticationConverter调用UserDetailsService.loadUserByUsername()然后Convert转换),然后OAuth2AuthenticationProcessingFilter调用SecurityContextHolder.getContext().setAuthentication(authResult);方法保存到上下文中。
进行权限校验的时候通过注解获取方法权限名称,再通过上下文获取权限列表进行对比判断是否有权限。
1 | Token→OAuthAuthenticationProcessingFilter→OAuth2AuthnticationManager→RemoteTOkenServices→CheckToken→DefaultAccessTokenConverter→DefaultUserAuthenticationConverter→userDetailsService→UserDetails |
- OAuth2AuthenticationManager.authenticate(),filter执行判断的入口
- 当用户携带token 去请求微服务模块,被资源服务器拦截调用RemoteTokenServices.loadAuthentication ,执行所谓的check-token过程
- CheckToken 处理逻辑很简单,就是调用redisTokenStore 查询token的合法性,及其返回用户的部分信息
- 返回给 RemoteTokenServices.loadAuthentication 最后一句tokenConverter.extractAuthentication 解析组装服务端返回的信息 userTokenConverter.extractAuthentication(map);
- 最重要的一步,是否判断是否有userDetailsService实现,如果有 的话去查根据 返回的 username 查询一次全部的用户信息,没有实现直接返回username。
- UerDetailsServiceImpl.loadUserByUsername 根据用户名去换取用户全部信息
RemoteTokenServices远程调用流程图:
认证会用到的请求
- 获取access_token(/oauth/token) 请求所需参数:client_id、client_secret、grant_type、username、password
1
http://127.0.0.1:9000/auth/oauth/token?client_id=web&client_secret=123456&grant_type=password&username=admin&password=admin123
- 检查请求是否有效(/oauth/check_token) 请求所需参数:token
1
http://127.0.0.1:9000/auth/oauth/check_token?token=fsdfsdf-223d-4b3d7-221-fffssdf
- 刷新token请求(/oauth/token) 请求所需参数:grant_type、refresh_token、client_id、client_secret 其中grant_type为固定值:grant_type=refresh_token
1
http://127.0.0.1:9000/oauth/token?grant_type=refresh_token&refresh_token=fbde821e-f419-4221-1224-91121232e9&client_id=web&client_secret=123456
认证流程
获取token流程
- 用户发起获取token的请求。
- 过滤器会验证path是否是认证的请求/oauth/token,如果为false,则直接返回没有后续操作。
- 过滤器通过clientId查询生成一个Authentication对象。
- 然后会通过username和生成的Authentication对象生成一个UserDetails对象,并检查用户是否存在。
- 以上全部通过会进入地址/oauth/token,即TokenEndpoint的postAccessToken方法中。
- postAccessToken方法中会验证Scope,然后验证是否是refreshToken请求等。
- 之后调用AbstractTokenGranter中的grant方法。
- grant方法中调用AbstractUserDetailsAuthenticationProvider的authenticate方法,通过username和Authentication对象来检索用户是否存在。
- 然后通过DefaultTokenServices类从tokenStore中获取OAuth2AccessToken对象。
- 然后将OAuth2AccessToken对象包装进响应流返回。
刷新token(refresh token)的流程
获取token调用的是AbstractTokenGranter中的getAccessToken方法,然后调用tokenStore中的getAccessToken方法获取token。
刷新token调用的是RefreshTokenGranter中的getAccessToken方法,然后使用tokenStore中的refreshAccessToken方法获取token。
tokenStore的特点
tokenStore通常情况为自定义实现,一般放置在缓存或者数据库中。此处可以利用自定义tokenStore来实现多种需求,如:
同已用户每次获取token,获取到的都是同一个token,只有token失效后才会获取新token。
同一用户每次获取token都生成一个完成周期的token并且保证每次生成的token都能够使用(多点登录)。
同一用户每次获取token都保证只有最后一个token能够使用,之前的token都设为无效(单点token)。
获取token详细流程
- 重要的过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) { // 验证Path
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response); // 调用ClientCredentialsTokenEndpointFilter类的方法,进行clientid校验
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult); // clientid校验通过了,通过各种filter,最后调用TokenEndpoint中真正的/oauth/token
}
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult); //调用TokenEndpoint中真正的/oauth/token,进行后续username,password验证
}
} - 1中的attemptAuthentication方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
}
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
// If the request is already authenticated we can assume that this
// filter is not needed
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
}
if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
}
if (clientSecret == null) {
clientSecret = "";
}
clientId = clientId.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
return this.getAuthenticationManager().authenticate(authRequest); // ** 重要 调用ProviderManager类中方法,校验username与password
}
} - 2中调用的authenticate方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication); // 遍历所有Provider,只要有一个结果就返回,这里是AbstractUserDetailsAuthenticationProvider类返回结果
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
} - 3中调用的AbstractUserDetailsAuthenticationProvider的authenticate方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); // 检查用户是否存在,调用DaoAuthenticationProvider类
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); // 验证client_id,client_secret是否正确,调用DaoAuthenticationProvider类
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
} - 4中调用的DaoAuthenticationProvider的retrieveUser方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // 调用ClientDetailsUserDetailsService类
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
} - 5中调用的ClientDetailsUserDetailsService类的loadUserByUsername方法,执行完后接着返回执行4之后的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public class ClientDetailsUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ClientDetails clientDetails;
try {
clientDetails = clientDetailsService.loadClientByClientId(username); // 这边clientDetailsService一般是自己注入的service,RedisClientDetailsService根据clienid获取Client信息
} catch (NoSuchClientException e) {
throw new UsernameNotFoundException(e.getMessage(), e);
}
String clientSecret = clientDetails.getClientSecret();
if (clientSecret== null || clientSecret.trim().length()==0) {
clientSecret = emptyPassword;
}
return new User(username, clientSecret, clientDetails.getAuthorities());
}
}
public class JdbcClientDetailsService implements ClientDetailsService, ClientRegistrationService {
}
public class RedisClientDetailsService extends JdbcClientDetailsService
{
public RedisClientDetailsService(DataSource dataSource)
{
super(dataSource);
super.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
super.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);
}
public ClientDetails loadClientByClientId(String clientId)
{
return super.loadClientByClientId(clientId);
}
}
public class UserDetailsServiceImpl implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 自己注入的实现类,根据username获取用户信息
Result result = remoteUserService.getUserInfo(username);
checkUser(result, username);
return getUserDetails(result);
}
} - 4中调用的DaoAuthenticationProvider类的additionalAuthenticationChecks方法,此处执行完则主要过滤器执行完毕,后续会进入/oauth/token映射的方法,进行真正的username password校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { // presentedPassword为client_secret。后面真正用户passowrd也是调用该方法
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
} - 此处进入/oauth/token映射的TokenEndpoint类的postAccessToken方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65public class TokenEndpoint extends AbstractEndpoint {
public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal,
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!allowedRequestMethods.contains(HttpMethod.GET)) {
throw new HttpRequestMethodNotSupportedException("GET");
}
return postAccessToken(principal, parameters);
}
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal,
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); // 获取ClientDetails
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) { // 验证scope
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); // 调用AbstractTokenGranter类,生成OAuth2AccessToken对象
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token); // 包装OAuth2AccessToken对象返回
}
} - 此处是8中的AbstractTokenGranter类的grant方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50public abstract class AbstractTokenGranter implements TokenGranter {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);// 调用下面方法
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); // 调用ResourceOwnerPasswordTokenGranter类返回OAuth2Authentication对象
}
}
// 可以忽略
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); // 调用 ResourceOwnerPasswordTokenGranter,执行AbstractTokenGranter grant方法
if (grant!=null) {
return grant;
}
}
return null;
}
public void addTokenGranter(TokenGranter tokenGranter) {
if (tokenGranter == null) {
throw new IllegalArgumentException("Token granter is null");
}
tokenGranters.add(tokenGranter);
}
} - 9中ResourceOwnerPasswordTokenGranter类中的getOAuth2Authentication方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String username = parameters.get("username");
String password = parameters.get("password");
// Protect from downstream leaks of password
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password); // ** 重要 准备验证用户密码 ,username password
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth); // provider通常为用户自定义,** 验证username password
}
catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
} - 9中调用的DefaultTokenServices中的createAccessToken方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean {
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication); // 通过tokenStore获取token
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;
}
}
// Only create a new refresh token if there wasn't an existing one
// associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in
// the case that the old access token
// expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication); // 不存在就创建
}
// But the refresh token itself might need to be re-issued if it has
// expired.
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; // 返回token
}
} - 11中RedisTokenStore中的getAccessToken方法等,此处执行完,则一直向上返回8执行后续方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class RedisTokenStore implements TokenStore {
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String key = authenticationKeyGenerator.extractKey(authentication);
byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key);
byte[] bytes = null;
RedisConnection conn = getConnection();
try {
bytes = conn.get(serializedKey);
} finally {
conn.close();
}
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
if (accessToken != null) {
OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());
if ((storedAuthentication == null || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) {
// Keep the stores consistent (maybe the same user is
// represented by this authentication but the details have
// changed)
storeAccessToken(accessToken, authentication);
}
}
return accessToken;
}
} - 8中获取到token后需要包装返回流操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public class TokenEndpoint extends AbstractEndpoint {
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal,
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token); // token返回响应
}
}
POSTMAN访问时传递的ClientID会再DaoAuthenticationProvider方法中进行校验通过JdbcClientDetailsService实现。然后调用真正/oauth/token
方法进行username校验,也是调用DaoAuthenticationProvider.retrieveUser进行校验,getUserDetailsService对象不一样,校验username的时候是remoteUserService。UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
扩展
请求拦截
1 | ---Auth授权服务器,鉴权模块的信息--- |
1 | public class FilterChainProxy extends GenericFilterBean { |
被调用模块记得引入Auth2.0 Security模块
引入模块并且通过注解启用
1 | /** |
SecurityImportBeanDefinitionRegistrar调用链
1 | java.lang.Thread.State: RUNNABLE |
OAuth2FeignConfig初始调用链
1 | java.lang.Thread.State: RUNNABLE |
微服务权限应用
权限控制可以分为三个部分:用户认证,服务权限,用户权限。
在微服务上校验用户token的话,还要考虑到要与服务调用服务的场景区分,增加了校验的复杂程度,所以选择将用户认证统一放在gateway网关校验。
服务权限认证,放在在被调用具体服务上实现。微服务间的权限校验。
用户权限认证,放在在被调用具体服务上实现(调用鉴权服务),功能权限、数据权限。主要是因为权限注解在controller层,如果把权限与API接口信息配置到数据库中也是可以直接在gateway网关中获取权限信息直接校验过滤的。
使用Feign可以实现RequestInterceptor
在请求头中增加TOKEN。服务间权限校验除了使用token还可以使用私钥。
其他方案:
比如直接用redis
登录:登录成功后把token与token对应用户信息存储在redis中
请求:在gateway中验证token是否有效(redis中获取token信息),更新redis中token过期时间,把token对应的用户信息(用户id、用户名等)存储到请求头中转发给后端微服务,微服务根据token从redis中获取用户详细信息校验是否有访问接口的权限
demo:
1 |
|
问题
返回BUG
if (!Boolean.TRUE.equals(map.get("active"))) {
返回的是String类型,需要的是boolean类型。可能是引入的包导致的。解决方法就行重写org.springframework.security.oauth2.provider.token.RemoteTokenServices类。下面这个依赖导致的问题。这个json转xml
1 | <dependency> |
参考
- The OAuth 2.0 Authorization Framework
- OAuth 2.0
- 核心组件之SecurityContextHolder
- OAuth 2 Developers Guide
- SpringSecurity的AuthenticationSuccessHandler中的Authentication参数信息说明(用户名密码登陆,非第三方登录)
- SpringSecurityOauth2实现前后端分离的token服务,app登录
- Spring Cloud OAuth2(二) 扩展登陆方式:账户密码登陆、 手机验证码登陆、 二维码扫码登陆
- OAuth2.0用户名,密码登录解析
- Spring整合Oauth2单机版认证授权详情
- Spring Security OAuth2 Demo —— 密码模式(Password)
- 拦截器中解析OAuth2身份认证,保存用户信息至SecurityContextHolder当前线程
- Spring Security Oauth2 之核心架构配置
- 基于Spring oauth2.0统一认证登录,返回自定义用户信息
- Springsecurity-oauth2之RemoteTokenServices
- pig 校验令牌详解 【原理】
- SpringSecurity OAuth2 (8) 自定义: ResourceServerTokenServices 资源服务器自行验证签名并解析令牌,其他文章也可以参考
- 扩展jwt解决oauth2 性能瓶颈
- Spring Security OAuth2 源码分析(三) TokenServices 其他文章也可以参考
- OAuth2.0 原理流程及其单点登录和权限控制
- Spring Cloud OAuth2 实现用户认证及单点登录
- SpringCloud微服务权限控制(一)概述
- 第六章 Auth2微服务权限校验笔记
- Spring Security Oauth2 认证(获取token/刷新token)流程(password模式)
- Spring-Security@PreAuthorize(“hasAuthority(‘’)”)源码分析
- SpringBoot集成Spring Security(5)——权限控制
- Spring Boot Feign服务调用之间带token