Spring Security OAuth 2.0

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343