OAuth 是一个关于授权的开放网络标准. 数据的所有者告诉系统, 同意授权第三方应用进入系统, 获取这些数据. 系统从而产生一个短期的进入令牌 (token), 用来代替密码, 供第三方应用使用.
OAuth 2.0 协议一共支持 4 种不同的授权模式:
OAuth 2.0 的一个简单解释
令牌的有效期到了, 如果让用户重新走一遍上面的流程, 再申请一个新的令牌, 不仅体验差, 且没必要. OAuth 2.0 允许用户自动更新令牌.
具体方法是, B 网站颁发令牌的时候, 一次性颁发两个令牌, 一个用于获取数据 (access-token, 默认生命周期 1 小时), 另一个用于获取新的令牌 (refresh-token 字段, 使用一次就过期). 令牌到期前, 用户使用 refresh token 发一个请求, 去更新令牌.
https://b.com/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=
上面 URL 中, grant_type
参数为refresh_token
表示要求更新令牌, client_id
参数和client_secret
参数用于确认身份, refresh_token
参数就是用于更新令牌的令牌.
B 网站验证通过以后, 就会颁发新的令牌.
☆ 支持 Authorization Code Grant 和 Resource Owner Password Grant.
Spring OAuth 2.0 提供者实际上分为:
虽然两者有时候可能存在于同一个应用程序中, 但 Spring Security OAuth 中你可以把它们各自放在不同应用上, 而且你可以有多个资源服务, 它们共享同一个中央授权服务.所有获取令牌的请求都将在 Spring MVC controller endpoints 中处理, 并且访问受保护的资源服务的处理流程将会放在标准的 Spring Security 请求过滤器中.
下面是配置一个授权服务必须要实现的 endpoints:
下面是配置一个资源服务必须要实现的过滤器:
用 @EnableAuthorizationServer
来配置 OAuth 2.0 授权服务.接下来介绍几个配置类, 它们是由 Spring
创建的独立的配置对象, 会被 Spring 传入 AuthorizationServerConfigurer 中:
configure
方法来配置)void configure(ClientDetailsServiceConfigurer clients) throw Exception
ClientDetailsServiceConfigurer
, AuthorizationServerConfigurer
的一个回调配置项. 能够使用内存或者 JDBC 来实现客户端详情服务 (ClientDetailsService). 有几个重要属性:
authorization_code
, refresh_token
客户端详情 (Client Details) 能够在应用程序运行的时候更新, 可以通过访问底层的存储服务 (例如将客户端详情存储在一个关系数据库表中, 就可以使用 JdbcClientDetailsService) 或者通过 ClientDetailsManager 接口 (同时你也可以实现 ClientDetailsService 接口) 来管理
void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
AuthorizationServerEndpointsConfigurer
, AuthorizationServerConfigurer
的一个回调配置项. 用于配置授权以及令牌的访问端点和令牌服务.
void configure(AuthorizationServerSecurityConfigurer security) throws Exception
AuthorizationServerSecurityConfigurer
, AuthorizationServerConfigurer
的一个回调配置项. “授权服务器” 安全配置, 实际上就是 /oauth/token
端点.
security.authenticationEntryPoint(org.springframework.security.web.AuthenticationEntryPoint authenticationEntryPoint)
: ExceptionTranslationFilter
用于处理过滤器链中发生的 AccessDeniedException
或 AuthenticationException
异常:
AuthenticationException
时, ExceptionTranslationFilter
会调用 AuthenticationEntryPoint
的 commence 方法;AccessDeniedException
, ExceptionTranslationFilter
会判断当前用户是否是匿名用户: 如果是, AuthenticationEntryPoint
会被调用; 如果不是, ExceptionTranslationFilter
会委托 AccessDeniedHandler
来处理 (默认使用 AccessDeniedHandlerImpl
)security.allowFormAuthenticationForClients()
: 如果设置了, 就会在 BasicAuthenticationFilter
之前新增一个名为 ClientCredentialsTokenEndpointFilter
的过滤器. 后者会尝试从 HttpServletRequest
中获取 client_id
和 client_secret
用以执行认证.AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理, 需要注意:
当自己创建 AuthorizationServerTokenServices 这个接口的实现时, 你可能需要考虑使用 DefaultTokenServices 这个类, 里面包含了一些有用的实现, 你可以使用它们来修改令牌的格式和令牌的存储. 默认的, 当它尝试创建一个令牌的时候, 是使用随机值来进行填充的, 除了持久化令牌是委托一个 TokenStore 接口来实现的以外, 这个类几乎帮你做了所有的事情. 并且 TokenStore 这个接口有一个默认的实现, 就是 InMemoryTokenStore, 即所有的令牌是被保存在了内存中. 除了使用这个类以外, 你还可以使用一些其他的预定义实现. 下面有几个版本, 它们都实现了 TokenStore 接口:
使用 JWT 你需要在授权服务中使用 JwtTokenStore (依赖 spring-security-jwt), 资源服务器也需要一个解码的类: JwtAccessTokenConverter, JwtTokenStore 依赖这个来来解码以及编码, 因此你的授权服务以及资源服务都需要使用这个转换器. 令牌默认是有签名的, 并且资源服务需要验证这个签名, 因此你需要使用一个对称的 Key, 来参与签名计算, 这个 Key 存在于授权服务以及资源服务之中. 或者你可以使用非对称加密算法来对令牌签名.
公钥公布在 /oauth/token_key
这个 URL 连接中, 默认的访问安全规则是 denyAll()
, 即在默认情况下它是关闭的, 你可以注入一个标准的 SpEL 表达式到 AuthorizationServerSecurityConfigurer
配置中来开启它 (例如使用 permitAll()
来开启可能比较合适, 因为它是一个公钥.
授权是使用 AuthorizationEndpoint 和这个端点来控制的, 你能够使用 AuthorizationServerEndpointsConfigurer 这个对象的实例来配置 (AuthorizationServerConfigurer 的一个回调配置项). 如果不设置的话, 默认是支持除了密码模式外的所有模式. 接下来看一下这个配置对象的课配置属性:
AuthorizationServerEndpointsConfigurer
这个配置对象 (AuthorizationServerConfigurer
的一个回调配置项) 有一个叫做 pathMapping()
的方法用来配置端点 URL 链接, 它有两个参数:第一个参数 String 类型的, 这个端点URL的默认链接; 第二个参数: String 类型的, 你要进行替代的URL链接.
以上的参数都将以 “/” 字符为开始的字符串, 框架的默认 URL 链接如下列表, 可以作为这个 pathMapping() 方法的第一个参数:
/oauth/authorize
: 授权端点, 这个 URL 应该被 Spring Security 保护起来只供授权用户访问;/oauth/token
: 令牌端点;/oauth/confirm_access
: 用户确认授权提交端点;/oauth/error
: 授权服务错误信息端点;/oauth/check_token
: 用于资源服务访问的令牌解析端点; 当授权服务器和资源服务器分开部署的时候, 资源服务器需要访问这个地址验证令牌/oauth/token_key
: 提供公有密匙的端点, 如果使用 JWT 令牌的话;来看看在标准的 Spring Security 中 WebSecurityConfigurer
是怎么用的:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.and()
// default protection for all resources (including /oauth/authorize)
.authorizeRequests().anyRequest().hasRole("USER")
// ... more configuration, e.g. for form login
}
使用简单的 HTTP 请求来进行测试是可以的. 但是如果你要部署到产品环境上的时候, 你应该永远都使用 SSL 来保护授权服务器在与客户端进行通讯的时候进行加密. 你可以把授权服务应用程序放到一个安全的运行容器中, 或者你可以使用一个代理, 如果你设置正确了的话它们应该工作的很好 (这样的话你就不需要设置任何东西了)
但是也许你可能希望使用 Spring Security 的 requiresChannel() 约束来保证安全, 对于授权端点来说 (还记得上面的列表吗, 就是那个 /authorize 端点). 它应该成为应用程序安全连接的一部分. 而对于 /token 令牌端点来说的话, 它应该有一个标记被配置在 AuthorizationServerEndpointsConfigurer 配置对象中, 你可以使用 sslOnly() 方法来进行设置. 这两个设置是可选的. 不过在以上两种情况中, 会导致Spring Security 会把不安全的请求通道重定向到一个安全通道中. (即将 HTTP 请求重定向到 HTTPS 请求上)
端点实际上就是一个特殊的 Controller, 它用于返回一些对象数据
授权服务的错误信息是使用标准的 Spring MVC 来进行处理的, 也就是 @ExceptionHandler 注解的端点方法. 你也可以提供一个 WebResponseExceptionTranslator 对象. 最好的方式是改变响应的内容而不是直接进行渲染.
假如说在呈现令牌端点的时候发生了异常, 那么异常委托了 HttpMessageConverters 对象 (它能够被添加到 MVC 配置中) 来输出. 假如说在呈现授权端点的时候未通过验证, 则会被重定向到 /oauth/error 即错误信息端点中. whitelabel error (即 Spring 框架提供的一个默认错误页面) 错误端点提供了HTML的响应, 但是你大概可能需要实现一个自定义错误页面 (例如只是简单的增加一个 @Controller 映射到请求路径上 @RequestMapping("/oauth/error")).
有时候限制令牌的权限范围是很有用的, 这不仅仅是针对于客户端, 你还可以根据用户的权限来进行限制. 如果你使用 DefaultOAuth2RequestFactory 来配置 AuthorizationEndpoint 的话你可以设置一个flag即 checkUserScopes=true 来限制权限范围, 不过这只能匹配到用户的角色. 你也可以注入一个 OAuth2RequestFactory 到 TokenEnpoint 中, 不过这只能工作在 password 授权模式下. 如果你安装一个 TokenEndpointAuthenticationFilter 的话, 你只需要增加一个过滤器到 HTTP BasicAuthenticationFilter 后面即可. 当然了, 你也可以实现你自己的权限规则到 scopes 范围的映射和安装一个你自己版本的 OAuth2RequestFactory. AuthorizationServerEndpointConfigurer 配置对象允许你注入一个你自定义的 OAuth2RequestFactory, 因此你可以使用这个特性来设置这个工厂对象, 前提是你使用 @EnableAuthorizationServer 注解来进行配置 (见上面介绍的授权服务配置).
一个资源服务 (可以和授权服务在同一个应用中, 当然也可以分离开成为两个不同的应用程序) 提供一些受token令牌保护的资源, Spring OAuth 提供者是通过 Spring Security authentication filter 即验证过滤器来实现的保护, 你可以通过 @EnableResourceServer 注解到一个 @Configuration
配置类上, 并且必须使用 ResourceServerConfigurer
这个配置对象来进行配置 (可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法, 参数就是这个对象的实例). 下面是一些可以配置的属性:
@EnableResourceServer
注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter
的过滤器链,
使用授权服务的 /oauth/check_token
端点你需要将这个端点暴露出去, 以便资源服务可以进行访问, 这在咱们授权服务配置中已经提到了, 下面是一个例子:
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
在这个例子中, 我们配置了 /oauth/check_token
和 /oauth/token_key
这两个端点 (受信任的资源服务能够获取到公有密匙, 这是为了验证 JWT 令牌). 这两个端点使用了 HTTP Basic Authentication 即 HTTP 基本身份验证, 使用 client_credentials
授权模式可以做到这一点.
configure(ResourceServerSecurityConfigurer resources)
: 为资源服务器配置特定属性, 如 resource id.
org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurer#configure(ResourceServerSecurityConfigurer)
ResourceServerTokenServices
是组成授权服务的另一半, 如果你的授权服务和资源服务在同一个应用程序上的话, 你可以使用 DefaultTokenServices
, 这样你就不用考虑关于实现所有必要的接口的一致性问题, 这通常是很困难的. 如果你的资源服务器是分离开的, 那么你就必须要确保匹配授权服务提供的 ResourceServerTokenServices
, 它知道如何对令牌进行解码.
DefaultTokenServices
: 在授权服务器上, 你通常可以使用 DefaultTokenServices
并且选择一些主要的表达式通过 TokenStore
(后端存储或者本地编码).RemoteTokenServices
: 允许资源服务器通过 HTTP 请求来解码令牌 (也就是授权服务的 /oauth/check_token
端点). 如果你的资源服务没有太大的访问量的话, 那么使用 RemoteTokenServices
将会很方便 (所有受保护的资源请求都将请求一次授权服务用以检验 token 值), 或者你可以通过缓存来保存每一个 token 验证的结果.可以为每一个资源服务器 (可能是一个微服务实例) 设置一个 resource id. 在给客户端授权的时候, 可以设置这个客户端可以访问哪些微服务实例. 如果没有设置就是对所有的 resource 都有访问权限.
标注了 @EnableResourceServer
注解后, 会启用一个根据 OAuth 2.0 token 验证请求的 Spring Security 过滤器. 这个过滤器就是 OAuth2AuthenticationProcessingFilter
, 并且这个过滤器执行时机先于 FilterSecurityInterceptor
, 所以会先验证客户端有没有此 resource 的权限.
它会使用 OAuth2AuthenticationManager
来验证 token. 后者核心代码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
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 + ")");
}
checkClientDetails(auth);
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;
}
configure(HttpSecurity http)
: 配置资源的访问规则. 默认除了 /oauth/** 之外的所有资源都被保护
org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurer#configure(HttpSecurity)
在 ResourceServerConfigurer#configure(HttpSecurity)
的方法注释中可以看到默认一个名为 OAuth2WebSecurityExpressionHandler
的处理类已经被注入 (ResourceServerSecurityConfigurer
的 74 行: private SecurityExpressionHandler
). 在 ResourceServerSecurityConfigurer
的 configure(HttpSecurity http)
方法中为 HttpSecurity
对象注入了这个表达式处理器.
我们来看看这个处理器究竟是干嘛的, 要提到这个类就不得不说一下 StandardEvaluationContext
, 当解析属性, 方法或帮助执行类型转换的时候, Spring 提供了一个用于解析这类表达式的接口: EvaluationContext
. 它有两个实现:
SimpleEvaluationContext
: 暴露了 SpEL 核心特性和配置属性, 是 SpEL 的一个子集. 为那种不需要 SpEL 全部特性的表达式.StandardEvaluationContext
: 暴露 SpEL 的全部特性.通过下面这个简单的代码片段简述 StandardEvaluationContext
的作用:
/**
* A simple test for {@link StandardEvaluationContext}
*/
@Test
public void testStandardEvaluationContext() {
ExpressionParser expressionParser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(new Root(true));
context.setVariable("name", new Name("caplike"));
// 调用 rootObject 的方法
log.debug("{}", expressionParser.parseExpression("isRoot('caplike')").getValue(context, Boolean.class));
// 调用 name 所对应对象的 is 方法: false
log.debug("{}", expressionParser.parseExpression("#name.is('like')").getValue(context, Boolean.class));
// 调用 name 所对应对象的 is 方法: true
log.debug("{}", expressionParser.parseExpression("#name.is('caplike')").getValue(context, Boolean.class));
}
private static class Root {
private final boolean root;
public Root(Boolean root) { this.root = root; }
public boolean isRoot(String state) {
log.debug("{} :: Root#isRoot() called ...", state);
return root;
}
}
private static class Name {
private final String specifiedName;
public Name(String specifiedName) { this.specifiedName = specifiedName; }
public boolean is(String name) { return specifiedName.equals(name); }
}
控制台输出:
09:17:12.331 [main] DEBUG org.springframework.expression.spel.support.StandardEvaluationContextTest - caplike :: Root#isRoot() called ...
09:17:12.338 [main] DEBUG org.springframework.expression.spel.support.StandardEvaluationContextTest - true
09:17:12.339 [main] DEBUG org.springframework.expression.spel.support.StandardEvaluationContextTest - false
09:17:12.339 [main] DEBUG org.springframework.expression.spel.support.StandardEvaluationContextTest - true
从 OAuth2WebSecurityExpressionHandler
的继承关系可以看到其继承自 DefaultWebSecurityExpressionHandler
, 后者又继承自 AbstractSecurityExpressionHandler
. 在 OAuth2WebSecurityExpressionHandler
中我们看到:
protected StandardEvaluationContext createEvaluationContextInternal(Authentication authentication,
FilterInvocation invocation) {
StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, invocation);
ec.setVariable("oauth2", new OAuth2SecurityExpressionMethods(authentication));
return ec;
}
OAuth2SecurityExpressionMethods
: 包含了所有支持的方法签名, 如 hasScope(String)
. 调取方式为: #oauth2.hasScope('some scope')
, 同理, 再来看看 AbstractSecurityExpressionHandler
的源代码:
/**
* Invokes the internal template methods to create {@code StandardEvaluationContext}
* and {@code SecurityExpressionRoot} objects.
*
* @param authentication the current authentication object
* @param invocation the invocation (filter, method, channel)
* @return the context object for use in evaluating the expression, populated with a
* suitable root object.
*/
public final EvaluationContext createEvaluationContext(Authentication authentication, T invocation) {
SecurityExpressionOperations root = createSecurityExpressionRoot(authentication, invocation);
StandardEvaluationContext ctx = createEvaluationContextInternal(authentication, invocation);
ctx.setBeanResolver(br);
ctx.setRootObject(root);
return ctx;
}
SecurityExpressionOperations
则包含了所有 Spring Security 对于用户层级的方法签名, 如 hasRole(String)
. 由此可见, 在 configure(HttpSecurity http)
中配置 HttpSecurity
访问规则的时候可以使用 Spring Security OAuth 2.0 的支持客户端的表达式, 也可以使用 Spring Security 提供过的支持用户级别的表达式
CAS (Central Authentication Service), 中央认证服务. 一个基于 Kerberos 票据方式实现 SSO 单点登录的框架, 为 Web 应用系统提供一种可靠的单点登录解决方法 (属于 Web SSO)
SSO (Single Sign On), 是在多个应用系统中, 用户只需要登陆一次就可以访问所有相互信息的系统.
OAuth 2.0 是一个相对复杂的协议, 有4种授权模式, 其中的授权码模式在实现时可以使用 JWT 来生成授权码, 也可以不用. 它们之间没有必然的联系. OAuth 2.0 有 client 和 scope 的概念, JWT 没有. 如果只是拿来用于颁布 access-token 的话, 二者没区别. 常用的 bearer 算法 OAuth 2.0, JWT 都可以用, 只是应用场景不同而已.
EvaluationContext
- To Be Continued -
- 接下来的文章将会仔细探讨 Spring Security OAuth 2.0 的方方面面…