1.1 简介
1.1.1 概述
OAuth(开放授权,Open Authorization)是一个开放标准,为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth 是安全的。OAuth 2.0 是 OAuth 协议的延续版本,但不向后兼容 OAuth 1.0 即完全废止了 OAuth 1.0。很多大公司如 Google,Yahoo,Microsoft 等都提供了 OAuth 认证服务,这些都足以说明 OAuth 标准逐渐成为开放资源授权的标准。Oauth 协议目前发展到 2.0 版本,1.0 版本过于复杂,2.0 版本已得到广泛应用。Spring-Security-OAuth2 是对 OAuth2 的一种实现,并且跟 Spring Security 相辅相成,与 Spring Cloud 体系的集成也非常便利,最终使用它实现分布式认证授权解决方案。
1.1.2 四种授权方式
☞ 授权码模式(authorization code)
第三方先获取授权码,然后用该授权码获取授权。这种方式是最常用,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。一般流程为:
♞ 用户打开客户端,客户端要求资源拥有者给予授权,浏览器重定向到认证中心(含有客户端信息)
♞ 跳转后,网站会要求用户登录,然后询问是否同意给予授权,这一步需要用户事先具有资源的使用权限。
♞ 认证中心通过第一步提供的回调地址将授权码返回给服务端。注意这个授权码并非通行凭证。
♞ 服务端拿着授权码向认证中心索要访问 access_token,认证中心返回 token 和 refresh token
☞ 简化模式(implicit)
一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。这时就不能用授权码模式,必须将令牌储存在前端,token 直接暴露再浏览器。这种方式没有授权码这个中间步骤,所以称为授权码简化模式。一般流程为:
♞ 用户打开客户端,客户端要求资源拥有者给予授权,浏览器重定向到认证中心(含有客户端信息)
♞ 跳转后,网站会询问是否同意给予授权
♞ 认证中心将授权码将 access_token 以 Hash 的形式存放在重定向 uri 的 fargment 中发送给浏览器。( fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的未尾通过 # 作为 fragment 的开头,其中 # 不属于 fragment 的值)
☞ 密码模式(resource owner password credentials)
这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了客户端,因此这就说明这种模式只能用于客户端是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生 app 或第一方单页面应用。一般流程为:
♞ 用户再客户端填写用户名、密码
♞ 客户端拿着资源拥有者的用户名、密码向认证中心请求 access_token
♞ 认证中心给客户端返回 access_token
☞ 客户端模式(client credentials)
这种模式其实已经不太属于 OAuth2 的范畴了。A 服务完全脱离用户,以自己的身份去向 B 服务索取 token。换言之,用户无需具备 B 服务的使用权也可以。完全是 A 服务与 B 服务内部的交互,与用户无关了。适用于没有前端的命令行应用。一般流程为:
♞ 客户端向认证中心发送自己的身份信息,并请求 access_token
♞ 确认客户端身份无误后,将 access_token 发送给客户端
1.2 表结构说明
1.2.1 官方 SQL
/*
SQLyog Ultimate v12.08 (64 bit)
MySQL - 8.0.16 : Database - security_authority
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*Table structure for table `oauth_access_token` */
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` longblob,
`refresh_token` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `oauth_access_token` */
/*Table structure for table `oauth_approvals` */
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(255) DEFAULT NULL,
`clientId` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` datetime DEFAULT NULL,
`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `oauth_approvals` */
/*Table structure for table `oauth_client_details` */
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(255) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `oauth_client_details` */
/*Table structure for table `oauth_client_token` */
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `oauth_client_token` */
/*Table structure for table `oauth_code` */
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL,
`authentication` varbinary(2550) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `oauth_code` */
/*Table structure for table `oauth_refresh_token` */
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication` longblob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `oauth_refresh_token` */
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
1.2.2 oauth_client_details【核心表】
字段名 | 描述 |
---|---|
client_id | 主键,必须唯一且不能为空。用于唯一标识每一个客户端,在注册时必须填写(也可由服务端自动生成),对于不同的 grant_type,该字段都是必须的。在实际应用中的另一个名称叫 appKey,与 client_id 是同一个概念 |
resource_ids | 客户端所能访问的资源 id 集合,多个资源时用逗号分隔。当注册客户端时,根据实际需要可选择资源 id,也可根据不同的注册流程,赋予对应的资源 id |
client_secret | 用于指定客户端的访问密匙;在注册时必须填写(也可由服务端自动生成)。对于不同的 grant_type,该字段都是必须的。在实际应用中的另一个名称叫 appSecret,与 client_secret 是同一个概念. |
scope | 指定客户端申请的权限范围,可选值包括 read、write、trust。若有多个权限范围用逗号分隔。在实际应该中,该值一般由服务端指定,常用的值为 read,write |
authorized_grant_types | 指定客户端支持的 grant_type,可选值包括authorization_code、password、refresh_token、implicit、client_credentials。若支持多个 grant_type 用逗号分隔,在实际应用中,该字段是一般由服务器端指定的,而不是由申请者去选择的。 |
web_server_redirect_uri | 客户端的重定向 URI 可为空,当 grant_type 为 authorization_code 或 implicit 时,在 Oauth 的流程中会使用并检查与注册时填写的 redirect_uri 是否一致 |
authorities | 指定客户端所拥有的 Spring Security 的权限值,可选,若有多个权限值用逗号分隔。对于是否要设置该字段的值,要根据不同的 grant_type 来判断。若客户端在 Oauth 流程中需要用户的用户名与密码的(authorization_code、password),则该字段可以不需要设置值,因为服务端将根据用户在服务端所拥有的权限来判断是否有权限访问对应的 API。客户端在 Oauth 流程中不需要用户信息的(implicit、client_credentials),则该字段必须要设置对应的权限值,因为服务端将根据该字段值的权限来判断是否有权限访问对应的 API |
access_token_validity | 设定客户端的 access_token 的有效时间值(单位:秒),可选,若不设定值则使用默认的有效时间值(60 * 60 * 12 = 12 小时)。在实际应用中,该值一般是由服务端处理的,不需要客户端自定义 |
refresh_token_validity | 设定客户端的 refresh_token 的有效时间值(单位:秒),可选,若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30 = 30 天)。若客户端的 grant_type 不包括 refresh_token,则不用关心该字段。在实际应用中,该值一般是由服务端处理的,不需要客户端自定义 |
additional_information | 这是一个预留的字段,在 Oauth 的流程中没有实际的使用,可选,但若设置值,必须是 JSON 格式的数据。在实际应用中,可以用该字段来存储关于客户端的一些其他信息,如客户端的国家、地区、注册时的 IP 地址等等 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
autoapprove | 设置用户是否自动 Approval 操作,默认值为 false,可选值包括 true、false、read、write 该字段只适用于grant_type="authorization_code" 的情况,当用户登录成功后,若该值为 true 或支持的 scope 值,则会跳过用户 Approve 的页面,直接授权 |
1.2.3 oauth_client_token
字段名 | 描述 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 从服务器端获取到的 access_token 的值 |
token | 这是一个二进制的字段,存储的数据是 OAuth2AccessToken.java 对象序列化后的二进制数据. |
authentication_id | 该字段具有唯一性,是根据当前的 username(如果有),client_id 与 scope 通过 MD5 加密生成的 |
user_name | 登录时的用户名 |
client_id | 唯一标识每一个客户端 |
1.2.4 oauth_access_token
字段名 | 描述 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 该字段的值是将 access_token 的值通过 MD5 加密后存储的 |
token | 存储将 OAuth2AccessToken.java 对象序列化后的二进制数据, 是真实的 AccessToken 的数据值 |
authentication_id | 该字段具有唯一性,其值是根据当前的 username(如果有),client_id 与 scope 通过 MD5 加密生成的 |
user_name | 登录时的用户名,若客户端没有用户名,则该值等于 client_id |
client_id | 唯一标识每一个客户端 |
authentication | 存储将 OAuth2Authentication.java 对象序列化后的二进制数据 |
refresh_token | 该字段的值是将 refresh_token 的值通过 MD5 加密后存储的 |
1.2.5 oauth_refresh_token
字段名 | 描述 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 该字段的值是将 refresh_token 的值通过 MD5 加密后存储的 |
token | 存储将 OAuth2RefreshToken.java 对象序列化后的二进制数据 |
authentication | 存储将 OAuth2Authentication.java 对象序列化后的二进制数据 |
1.2.6 oauth_code
字段名 | 描述 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
code | 存储服务端系统生成的 code(授权码) 的值(未加密) |
authentication | 存储将 AuthorizationRequestHolder.java 对象序列化后的二进制数据 |
1.3 授权服务
1.3.1 相关依赖
<!-- 其实这里这里创建的是 SpringCloud 项目,省略注册中心等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
1.3.2 服务配置
server:
port: 8081
spring:
datasource:
# mysql 驱动 6.0 以上使用如下配置
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db?serverTimezone=UTC
username: root
password: root
jpa:
database: MySQL
show-sql: true
generate-ddl: true
hibernate:
ddl-auto: update # 没有表创建表,有表更新表
naming:
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl # 命名策略
main:
# 允许覆盖 OAuth2 放在容器中的 Bean 对象
allow-bean-definition-overriding: true
1.3.3 Security 配置文件
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/26
* @description Security 配置文件
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl UserServiceImpl;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(UserServiceImpl).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.anyRequest().authenticated();
}
// AuthenticationManager 对象在 OAuth2 认证服务中要使用,提前放入 IOC 容器中
// 不注入没有 password grant_type
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
1.3.4 OAuth2 配置文件
☞ OAuthServerConfig
可以用 @EnableAuthorizationServer 注解并继承 AuthorizationServerConfigurerAdapter 来配置 OAuth2.0 授权。AuthorizationServerConfigurerAdapter 要求配置以下几个类,这几个类是由 Spring 创建的独立的配置对象,它们会被 Spring 传入 AuthorizationServerConfigurer 中进行配置。
♞ ClientDetailsServiceConfigurer
:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,可以把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
♞ AuthorizationServerEndpointsConfigurer
:用来配置令牌(token)的访问端点和令牌服务(token services)。
♞ AuthorizationServerSecurityConfigurer
:用来配置令牌端点的安全约束。
☞ 配置客户端详细信息
ClientDetailsServiceConfigurer 能够使用内存或者 JDBC 来实现客户端详情服务(ClientDetailsService),ClientDetailsService 负责查找 ClientDetails,而 ClientDetails 有几个重要的属性:
♞ clientld
:用来标识客户的 id,必须。
♞ secret
:客户端安全码,如果有的话。
♞ scope
:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
♞ authorizedGrantTypes
:此客户端可以使用的授权类型,默认为空。
♞ authorities
:此客户端可以使用的权限【基于Spring Security authorities】。
客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务,例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService 或者通过自己实现 ClientRegistrationService 接口,也可以实现 ClientDetailsService 接口来进行管理。
♘ 来源于数据库
// 数据库连接池对象,SpringBoot 配置完成后自动注入
@Autowired
private DataSource dataSource;
// 客户端信息来源
@Bean
public ClientDetailsService jdbcClientDetailsService(){
return new JdbcClientDetailsService(dataSource);
}
// 指定客户端信息的数据库来源
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
♘ 来源于内存
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 客户端 id
.withClient("java")
// 客户端密码
.secret(new BCryptPasswordEncoder().encode("123456"))
// 资源 id
.resourceIds("res")
// 该客户端允许授权的类型
.authorizedGrantTypes("authorization_code","refresh_token")
// 允许授权的范围
.scopes("all")
// 验证回调地址
.redirectUris("http://www.baidu.com");
}
☞ 管理令牌
AuthorizationServerTokenServices 接口定义了一些操作可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。自定义 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类,里面包含了一些有用实现,可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了 TokenStore 接口:
♞ InMemoryTokenStore
:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
♞ JdbcTokenStore
:这是一个基于 JDBC 的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息。
♞ JwtTokenStore
:这个版本的全称是 JSON Web Token,它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。jwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。
// token 保存策略,指你生成的 Token 要往哪里存储
// 存在数据库:new JdbcApprovalStore(dataSource),存在内存中:new InMemoryTokenStore()
@Bean
public TokenStore tokenStore(){
return new JdbcTokenStore(dataSource);
}
// 客户端信息来源
@Bean
public ClientDetailsService jdbcClientDetailsService(){
return new JdbcClientDetailsService(dataSource);
}
// 令牌管理
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
// token 保存策略
tokenServices.setTokenStore(tokenStore());
// 支持刷新模式,即 refresh token 会同步刷新
tokenServices.setSupportRefreshToken(true);
// 客户端信息来源
tokenServices.setClientDetailsService(jdbcClientDetailsService());
// token 有效期自定义设置,默认 12 小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 12);
// refresh token 有效期自定义设置,默认 30 天
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
☞ 管理访问端点配置
AuthorizationServerEndpointsConfigurer 这个对象的实例可以完成令牌服务以及令牌 endpoint 配置。通过设定以下属性决定支持的授权类型(Grant Types):
♞ authenticationManager
:认证管理器,当选择了密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
♞ userDetailsService
:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把它设置到全局域上面去,当你设置了这个之后,那么 refresh_token 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效。
♞ authorizationCodeServices
:这个属性是用来设置授权码服务的,主要用于 authorization_code 授权码类型模式。
♞ implicitGrantService
:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
♞ tokenGranter
:当你设置了这个东西,那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。
AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点 URL 链接,它有两个参数,第一个参数:String 类型的,这个端点URL的默认链接。第二个参数:String 类型的,你要进行替代的 URL 链接。以上的参数都将以 /
字符为开始的字符串,框架的默认 URL 链接如下列表,可以作为 pathMapping() 方法的第一个参数:
♞ /oauth/authorize
:授权端点。
♞ /oauth/token
:令牌端点。
♞ /oauth/confirm_access
:用户确认授权提交端点。
♞ /oauth/error
:授权服务错误信息端点。
♞ /oauth/check_token
:用于资源服务访问的令牌解析端点。
♞ /oauth/token_key
:提供公有密匙的端点,如果你使用JWT令牌的话。
需要注意的是授权端点这个 URL 应该被 Spring Security 保护起来只供授权用户访问。
// 授权模式专用对象,在 Security 配置中注入容器
@Autowired
private AuthenticationManager authenticationManager;
// 授权信息保存策略
@Bean
public ApprovalStore approvalStore(){
return new JdbcApprovalStore(dataSource);
}
// 授权码模式数据来源
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
// OAuth2 的主配置信息
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenServices(tokenServices());
}
☞ 令牌端点的安全约束
AuthorizationServerSecurityConfigurer 用来配置令牌端点(Token Endpoint)的安全约束,在AuthorizationServer 中配置如下
// 检查 token 的策略,即配置令牌端点的安全约束
// 就是这个端点谁能访问,谁不能访问
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// tokenkey 这个 endpoint 当使用用 JwtToken 且使用非对称加密时,资源服务用于获取公钥而开放的,此时指 endpoint 完全公开
security.tokenKeyAccess("permitAll()");
// checkToken 这个 endpoint 完全公开
security.checkTokenAccess("permitAll()");
// 允许表单认证
security.allowFormAuthenticationForClients();
}
☞ 完整配置
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/26
* @description OAuth2 配置文件
*/
@Configuration
@EnableAuthorizationServer
public class OAuthServerConfig extends AuthorizationServerConfigurerAdapter {
// 数据库连接池对象,SpringBoot 配置完成后自动注入
@Autowired
private DataSource dataSource;
// 授权模式专用对象,在 Security 配置中注入容器
@Autowired
private AuthenticationManager authenticationManager;
// 客户端信息来源
@Bean
public ClientDetailsService jdbcClientDetailsService(){
return new JdbcClientDetailsService(dataSource);
}
// token 保存策略,指你生成的 Token 要往哪里存储
// 存在数据库:new JdbcApprovalStore(dataSource),存在内存中:new InMemoryTokenStore()
@Bean
public TokenStore tokenStore(){
return new JdbcTokenStore(dataSource);
}
// 指定客户端信息的数据库来源
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
// 授权信息保存策略
@Bean
public ApprovalStore approvalStore(){
return new JdbcApprovalStore(dataSource);
}
// 授权码模式数据来源
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
// 令牌管理
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
// token 保存策略
tokenServices.setTokenStore(tokenStore());
// 支持刷新模式
tokenServices.setSupportRefreshToken(true);
// 客户端信息来源
tokenServices.setClientDetailsService(jdbcClientDetailsService());
// token 有效期自定义设置,默认 12 小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 12);
// refresh token 有效期自定义设置,默认 30 天
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
// 检查 token 的策略,即配置令牌端点的安全约束
// 就是这个端点谁能访问,谁不能访问
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 此时指 endpoint 完全公开
security.tokenKeyAccess("permitAll()");
// checkToken 这个 endpoint 完全公开
security.checkTokenAccess("permitAll()");
// 允许表单认证
security.allowFormAuthenticationForClients();
}
// OAuth2 的主配置信息
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenServices(tokenServices());
}
}
1.4 资源服务
1.4.1 完整配置
继承 ResourceServerConfigurerAdapter 来进行配置,并使用 @EnableResourceServer 和 @Configuration 注解到配置类上,其中 @EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链;配置类中主要包括主要包括:
♞ tokenServices
:ResourceServerTokenServices 类的实例,用来实现令牌服务。
♞ tokenStore
:TokenStore 类的实例,指定令牌如何访问,与 tokenServices 配置可选
♞ resourceld
:这个资源服务的 id,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌,HttpSecurity 配置与 Spring Security 类似。
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/26
* @description 资源服务配置
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true) // Spring Security 方法权限
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore(){
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("res").tokenStore(tokenStore());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 指定不同请求方式访问资源所需要的权限,一般查询是 read,其余是 write。
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
.and()
.headers().addHeaderWriter((request, response) -> {
// 允许跨域
response.addHeader("Access-Control-Allow-Origin", "*");
// 如果是跨域的预检请求,则原封不动向下传达请求头信息
if (request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Methods",
request.getHeader("Access-Control-Request-Method"));
response.setHeader("Access-Control-Allow-Headers",
request.getHeader("Access-Control-Request-Headers"));
}
});
}
}
1.4.2 简要配置
☞ 服务配置文件
# 省略端口,数据源等配置
security:
oauth2:
client:
access-token-uri: http://localhost:8081/oauth/token
user-authorization-uri: http://localhost:8081/oauth/authorize
client-id: web
resource:
user-info-uri: http://localhost:8081/user
prefer-token-info: false
☞ 修改授权服务
由于在资源服务中要用到 token-info-uri
,所以我们需要提供一个请求路径为 /user
的资源返回用户信息,这个资源一般有授权服务提供。同时需要启动类上添加 @EnableResourceServer 注解,表示这也是一个资源服务器,否则 Principal user 无法获取。
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/26
* @description 返回用户信息
*/
@RestController
public class GetUser {
@GetMapping("/user")
public Principal user(Principal user){
return user;
}
}
1.4.3 其他
省略,详情查看源码,地址在最后。
1.5 测试
1.5.1 相关端点
端点 | 含义 |
---|---|
/oauth/authorize | 这个是授权的端点 |
/oauth/token | 这个是用来获取令牌的端点 |
/oauth/confirm_access | 用户确认授权提交的端点 |
/oauth/error | 授权出错的端点 |
/oauth/check_token | 校验 access_token 的端点 |
/oauth/token_key | 提供公钥的端点 |
1.5.2 授权码模式
在地址栏访问 http://localhost:8081/oauth/authorize?response_type=code&client_id=web
,会自动跳转到 Spring Security 的默认登录页面。登录成功后会询问用户是否给与权限,给与什么权限。注意此时数据库中 web_server_redirect_uri
一定要有回调地址,autoapprove
设置为 true 不会询问是否给与权限。
点击 Authorize 之后跳转回调地址,在地址栏出现授权码,拿到授权码后可以在服务器申请 token
拿到 token 之后就可以去请求资源了,token 可以在请求头中,也可以直接拼到 url 中。
1.5.3 简化模式
在地址栏访问 http://localhost:9001/oauth/authorize?response_type=token&client_id=web
,与授权码模式一样会跳转至登录页登录。不同的是登录成功后直接跳转回调地址,在参数中有 access_token。
1.5.4 密码模式
将 grant_type 改为 password 直接向授权服务器请求 access_token
1.5.5 客户端模式
将 grant_type 改为 password 直接向授权服务器请求 access_token,这次连用户账号密码都不需要
1.5.6 刷新 token
获取 access_token 的同时,一般会同时返回 refresh_token,使用 refresh_token 进行刷新,刷新后旧的 token 将失效