OAuth 2.0 Provider 实现
在OAuth 2.0中,provider角色事实上是把授权服务和资源服务分开,有时候它们也可能在同一个应用中,用Spring Security OAuth你可以选择把它们分成两个应用,当然多个资源服务可以共享同一个授权服务。
获取token的请求由Spring MVC的控制端点处理,访问受保护的资源由标准的Spring Security请求过滤器处理。
为了实现OAuth 2.0授权服务器,在Spring Security的过滤器链中需要有以下端点:
- AuthorizationEndpoint:用于服务授权请求。默认URL是/oauth/authorize
- TokenEndpoint:用于服务访问令牌请求。默认URL是/oauth/token
在OAuth 2.0的资源服务器中需要实现下列过滤器:
- OAuth2AuthenticationProcessingFilter:用于加载认证
对于所有的OAuth 2.0 provider特性,最简单的配置是用Spring OAuth @Configuration适配器。
Authorization Server 配置
只要你配置了授权服务器,那么你应该考虑客户端用于获取access token的授权类型(例如,授权码,用户凭证,刷新token)。
服务器的配置是用来提供client detail服务和token服务的,并且可以启用或者禁用全局的某些机制。
每个客户端可以配置不同的权限
@EnableAuthorizationServer注解被用来配置授权服务器,也可以和实现了AuthorizationServerConfigurer接口的任意被标记为@Bean的Bean一起来对授权服务器进行配置。
下列特性被委托给AuthorizationServerConfigurer:
- ClientDetailsServiceConfigurer:定义客户端详细信息服务的配置(a configurer that defines the client details service)
- AuthorizationServerSecurityConfigurer:定义令牌终结点上的安全约束(defines the security constraints on the token endpoint)
- AuthorizationServerEndpointsConfigurer:定义授权和令牌端点以及令牌服务(defines the authorization and token endpoints and the token services)
一件重要的事情是,provider配置了将授权码给OAuth客户端的方式(PS:在授权码类型授权过程中)
OAuth客户端通过将end-user(最终用户)导向授权页,用户可用在此输入他的凭证。之后,授权服务器携带授权码通过重定向的方式将授权码返回给客户端。
配置 Client Details
ClientDetailsServiceConfigurer可用使用client details service的两种实现中的任意一种:in-memory 或者 JDBC
客户端重要的属性是:
- clientId:(必须的)客户端ID
- secret:(对于信任的客户端需要)客户端秘钥
- scope:客户端被限定的范围。如果scope为定义或者为空(默认为空)则客户端不受scope限制
- authorizedGrantTypes:客户端使用到的授权类型
- authorities:授予客户端的权限
客户端details可以在应用运行时被更新,通过直接访问存储(例如:如果用JdbcClientDetailsService的话可以实时改变数据库表中的数据)或者通过实现ClientDetailsManager接口(它们也都实现了ClientDetailsService接口)。
注意:用于JDBC服务的数据库schema并没有打包到library中(因为你再实际使用的时候可能有诸多差异),但是这里有一个例子你可以参考一下。
spring-security-oauth官方表结构文件:https://github.com/spring-projects/spring-security-oauth/blob/main/spring-security-oauth2/src/test/resources/schema.sql
客户端详细信息表:oauth_client_details
对oauth_client_details的操作主要集中在JdbcClientDetailsService.java中
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
字段 | 注释 |
---|---|
client_id | 主键,客户端ID |
resource_ids | 客户端所能访问的资源id集合,多个资源时用逗号(,)分隔 |
client_secret | 客户端访问密匙 |
scope | 客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔 |
authorized_grant_types | 客户端支持的授权许可类型(grant_type),可选值包括authorization_code,password,refresh_token,implicit,client_credentials,若支持多个授权许可类型用逗号(,)分隔 |
web_server_redirect_uri | 客户端重定向URI,当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与数据库内的redirect_uri是否一致 |
authorities | 客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔 |
access_token_validity | 设定客户端的access_token的有效时间值(单位:秒),若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时) |
refresh_token_validity | 设定客户端的refresh_token的有效时间值(单位:秒),若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天) |
additional_information | 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据 |
create_time | 记录创建时间 |
autoapprove | 设置用户是否自动批准授予权限操作, 默认值为 ‘false’, 可选值包括 ‘true’,‘false’, ‘read’,‘write’ |
客户端系统中存储从服务端获取的token:oauth_client_token
该表用于在客户端系统中存储从服务端获取的token数据,
create table oauth_client_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
字段 | 注释 |
---|---|
token_id | 从服务器端获取到的access_token的值 |
token | 这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进制数据 |
authentication_id | 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的,具体实现请参考DefaultClientKeyGenerator.java类 |
user_name | 登录时的用户名 |
client_id | 主键,客户端ID |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
认证授权Token记录表:oauth_access_token
对oauth_access_token表的操作主要集中在JdbcTokenStore.java中
create table oauth_access_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication LONGVARBINARY,
refresh_token VARCHAR(256)
);
字段 | 注释 |
---|---|
client_id | 认证授权客户端ID |
token_id | Token标识:通过MD5加密access_token的值 |
token | OAuth2AccessToken.java对象序列化内容 |
user_name | 用户名,若客户端没有用户名则该值等于client_id |
authentication_id | 根据当前的username、client_id与scope通过MD5加密生成该字段的值 |
authentication | OAuth2Authentication.java对象序列化内容 |
refresh_token | RefreshToken标识:通过MD5加密refresh_token的值 |
create_time | 记录创建时间 |
刷新授权Token记录表:oauth_refresh_token
对oauth_refresh_token的操作主要集中在JdbcTokenStore.java中
create table oauth_refresh_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication LONGVARBINARY
);
字段 | 注释 |
---|---|
token_id | RefreshToken标识:通过MD5加密refresh_token的值 |
token | OAuth2RefreshToken.java对象序列化内容 |
authentication | OAuth2Authentication.java对象序列化内容 |
create_time | 记录创建时间 |
授权码Code记录表:oauth_code
对oauth_code的操作主要集中在JdbcAuthorizationCodeServices.java中
create table oauth_code (
code VARCHAR(256), authentication LONGVARBINARY
);
字段 | 注释 |
---|---|
authentication | AuthorizationRequestHolder.java对象序列化内容 |
code | 存储服务端系统生成的code的值(未加密) |
授权记录:oauth_approvals
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
字段 | 注释 |
---|---|
userId | 登录的用户名 |
clientId | 客户端ID |
scope | 申请的权限范围 |
status | 状态(Approve或Deny) |
expiresAt | 过期时间 |
lastModifiedAt | 最终修改时间 |
自定义customized oauth_client_details表
-- customized oauth_client_details table
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
管理Tokens
AuthorizationServerTokenServices定义了管理OAuth 2.0 Token所必须的操作。请注意:
- 当创建一个access token的时候,这个认证必须被存储起来,以便后续访问资源的时候对接收到的access token进行引用校验。
- access token用来加载认证
当你实现了AuthorizationServerTokenServices接口,你可能考虑用DefaultTokenServices。有许多内置的插件化的策略可以用来改变access token的格式和存储。
默认情况下,用随机值来生成token,并且用TokenService来处理所有(除了token持久化以外)事情。默认的存储是in-memory实现,但是有其它的实现可以使用。
对于单服务器而言,默认的InMemoryTokenStore是完美的。大多数的项目是从这里开始的,为了使它很容易启动,也不需要其它依赖,并且可能以开发模式进行操作。
JdbcTokenStore是JDBC版本的Token存储。它把Token数据存储到关系型数据库中。为了使用JdbcTokenStore需要classpath下有"spring-jdbc"。
JSON Web Token (JWT) 它将授权的token的所有数据进行编码后存储(没有使用后端存储是它最大的优势)。这种方式的一个缺点是你不能很容易的撤销一个access token,因此一般用该方式存储的token的有效期很短,并且在刷新token的时候之前的token会被废除。另一个缺点是,token很长,因为它里面存了很多关于用户凭证的信息。JwtTokenStore不会真的存储数据,它不持久化任何数据。但是在DefaultTokenServices中,它扮演着token值和认证信息转换的角色。
注意:对于JDBC的schema没有打包到library中,但是这儿有一个例子你可以参考一下test code in github。确保用@EnableTransactionManagement来防止多个客户端在同一行创建token。注意,示例中的schema都有明确地主键声明,在并发环境中这是必须的。
JWT Tokens
为了使用JWT Tokens,你需要在你的授权服务器中有一个JwtTokenStore。资源服务器也需要解码这个token,所以JwtTokenStore有一个依赖JwtAccessTokenConverter,相同的实现需要被包含在授权服务器和资源服务器中。也就是说,授权服务器和资源服务器中都需要JwtTokenStore实现。默认情况下,token是被签名的,而且资源服务器必须能够校验这个签名,因此需要有相同的对称key,或者需要公钥来匹配授权服务器上的私钥。公钥被授权服务器暴露在/oauth/token_key端点,默认情况下这个端点的访问规则是"denyAll()"。你可以用标准的SpEL表达式(例如:permitAll())到AuthorizationServerSecurityConfigurer来开放它。
为了使用JwtTokenStore,在classpath下需要有"spring-security-jwt"
Grant Types
授权类型通过AuthorizationEndpoint来支持。默认情况下,除了password以外,所有授权类型都支持。下面是授权类型的一些属性:
- authenticationManager:通过注入一个AuthenticationManager来切换成password授权
- userDetailsService:如果你注入一个UserDetailsService或者以任意方式配置了一个全局的UserDetailsService(例如:在GlobalAuthenticationManagerConfigurer中),那么一个刷新token将被包含在user detail中,为了强制账户是激活的。
- authorizationCodeServices:定义授权码服务(AuthorizationCodeServices的实例)
- implicitGrantService:在隐式授权期间管理状态
- tokenGranter:tokenGranter
Configuring the Endpoint URLs
AuthorizationServerEndpointsConfigurer有一个pathMapping()方法。它有两个参数:
- 端点的默认URL路径
- 自定义的路径(必须以"/"开头)
下面是框架提供的URL路径:
- /oauth/authorize:授权端点
- /oauth/token:令牌端点
- /oauth/confirm_access:用户批准授权的端点
- /oauth/error:用于渲染授权服务器的错误
- /oauth/check_token:资源服务器解码access token
- /oauth/check_token:当使用JWT的时候,暴露公钥的端点
授权端点/oauth/authorize应该被保护起来,以至于它只能被认证过的用户访问。下面是一个例子,用标准的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
}
注意:如果您的授权服务器同时也是一个资源服务器的话,那么就有另一个具有较低优先级的安全过滤器链来控制API资源。通过访问令牌来保护这些请求,你需要它们的路径不能与主用户过滤器链中的那些相匹配,所以请确保包含一个请求matcher,它只挑选出上面的WebSecurityConfigurer中的非api资源。
Customizing the UI
授权服务器的大多数端点主要都是被机器使用的,但是有两个资源是需要UI,它们分别是/oauth/confirm_access和HTML响应/oauth/error。框架为它们提供的实现是空白页,真实的情况是大多数授权服务器可能想要提供它们自己的实现来控制样式和内容。所以,你需要做的事情就是提供一个Spring MVC 被标注了@RequestMappings注解的Controller来映射这些端点,并且框架将用一个低的优先级来发放请求。在默认的/oauth/confirm_access你期望一个AuthorizationRequest绑定到session。你可以抓取请求的所有数据并按照自己喜欢的方式渲染它们,然后用户需要做的就是向/oauth/authorize发送关于批准或拒绝授予的信息。默认的UserApprovalHandler取决于是否你再AuthorizationServerEndpointsConfigurer中提供了一个ApprovalStore。标准的审批处理器如下:
- TokenStoreUserApprovalHandler:通过user_oauth_approval做一个简单的yes/no决定等同于“true”或“false”
- ApprovalStoreUserApprovalHandler:一组"scope*"参数key。参数的值可以是"true"或者"approval"。至少有一个scope是approval才算是授权成功。(A grant is successful if at least one scope is approved.)
强制SSL
纯HTTP对于测试来说是可以的,但是在生成中授权服务器应该使用SSL。你可以在一个安全的容器或代理后面运行应用程序,如果你正确地设置代理和容器(这与OAuth2无关),那么它应该可以正常工作。对于/authorize端点你需要把它当作正常的应用安全的一部分来做,对于/token端点在AuthorizationServerEndpointsConfigurer中有一个标记可以设置,通过用sslOnly()方法。
自定义错误处理
授权服务器用标准的Spring MVC特性来进行错误处理。
你可以提供自己的实现,通过添加@Controller并且带上@RequestMapping("/oauth/error")
Mapping User Roles to Scopes
有时候,为了限制token的scope,不仅仅要根据指定的客户端的范围,也要根据用户自己的权限来进行限制。如果你在你的AuthorizationEndpoint用DefaultOAuth2RequestFactory,你可以设置checkUserScopes=true来限制匹配的用户角色的允许范围。AuthorizationServerEndpointsConfigurer允许你注入一个自定义的OAuth2RequestFactory
资源服务器配置
一个资源服务器(可能与授权服务器是相同的应用,也可能与授权服务器是分开的应用)通过OAuth2 Token服务受保护的资源。
Spring OAuth 提供一个Spring Security认证过滤器来实现这个保护。你可以
你可以在一个@Configuration类上用@EnableResourceServer来切换它,并且用ResourceServerConfigurer配置它。下列特性可以被配置:
- tokenServices :一个ResourceServerTokenServices的实例
- resourceId :资源ID(推荐的,如果存在的话会被授权服务器校验)
- 资源服务器的其它扩展端点
- request matchers for protected resources (defaults to all)
- access rules for protected resources (defaults to plain "authenticated")
- 其它通过HttpSecurity配置的自定义的受保护的资源
@EnableResourceServer注释将自动添加一个OAuth2AuthenticationProcessingFilter类型的过滤器到Spring安全过滤器链中。
OAuth 2.0 客户端
受保护的资源配置
受保护的资源(或者叫远程资源)可以用OAuth2ProtectedResourceDetails类型的bean来定义。一个被保护的资源由下列属性:
- id:资源的id。这个id只是用于客户端查找资源。
- clientId:OAuth Client id。
- clientSecret:关联的资源的secret。默认非空
- accessTokenUri:提供access_token的端点的uri
- scope:逗号分隔的字符串,代表访问资源的范围。默认为空
- clientAuthenticationScheme:客户端认证所采用的schema。建议的值:"http_basic"和"form"。默认是"http_basic"。
不同的授权类型有不同的OAuth2ProtectedResourceDetails的具体实现(例如:ClientCredentialsResourceDetails是"client_credentials"类型的具体实现)
- userAuthorizationUri :用户授权uri,非必需的。
客户端配置
对于OAuth 2.0客户端配置,简化的配置用@EnableOAuth2Client。这个注解做两件事情:
- 创建一个过滤器(ID是oauth2ClientContextFilter)来存储当前的请求和上下文。在请求期间需要进行身份认证时,它管理重定向URI。
- 在请求范围内创建一个AccessTokenRequest类型的bean。对于授权代码(或隐式)授予客户端是很有用的,可以避免与单个用户相关的状态发生冲突。
AccessTokenRequest可以用在一个OAuth2RestTemplate中,就像下面这样:
@Autowired
private OAuth2ClientContext oauth2Context;
@Bean
public OAuth2RestTemplate sparklrRestTemplate() {
return new OAuth2RestTemplate(sparklr(), oauth2Context);
}
访问受保护的资源
建议用RestTemplate访问受保护的资源。
Spring Security为OAuth提供了一个扩展的RestTemplate只需要你提供一个OAuth2ProtectedResourceDetails的实例即可。为了使它和用户token(授权码方式授权)一起使用,你应该考虑用@EnableOAuth2Client配置。
一般来说,web应用程序不应该使用密码授予,因此如果您可以支持AuthorizationCodeResourceDetails,请避免使用ResourceOwnerPasswordResourceDetails,App交互使用密码授权。
为了和用户令牌(授权码)一起使用,你应该考虑用@EnableOAuth2Client配置。
客户端持久化Token
客户端不需要持久化令牌,但是最好不要在每次重启客户端应用程序时都要求用户批准新的令牌授予。
ClientTokenServices接口定义了为特定用户保存OAuth 2.0令牌所需的操作。这是一个JDBC实现,但是如果您希望实现自己的服务,以便在持久数据库中存储访问令牌和相关的身份验证实例,则可以这样做。如果你想要使用这个特性,你需要为OAuth2RestTemplate提供一个经过特殊配置的TokenProvider。例如:
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2RestOperations restTemplate() {
OAuth2RestTemplate template = new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(accessTokenRequest));
AccessTokenProviderChain provider = new AccessTokenProviderChain(Arrays.asList(new AuthorizationCodeAccessTokenProvider()));
provider.setClientTokenServices(clientTokenServices());
return template;
}
参考:
https://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://www.cnblogs.com/cjsblog/p/9184173.html
https://blog.csdn.net/yangxiao_hui/article/details/109100140