文章目录
关于 OAuth 2.0
OAuth 是一个关于授权的开放网络标准. 数据的所有者告诉系统, 同意授权第三方应用进入系统, 获取这些数据. 系统从而产生一个短期的进入令牌 (token), 用来代替密码, 供第三方应用使用.
OAuth 2.0 协议一共支持 4 种不同的授权模式:
- 授权码模式 (Authorization Code Grant): 常见的第三方平台登录功能基本都是使用这种模式;
- 简化模式 (Implicit Grant): 不需要客户端服务器参与, 直接在浏览器中向授权服务器申请令牌 (token), 一般如果网站是纯静态页面则可以采用这种方式.
- 密码模式 (Resource Owner Password Grant): 密码模式是用户把用户名密码直接告诉客户端, 客户端使用说这些信息向授权服务器申请令牌(accesstoken). 这需要用户对客户端高度信任, 例如客户端应用和服务提供商就是同一家公司, 我们自己做前后端分离登录就可以采用这种模式;
- 客户端模式 (Client Credentials Grant): 客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权, 严格来说, 客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案. 但是, 对于开发者而言, 在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的.
refresh_token - 更新令牌
令牌的有效期到了, 如果让用户重新走一遍上面的流程, 再申请一个新的令牌, 不仅体验差, 且没必要. 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=<refresh-token>
上面 URL 中, grant_type
参数为refresh_token
表示要求更新令牌, client_id
参数和client_secret
参数用于确认身份, refresh_token
参数就是用于更新令牌的令牌.
B 网站验证通过以后, 就会颁发新的令牌.
☆ 支持 Authorization Code Grant 和 Resource Owner Password Grant.
关于 Spring Security OAuth 2.0
Spring OAuth 2.0 提供者实际上分为:
- 授权服务 Authorization Server
- 资源服务 Resource Server
虽然两者有时候可能存在于同一个应用程序中, 但 Spring Security OAuth 中你可以把它们各自放在不同应用上, 而且你可以有多个资源服务, 它们共享同一个中央授权服务.所有获取令牌的请求都将在 Spring MVC controller endpoints 中处理, 并且访问受保护的资源服务的处理流程将会放在标准的 Spring Security 请求过滤器中.
下面是配置一个授权服务必须要实现的 endpoints:
- AuthorizationEndpoint: 用来作为请求者获得权限的服务, 默认 URL 是 /oauth/authorize
- TokenEndpoint: 用来作为请求者获取令牌的服务, 默认 URL 是 /oauth/token
下面是配置一个资源服务必须要实现的过滤器:
- OAuth2AuthenticationProcessingFilter: 用来作为认证令牌的一个处理流程过滤器. 只有当它放行后, 请求才能访问被保护的资源
Authorization Server - 授权服务配置
用 @EnableAuthorizationServer
来配置 OAuth 2.0 授权服务.接下来介绍几个配置类, 它们是由 Spring
创建的独立的配置对象, 会被 Spring 传入 AuthorizationServerConfigurer 中:
- ClientDetailsServiceConfigurer: 用来配置客户端详情服务, 客户端详情信息在这里初始化, 你能够把客户端详情硬编码在这里或是通过数据库来存取.
- AuthorizationServerEndpointsConfigurer: 用来配置授权以及令牌的访问端点和令牌服务;
- AuthorizationServerSecurityConfigurer: 用来配置令牌端点的安全约束.
(以上配置可以选择继承 AuthorizationServerConfigurerAdapter 并且覆盖其中的三个configure
方法来配置)
如果是授权码模式, 配置授权服务的一个比较重要的方面就是提供一个授权码给一个 OAuth 2.0 客户端, 一个授权码的获取是 OAuth 2.0 客户端跳转到一个授权页面, 然后通过验证之后服务器重定向到 OAuth 2.0 客户端, 并且在链接中附带返回一个授权码.
☀ ClientDetailsServiceConfigurer
void configure(ClientDetailsServiceConfigurer clients) throw Exception
ClientDetailsServiceConfigurer
, AuthorizationServerConfigurer
的一个回调配置项. 能够使用内存或者 JDBC 来实现客户端详情服务 (ClientDetailsService). 有几个重要属性:
- clientId: (required) 用来标识客户的 Id
- secret: (需要值得信任的客户端) 客户端的安全码, 如果有
- scope: 用来限制客户端的访问范围, 如果为空 (默认), 客户端就拥有全部的访问范围
- authorizedGrantTypes: 此客户端可以使用的授权类型, 默认为空.
authorization_code
,refresh_token
- authorities: 此客户端可以用的权限 (基于 Spring Security Authorities)
客户端详情 (Client Details) 能够在应用程序运行的时候更新, 可以通过访问底层的存储服务 (例如将客户端详情存储在一个关系数据库表中, 就可以使用 JdbcClientDetailsService) 或者通过 ClientDetailsManager 接口 (同时你也可以实现 ClientDetailsService 接口) 来管理
☀ AuthorizationServerEndpointsConfigurer
void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
AuthorizationServerEndpointsConfigurer
, AuthorizationServerConfigurer
的一个回调配置项. 用于配置授权以及令牌的访问端点和令牌服务.
☀ AuthorizationServerSecurityConfigurer
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
用以执行认证.
Odds And Ends
AuthorizationServerTokenServices - 管理令牌
AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理, 需要注意:
- 当一个令牌被创建了, 你必须保存, 这样当一个客户端使用这个令牌对资源服务请求的时候才可以应用到这个令牌
- 当一个令牌是有效的时候, 它可以被用来加载身份信息, 里面包含了这个令牌的相关权限
当自己创建 AuthorizationServerTokenServices 这个接口的实现时, 你可能需要考虑使用 DefaultTokenServices 这个类, 里面包含了一些有用的实现, 你可以使用它们来修改令牌的格式和令牌的存储. 默认的, 当它尝试创建一个令牌的时候, 是使用随机值来进行填充的, 除了持久化令牌是委托一个 TokenStore 接口来实现的以外, 这个类几乎帮你做了所有的事情. 并且 TokenStore 这个接口有一个默认的实现, 就是 InMemoryTokenStore, 即所有的令牌是被保存在了内存中. 除了使用这个类以外, 你还可以使用一些其他的预定义实现. 下面有几个版本, 它们都实现了 TokenStore 接口:
- InMemoryTokenStore: 这个版本的实现是被默认采用的. 它可以完美工作在但服务器上 (即访问并发量压力不大的情况下, 并且它在失败的时候不会进行备份), 大多数的项目都可以使用这个版本的实现来进行尝试, 你可以在开发的时候使用它来管理, 因为不会保存到磁盘中所以更易于调试.
- JdbcTokenStore: 这是一个机遇 JDBC 的实现的版本, 令牌会被保存进关系型数据库中. 使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息, 需要依赖 spring-jdbc
- JwtTokenStore: 这个版本的全称瑟吉欧 Json Web Token, 它可以把令牌相关数据编码, (因此对于后端来说, 不需要存储, 这是一个巨大优势), 但是它有一个缺点就是撤销一个已经授权的令牌会非常困难, 所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌 (refresh_token),. 另外一个缺点就是占用空间大. JwtTokenStore 不会保存任何数据, 但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的.
Json Web Token
使用 JWT 你需要在授权服务中使用 JwtTokenStore (依赖 spring-security-jwt), 资源服务器也需要一个解码的类: JwtAccessTokenConverter, JwtTokenStore 依赖这个来来解码以及编码, 因此你的授权服务以及资源服务都需要使用这个转换器. 令牌默认是有签名的, 并且资源服务需要验证这个签名, 因此你需要使用一个对称的 Key, 来参与签名计算, 这个 Key 存在于授权服务以及资源服务之中. 或者你可以使用非对称加密算法来对令牌签名.
公钥公布在 /oauth/token_key
这个 URL 连接中, 默认的访问安全规则是 denyAll()
, 即在默认情况下它是关闭的, 你可以注入一个标准的 SpEL 表达式到 AuthorizationServerSecurityConfigurer
配置中来开启它 (例如使用 permitAll()
来开启可能比较合适, 因为它是一个公钥.
Grant Type - 配置授权类型
授权是使用 AuthorizationEndpoint 和这个端点来控制的, 你能够使用 AuthorizationServerEndpointsConfigurer 这个对象的实例来配置 (AuthorizationServerConfigurer 的一个回调配置项). 如果不设置的话, 默认是支持除了密码模式外的所有模式. 接下来看一下这个配置对象的课配置属性:
- authenticationManager: 认证管理器, 当你选择了密码模式时, 清注入这个对象.
- userDetailsService: 如果你设置了这个属性, 那说明你有一个自己的 UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去 (例如 GlobalAuthenticationManagerConfigurer 这个配置对象), 当你设置了这个之后, 那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查: 用来确保这个账号是否仍然有效,假如说你禁用了这个账户
- authorizationCodeServices: 这个属性是用来设置授权码服务的 (即 AuthorizationCodeServices 的实例对象), 主要用于授权码模式
- implicitGrantService: 这个属性用于设置隐式授权模式, 用来管理隐式授权模式的状态
- tokenGranter: 这个属性就很牛B了, 当你设置了这个东西 (即 TokenGranter 接口实现), 那么授权将会交由你来完全掌控, 并且会忽略掉上面的这几个属性. 这个属性一般是用作拓展用途的, 即标准的四种授权模式已经满足不了你的需求的时候, 才会考虑使用这个
Endpoint URL - 配置授权端点
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
}
Enforcing SSL - 强制 SSL
使用简单的 HTTP 请求来进行测试是可以的. 但是如果你要部署到产品环境上的时候, 你应该永远都使用 SSL 来保护授权服务器在与客户端进行通讯的时候进行加密. 你可以把授权服务应用程序放到一个安全的运行容器中, 或者你可以使用一个代理, 如果你设置正确了的话它们应该工作的很好 (这样的话你就不需要设置任何东西了)
但是也许你可能希望使用 Spring Security 的 requiresChannel() 约束来保证安全, 对于授权端点来说 (还记得上面的列表吗, 就是那个 /authorize 端点). 它应该成为应用程序安全连接的一部分. 而对于 /token 令牌端点来说的话, 它应该有一个标记被配置在 AuthorizationServerEndpointsConfigurer 配置对象中, 你可以使用 sslOnly() 方法来进行设置. 这两个设置是可选的. 不过在以上两种情况中, 会导致Spring Security 会把不安全的请求通道重定向到一个安全通道中. (即将 HTTP 请求重定向到 HTTPS 请求上)
Error Handling - 错误处理
端点实际上就是一个特殊的 Controller, 它用于返回一些对象数据
授权服务的错误信息是使用标准的 Spring MVC 来进行处理的, 也就是 @ExceptionHandler 注解的端点方法. 你也可以提供一个 WebResponseExceptionTranslator 对象. 最好的方式是改变响应的内容而不是直接进行渲染.
假如说在呈现令牌端点的时候发生了异常, 那么异常委托了 HttpMessageConverters 对象 (它能够被添加到 MVC 配置中) 来输出. 假如说在呈现授权端点的时候未通过验证, 则会被重定向到 /oauth/error 即错误信息端点中. whitelabel error (即 Spring 框架提供的一个默认错误页面) 错误端点提供了HTML的响应, 但是你大概可能需要实现一个自定义错误页面 (例如只是简单的增加一个 @Controller 映射到请求路径上 @RequestMapping("/oauth/error")).
Mapping User Roles to Scopes
有时候限制令牌的权限范围是很有用的, 这不仅仅是针对于客户端, 你还可以根据用户的权限来进行限制. 如果你使用 DefaultOAuth2RequestFactory 来配置 AuthorizationEndpoint 的话你可以设置一个flag即 checkUserScopes=true 来限制权限范围, 不过这只能匹配到用户的角色. 你也可以注入一个 OAuth2RequestFactory 到 TokenEnpoint 中, 不过这只能工作在 password 授权模式下. 如果你安装一个 TokenEndpointAuthenticationFilter 的话, 你只需要增加一个过滤器到 HTTP BasicAuthenticationFilter 后面即可. 当然了, 你也可以实现你自己的权限规则到 scopes 范围的映射和安装一个你自己版本的 OAuth2RequestFactory. AuthorizationServerEndpointConfigurer 配置对象允许你注入一个你自定义的 OAuth2RequestFactory, 因此你可以使用这个特性来设置这个工厂对象, 前提是你使用 @EnableAuthorizationServer 注解来进行配置 (见上面介绍的授权服务配置).
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
端点你需要将这个端点暴露出去, 以便资源服务可以进行访问, 这在咱们授权服务配置中已经提到了, 下面是一个例子:
@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
授权模式可以做到这一点.
☀ 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 都有访问权限.
标注了 @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;
}
☀ HttpSecurity
configure(HttpSecurity http)
: 配置资源的访问规则. 默认除了 /oauth/** 之外的所有资源都被保护
org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurer#configure(HttpSecurity)
OAuth2WebSecurityExpressionHandler
在 ResourceServerConfigurer#configure(HttpSecurity)
的方法注释中可以看到默认一个名为 OAuth2WebSecurityExpressionHandler
的处理类已经被注入 (ResourceServerSecurityConfigurer
的 74 行: private SecurityExpressionHandler<FilterInvocation> expressionHandler = new OAuth2WebSecurityExpressionHandler();
). 在 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 提供过的支持用户级别的表达式
OAuth 2.0 & CAS’s SSO
CAS (Central Authentication Service), 中央认证服务. 一个基于 Kerberos 票据方式实现 SSO 单点登录的框架, 为 Web 应用系统提供一种可靠的单点登录解决方法 (属于 Web SSO)
SSO (Single Sign On), 是在多个应用系统中, 用户只需要登陆一次就可以访问所有相互信息的系统.
- CAS’s SSO 是保障客户端的用户资源的安全. OAuth 2.0 则是保障服务端的用户资源的安全.
- CAS’s SSO 要确认的最终信息是: 这个用户到底有没有权限访问我 (CAS 客户端) 的资源. OAuth 2.0 是我 (OAuth 2.0 服务提供方) 的用户资源到底能不能让你 (OAuth 2.0 的客户端) 访问.
- CAS’s SSO 资源都在客户端这边, 不在 CAS 服务器那边, 是用户想访问 CAS 客户端的资源. 用户在给 CAS 服务端提供用户名和密码后, 作为 CAS 客户端并不知道这件事. OAuth 2.0 资源都在 OAuth 2.0 服务提供者那边, 是客户端想访问用户的资源. 所以在最安全的模式下, 用户授权后服务端不能直接返回 access-token, 通过重定向给客户端, 因为这个 access-token 可能被黑客截取, 于是服务端发送一个了一个授权码通过重定向给客户端. 客户端在后台通过 HTTPS 的方式用授权码以及一串客户端和服务端预先商量好的密码, 才能获取 access-token 和 refresh-token.
OAuth 2.0 & JWT
- OAuth 2.0 用在使用第三方账号登录的情况 (比如使用 weibo, qq, github 登录某个 app)
- JWT 是用在前后端分离, 需要对后台 API 进行保护时使用 (前后端分离无 session, 频繁传用户密码不安全)
OAuth 2.0 是一个相对复杂的协议, 有4种授权模式, 其中的授权码模式在实现时可以使用 JWT 来生成授权码, 也可以不用. 它们之间没有必然的联系. OAuth 2.0 有 client 和 scope 的概念, JWT 没有. 如果只是拿来用于颁布 access-token 的话, 二者没区别. 常用的 bearer 算法 OAuth 2.0, JWT 都可以用, 只是应用场景不同而已.
Reference
- Spring OAuth 2.0 scope vs authorities(roles)
- What’s the differences between Spring Security and Spring Security OAuth 2.0?
- OAuth 2.0 Developers Guide
- 4.1.1. Understanding
EvaluationContext
修订记录
- 2020-6-29 11:18:37 The Very First Version.
- To Be Continued -
- 接下来的文章将会仔细探讨 Spring Security OAuth 2.0 的方方面面…