文档参考:https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#introduction
OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0 是 OAuth 协议的延续版本,但是不向前兼容 OAuth 1.0 即完全废止了 OAuth1.0。很多大公司如:Google、Yahoo、Microsoft 等都提供了 OAuth 认证服务,这些都足以说明 OAuth 标准逐渐成为开放资源授权的标准。
中文文档:https://colobu.com/2017/04/28/oauth2-rfc6749/
客户端:本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android 客户端、Web客户端、微信客户端等。
资源拥有者:通常为用户,也可以是应用程序,即该资源的拥有者。
授权服务器(也称认证服务器):用于服务提供商对资源拥有者的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源服务器的凭据。
资源服务器:存储资源的服务器。
- Authorization Code(授权码模式):正宗的OAuth2的授权模式,客户端先将用户导向授权服务器,登录后获取授权码,然后进行授权,最后根据授权码获取访问令牌;
- Implicit(简化模式):和授权码模式相比,取消了获取授权码的过程,直接获取访问令牌;
- Resource Owner Password Credentials(密码模式):客户端直接向用户获取用户名和密码,之后向授权服务器获取访问令牌;
- Client Credentials(客户端模式):客户端直接通过客户端授权(比如client_id和client_secret)从授权服务器获取访问令牌。
Spring-Security-OAuth2 是对 OAuth2 的一种实现,并且跟我们之前学习的 Spring Security 相辅相成,与 Spring Cloud 体系的集成也非常便利,接下来,我们需要对它进行学习,最终使用它来实现我们设计的分布式认证授权解决方案。
OAuth2.0 的服务提供方涵盖了两个服务,即授权服务(
Authorization Server
,也叫认证服务)和资源服务(Resource Server
),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。**授权服务(Authorization Server)**应包含对接入端以及登录用户的合法性进行验证并颁发 token 等功能,对令牌的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的 endpoints:
- AuthorizationEndpoint 服务于认证请求,默认 URL:
/oauth/authorize
。- TokenEndpoint 服务于访问令牌的请求。默认 URL:
/oauth/token
。**资源服务(Resource Server)**应包含对资源的保护功能,对非法请求进行拦截,对请求中 token 进行解析鉴权等,下面的过滤器用于实现 OAuth2.0 资源服务。
- OAuth2AuthenticationProcessingFilter 用来对请求给出的身份令牌解析鉴权。
本案例分别创建 uua 授权服务 和 资源服务。
认证流程如下:
1、客户端请求 UUA 授权服务进行认证。
2、认证通过后由 UUA 颁发令牌。
3、客户端携带令牌 Token 请求资源服务。
4、资源服务校验令牌的合法性,合法即返回资源信息。
数据库沿用前面 security 项目的 cloud 数据库
导入 pom 依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
添加 yml 配置
server:
port: 3030
spring:
application:
name: security-oauth2-auth-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3600/cloud?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
#mybatis-plus
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.akieay.cloud.security.oauth2.entity;
configuration:
#是否开启驼峰命名自动映射
map-underscore-to-camel-case: true
#全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。
cache-enabled: false
#指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法
call-setters-on-nulls: true
#指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
logging:
level:
root: info
主启动
@SpringBootApplication
@MapperScan(value = "com.akieay.cloud.security.oauth2.mapper")
public class SecurityOauth2AuthApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityOauth2AuthApplication.class, args);
}
}
配置类–认证服务配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Resource
private TokenStore tokenStore;
@Resource
private ClientDetailsService clientDetailsService;
@Resource
private AuthorizationCodeServices authorizationCodeServices;
@Resource
private AuthenticationManager authenticationManager;
/**
* 客户端详情配置
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 暂时使用内存方式
clients.inMemory()
// 客户端id
.withClient("c1")
// 客户端密码
.secret(new BCryptPasswordEncoder().encode("secret"))
// 源列表
.resourceIds("res1")
// 授权类型
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
// 允许的授权范围
.scopes("all")
.autoApprove(false)
// 验证回调地址
.redirectUris("http://www.baidu.com");
}
/**
* 令牌管理服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
// 客户端信息服务
services.setClientDetailsService(clientDetailsService);
// 是否产生刷新令牌
services.setSupportRefreshToken(true);
// 令牌存储策略
services.setTokenStore(tokenStore);
// 令牌默认有效期: 两小时
services.setAccessTokenValiditySeconds(2 * 60 * 60);
// 刷新令牌默认有效期: 3天
services.setRefreshTokenValiditySeconds(3 * 24 * 60 * 60);
return services;
}
/**
* 设置授权码模式的授权码如何获取,暂时采用内存方式
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
/**
* 令牌访问端点
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 认证管理器
.authenticationManager(authenticationManager)
// 授权码服务
.authorizationCodeServices(authorizationCodeServices)
// 令牌管理服务
.tokenServices(tokenServices())
// 允许 post 提交
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
/**
* 令牌访问端点安全策略
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.
// 公有密钥 /oauth/token_key 公开
tokenKeyAccess("permitAll()")
// 检测令牌 /oauth/check_token 公开
.checkTokenAccess("permitAll()")
// 允许表单认证(申请令牌)
.allowFormAuthenticationForClients();
}
}
配置类–令牌存储策略
@Configuration
public class TokenConfig {
/**
* 令牌存储策略
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
配置类–添加安全访问控制
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 认证管理器
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码加密算法 BCrypt 推荐使用
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 授权拦截机制
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
至于其它配置类 由于这里是沿用前面
security
工程的就不单独介绍了,可以参考security
模块博客,或者直接去 git 拉取工程项目。
至此,简单的授权服务创建完成,下面将介绍授权服务各项配置详情。
可以用
@EnableAuthorizationServer
注解并继承AuthorizationServerConfigurerAdapter
来配置 OAuth2.0 授权服务器。
AuthorizationServerConfigurerAdapter
要求配置以下几个类,这几个类是由 Spring 创建的独立的配置对象,它们会被 Spring 传入AuthorizationServerConfigurer
中进行配置。
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
- ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情服务在这里进行初始化,可以使用内存也可以从数据库获取详情信息。
- AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
- AuthorizationServerSecurityConfigurer:用来配置令牌访问端点的安全约束。
ClientDetailsServiceConfigurer
能够使用内存或者 JDBC 来实现客户端详情服务(ClientDetailsService),ClientDetailsService
负责查找ClientDetails
,而ClientDetails
有几个重要的属性,如下:
- clientId:客户端 Id。
- secret:客户端密钥。
- scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
- authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。可选项:“authorization_code”, “password”, “client_credentials”, “implicit”, “refresh_token”。
- authorities:此客户端拥有的权限(基于 Spring Security authorities)。
客户端详情(ClientDetails)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系型数据库中,就可以使用
jdbcClientDetailsService
)或者通过自己实现的ClientRegistrationService
接口(同时你也可以实现ClientDetailsService
接口)来进行管理。配置如下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 暂时使用内存方式
clients.inMemory()
// 客户端id
.withClient("c1")
// 客户端密码
.secret(new BCryptPasswordEncoder().encode("secret"))
// 源列表
.resourceIds("res1")
// 授权类型
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
// 允许的授权范围
.scopes("all")
.autoApprove(false)
// 验证回调地址
.redirectUris("http://www.baidu.com");
}
AuthorizationServerTokenServices
接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。若想要自己创建
AuthorizationServerTokenServices
这个接口的实现,则需要继承DefaultTokenServices
这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储策略。默认的:当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个TokenStore
接口来实现以外,这个类几乎帮你完成了所有的事情。并且TokenStore
这个接口有几个默认的实现,如下:
- InMemoryTokenStore:(默认)它所有令牌被保存在内存中,可以完美的工作在单服务器上(即访问并发量不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。。
- JdbcTokenStore:这是一个基于 JDBC 的实现版本,令牌会被保存在关系型数据库。使用这个版本实现时,你可以在不同的服务器之间共享令牌信息。
- JwtTokenStore :这个版本的全称是 JSON Web Token (JWT),优点是:它可以把令牌相关的数据进行编码与解码。(即:对于后端来说它不需要存储,这是一个优点)。缺点:(1) 撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌( refresh-token )。(2) 如果加入了比较多的用户凭证信息,那么这个令牌占用的空间会比较大。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与
DefaultTokenServices
所扮演的角色是一样的。
创建令牌存储策略
@Configuration
public class TokenConfig {
/**
* 令牌存储策略
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
创建令牌管理服务
@Resource
private TokenStore tokenStore;
@Resource
private ClientDetailsService clientDetailsService;
/**
* 令牌管理服务
* clientDetailsService 沿用之前在 Security 项目中配置的 CustomUserDetailsService
*
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
// 客户端信息服务
services.setClientDetailsService(clientDetailsService);
// 是否产生刷新令牌
services.setSupportRefreshToken(true);
// 令牌存储策略
services.setTokenStore(tokenStore);
// 令牌默认有效期: 两小时
services.setAccessTokenValiditySeconds(2 * 60 * 60);
// 刷新令牌默认有效期: 3天
services.setRefreshTokenValiditySeconds(3 * 24 * 60 * 60);
return services;
}
AuthorizationServerEndpointsConfigurer
这个对象的实例可以完成令牌服务以及令牌 endpoints 配置。
AuthorizationServerEndpointsConfigurer
通过设定以下的属性决定支持的授权类型(Grant Types
):
- authenticationManager:认证管理器,当你选择了密码(password)授权类型时,需要设置这个属性注入一个
AuthenticationManager
对象。- userDetailsService:(密码模式)如果你设置了这个属性的话,那么你需要有一个自己的 UserDetailsService 接口的实现。
- authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 “authorization_code” 授权码类型模式。
- implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
- tokenGranter:当你设置了这个属性(即 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
:提供公有密匙的端点,如果你使用wT令牌的话。需要注意的是授权端点这个 URL 应该被 Spring Security 保护起来只供授权用户访问。
在
AuthorizationServer
配置令牌访问端点:
@Resource
private AuthorizationCodeServices authorizationCodeServices;
@Resource
private AuthenticationManager authenticationManager;
/**
* 设置授权码模式的授权码如何获取,暂时采用内存方式
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
/**
* 令牌访问端点
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 认证管理器
.authenticationManager(authenticationManager)
// 授权码服务
.authorizationCodeServices(authorizationCodeServices)
// 令牌管理服务
.tokenServices(tokenServices())
// 配置授权端点 url
// .pathMapping("/oauth/authorize", "/auth/authorize")
// 允许 post 提交
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
AuthorizationServerSecurityConfigurer
用来配置令牌端点(Token Endpoint)的安全约束。
/**
* 令牌访问端点安全策略
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.
// 公有密钥 /oauth/token_key 公开
tokenKeyAccess("permitAll()")
// 检测令牌 /oauth/check_token 公开
.checkTokenAccess("permitAll()")
// 允许表单认证(申请令牌)
.allowFormAuthenticationForClients();
}
添加
WebSecurityConfig
配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 认证管理器
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码加密算法 BCrypt 推荐使用
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 授权拦截机制
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
**(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权chong,它将被浏览器重定向到授权服务器,重定向时会附加客户端的身份信息。**请求格式如下:
/oauth/authorize?client_id=客户端ID&response_type=code&scope=授权范围标识&redirect_uri=转发uri
参数列表如下:
- client_id:客户端 Id
- response_type:授权码模式固定为 code
- scope:客户端授权范围标识
- redirect_uri:跳转 uri,当授权码申请成功后将会跳转到此地址,并在地址后面带上 code 参数(授权码)。
访问:http://localhost:3030/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com ,将会直接跳转至登录页面。
(2)浏览器出现向授权服务器授权页面,用户可以选择同意授权或拒绝,同意将继续,拒绝将终止。
(3)授权服务器将授权码(authorization_code)按照指定的 uri(redirect_uri) 转发给 client。
**(4)客户端拿着授权码向授权服务器索要访问令牌 access_token。**请求格式如下:【post 请求】
/oauth/token 参数:client_id、client_secret、grant_type、code、redirect_uri
参数列表如下:
client_id:客户端 Id
client_secret:客户端密钥
grant_type:授权类型,固定填入
authorization_code
,表示授权码模式code:授权码,就是上一步获取到授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转 uri,一定要和申请授权码时用到的 redirect_uri 一致。
(5)授权服务器返回令牌(access_token)
总结:
这种模式是四种模式中最安全的一种模式。一般用于 client 是 Web 服务器端应用或者第三方的原生 App 调用资源服务的时候。因为在这种模式中 access_token 不会经过浏览器或移动端的 App,而是直接从服务端去交换,这样就最大限度的减小了令牌泄露的风险。
**(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将被浏览器重定向到授权服务器,重定向时会附加客户端的身份信息。**请求格式如下:
/oauth/authorize?client_id=客户端ID&response_type=token&scope=授权范围标识&redirect_uri=转发uri
参数描述同授权码模式,注意
response_type 固定为 token
,说明是简化模式。访问:http://localhost:3030/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com 将会直接跳转至登录页面。
(2)浏览器出现向授权服务器授权页面,用户可以选择同意授权或拒绝,同意将继续,拒绝将终止。
(3)授权服务器将授权码将令牌(access_token)以 Hash 的形式存放在重定向 uri 的 fargment 中发送给浏览器。
注:fargment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fargment 的开头,其中 # 不属于 fargment 的值,如:https://domain/index#L18 这个 URI 中的 L18 就是 fargment 的值。js 可以通过响应地址栏变化的方式获取到 fargment 。
总结:
一般来所,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。
(1)资源拥有者的用户名、密码发送给客户端。
**(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token)。**请求格式如下:【post 请求】
/oauth/token 参数:client_id、client_secret、grant_type、username、password
参数列表如下:
- client_id:客户端 Id
- client_secret:客户端密钥
- grant_type:授权类型,密码模式固定为 password
- username:资源拥有者用户名
- password:资源拥有者密码
(3)授权服务器将令牌(access_token)发送给 client。
总结:
这种模式十分简单,但同时也将用户的敏感信息直接泄露给了 client,因此这种模式只能适用于 client 是我们自己开放的情况下。即:第一方原生的 App 或第一方单页面应用。
(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)。
**(2)确认客户端身份无误后,将令牌(access_token)发送给客户端。**请求格式如下:
/oauth/token 参数:client_id、client_secret、grant_type
参数列表如下:
- client_id:客户端 Id
- client_secret:客户端密钥
- grant_type:授权类型,客户端模式固定为 client_credentials
总结:
这种模式是最方便但最不安全的模式。因此这就要求我们对 client 完全的信任,而 client 本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。
oauth2 提供了刷新令牌的功能,当令牌失效时,可以调用 刷新令牌请求格式如下:
/oauth/token 参数:grant_type、refresh_token
参数列表如下:
- grant_type:授权类型,刷新令牌时固定为 refresh_token
- refresh_token:刷新令牌的 token
如果使用
postman
测试,注意:需要在 Authorizatian 添加用户名密码认证,TYPE 为 Basic Auth,其中 Username 为 client_id、Password 为 client_secret。
如下:
首先 使用密码模式获取一个令牌,并等待一段时间 让它的有效期使用一段时间,再获取查看有效期,如下:
这时我们的 令牌有效期为 6795 秒,我们可以调用 http://localhost:3030/oauth/token 刷新有效期。【注意参数 以及 填充 Authorizatian 认证信息】刷新后,我们的令牌的有效期恢复为原来给定的默认有效期 -1秒【刷新过程消耗】
导入 pom 依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
添加 yml 配置文件
server:
port: 3040
spring:
application:
name: security-oauth2-resource-service
主启动
@SpringBootApplication
public class SecurityOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityOrderApplication.class, args);
}
}
配置类–资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 资源ID
resources.resourceId(RESOURCE_ID)
// 验证令牌的服务
.tokenServices(tokenServices())
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
// 所有资源 都要求请求者具有权限范围 "all"
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 资源服务令牌解析服务
*
* @return
*/
@Bean
public ResourceServerTokenServices tokenServices() {
// 使用远程服务请求授权服务器校验 token,必须指定校验 token 的 url、client_id、client_secret
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:3030/oauth/check_token");
services.setClientId("c1");
services.setClientSecret("secret");
return services;
}
}
配置类–添加安全访问控制
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 授权拦截机制
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
// 凡是匹配 "/v1/**" 的请求,都必须认证
.antMatchers("/v1/**").authenticated()
// 其它请求公开访问
.anyRequest().permitAll();
}
}
业务类–资源类
@PreAuthorize("hasAuthority('r:r3')")
表示该方法需要r:r3
权限才能访问。
@RestController
@RequestMapping("/v1/test")
public class TestController {
@GetMapping("/getAuthorInfo")
@PreAuthorize("hasAuthority('r:r3')")
public Map<String, Object> getAuthorInfo() {
Map<String, Object> result = new HashMap<>(6);
result.put("author", "akieay");
result.put("date", "2020-11-30");
result.put("method", "getAuthorInfo");
return result;
}
}
可以用
@EnableResourceServer
注解并继承ResourceServerConfigurerAdapter
覆写其中方法来配置 OAuth2.0 资源服务器。ResourceServerConfigurerAdapter
如下:public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); } }
ResourceServerSecurityConfigurer 中配置主要包括:
- tokenServices:
ResourceServerTokenServices
类的实例,用来实现令牌服务 可选。- tokenStore:
TokenStore
类的实例,指定令牌如何访问,与tokenServices
配置可选。- resourceId:这个是资源服务的 ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
- 其它的拓展属性:例如
tokenExtractor
令牌提取器用来提取请求中的令牌。HttpSecurity 配置【这个与 Spring Security 类似】:
- 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。
- 通过
http.authorizeRequests()
来设置受保护资源的访问规则。- 其它的自定义权限保护规则通过 HttpSecurity 来进行配置。
另外
@EnableResourceServer
注解自动为应用增加了一个类型为OAuth2AuthenticationProcessingFilter
的过滤器链。添加
ResourceServerConfig
配置类:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 资源ID
resources.resourceId(RESOURCE_ID)
// 验证令牌的服务
.tokenServices(tokenServices())
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
ResourceServerTokenServices
是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用DefaultTokenServices
。如果你的资源服务器是分离开的,那么你就必须要提供能够匹配授权服务的ResourceServerTokenServices
,使用它来对令牌进行解码。令牌解析方法:
授权与资源服务同一应用:使用
DefaultTokenServices
在资源服务器本地配置令牌存储、转码、解析方式。授权与资源服务不同应用:使用
RemoteTokenServices
资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点/oauth/check_token
,需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问。如下配置:【注意该配置在授权服务器中】
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.
// 公有密钥 /oauth/token_key 公开
tokenKeyAccess("permitAll()")
// 检测令牌 /oauth/check_token 公开
.checkTokenAccess("permitAll()")
// 允许表单认证(申请令牌)
.allowFormAuthenticationForClients();
}
由于我们的案例是 授权服务于资源服务分开的,所以在资源服务中配置
RemoteTokenServices
。
@Bean
public ResourceServerTokenServices tokenServices() {
// 使用远程服务请求授权服务器校验 token,必须指定校验 token 的 url、client_id、client_secret
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:3030/oauth/check_token");
services.setClientId("c1");
services.setClientSecret("secret");
return services;
}
逐次启动 授权服务 与 资源服务。
一、申请令牌,这里我们使用密码模式。
可以通过访问:http://localhost:3030/oauth/check_token 获取 token 的信息,如:权限范围,权限列表、客户端信息等。
二、请求资源,按照 oauth2.0 协议要求,请求资源需要携带 token,如下: token 的参数名称为
Authorization
,值为Bearer {token}
注意中间的空格不可省略。访问资源服务:http://localhost:3040/v1/test/getAuthorInfo 【注意将 token 添加到请求头中】,由于我们数据库中设置的
zhangsan
具有r:r1
、r:r2
、r:r3
三种权限,满足接口的r:r3
权限的条件,所以可以访问。
同理,由于
lisi
具有r:r1
、r:r2
两种权限,不满足接口的r:r3
权限的条件,所以不能访问接口。
至此,基本的 security oauth2 授权服务 与 资源服务创建完成。
由于我们上面的资源服务和授权服务不在同一个应用,资源服务需要使用
RemoteTokenServices
远程请求授权服务验证 token;如果访问量大的话,将会影响系统的性能。为了解决这个问题,我们引入的 JWT 令牌;用户认证通过会得到一个 JWT 令牌,JWT 令牌中已经包含了用户相关的信息,客户端只需要携带 JWT 令牌访问资源服务,资源服务根据事先约定的算法自行完成令牌的校验,无需每次都请求认证服务完成授权。
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),他定义了一种简洁的、自包含的协议格式,用于在通信双方传递 json 对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或使用 RSA 的公钥/私钥对来签名,防止被篡改。
官网:https://jwt.io/
优点:
- JWT 基于 json,非常方便解析。
- 可以在令牌中自定义丰富的内容,易扩展。
- 通过非对称加密算法及数字签名技术,JWT 防止篡改,安全性高。
- 资源服务使用 JWT 可以不依赖认证服务即可完成授权。
缺点:
- JWT 令牌较长,占存储空间比较大。
JWT 令牌由三部分组成,每部分中间使用 “.” 分隔,例如:xxxxx.yyyyy.zzzzz
头部包含令牌的类型(即 JWT)及使用的哈希算法(如:HMACSHA256 或 RSA)。例子如下:
{
"alg": "HS256",
"typ": "JWT"
}
将上面的内容使用 Base64Url 编码,得到一个字符串就是 JWT 令牌的第一部分。
第二部分是负载,内容也是一个 json 对象,它是存放有效信息的地方,它可以存放 jwt 提供的现成字段,比如:iss(签发者),exp(过期时间戳),sub(面向的用户)等,也可以自定义字段。此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。例子如下:
{
"sub": "client10023",
"name": "akieay",
"iss": "akieay",
"exp": "1606755715000"
}
将上面的 Payload 内容使用 Base64Url 编码,得到一个字符串就是 JWT 令牌的第二部分。
第三部分是签名,此部分用于防止 jwt 内容被篡改。这个部分使用 Base64Url 将前两部分进行编码,编码后使用 “.” 连接组成字符串,最后使用 header 中声明的签名算法进行签名。例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
base64UrlEncode(header):jwt 令牌的第一部分。
base64UrlEncode(payload):jwt 令牌的第二部分。
secret:签名所使用的秘钥。
修改 TokenConfig
@Configuration
public class TokenConfig {
/**
* 签名秘钥
*/
private String SIGNING_KEY = "akieay-security-oauth2-signing-key";
/**
* 令牌存储策略
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/**
* JWT访问令牌转换器
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 对称秘钥,资源服务器使用该密钥来验证
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
定义 JWT 令牌服务,修改
AuthorizationServer
。主要修改如下:注入新创建的 JWT 令牌转换器,并为令牌管理服务AuthorizationServerTokenServices
添加令牌增强配置。
@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* 令牌管理服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
// 客户端信息服务
services.setClientDetailsService(clientDetailsService);
// 是否产生刷新令牌
services.setSupportRefreshToken(true);
// 令牌存储策略
services.setTokenStore(tokenStore);
// 令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
services.setTokenEnhancer(tokenEnhancerChain);
// 令牌默认有效期: 两小时
services.setAccessTokenValiditySeconds(2 * 60 * 60);
// 刷新令牌默认有效期: 3天
services.setRefreshTokenValiditySeconds(3 * 24 * 60 * 60);
return services;
}
资源服务需要和授权服务拥有一致的签名、令牌服务等:
1、
将授权服务中的 TokenConfig 类拷贝到资源服务中
。2、删除资源服务原来的令牌解析服务,注入并使用
TokenStore
本地校验令牌。
ResourceServerConfig
的修改如下:
@Resource
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 资源ID
resources.resourceId(RESOURCE_ID)
// 本地校验令牌
.tokenStore(tokenStore)
.stateless(true);
}
逐次重启 授权服务 与 资源服务。获取访问令牌,这里我们使用 密码模式。
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MDY3OTYyMjIsImF1dGhvcml0aWVzIjpbInI6cjIiLCJyOnIzIiwicjpyMSJdLCJqdGkiOiJhMTVmODVmMy1jNWVlLTQ2MzEtOGFhNC0xMDQ4ZDMyNWQyY2UiLCJjbGllbnRfaWQiOiJjMSJ9.MARyqPVmvyY8Xp6bkducKHwznsug-Ith61izflnIyHw",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJhMTVmODVmMy1jNWVlLTQ2MzEtOGFhNC0xMDQ4ZDMyNWQyY2UiLCJleHAiOjE2MDcwNDgyMjIsImF1dGhvcml0aWVzIjpbInI6cjIiLCJyOnIzIiwicjpyMSJdLCJqdGkiOiI2NzdkZjhlOS1kZThkLTQzOGEtYjNjNC1iNjJlM2FkNjE0YzUiLCJjbGllbnRfaWQiOiJjMSJ9.LlpyUNlQ1Do_5m7vQV_fHS0oBisdDmvxVxGmv7CIPRA",
"expires_in": 7199,
"scope": "all",
"jti": "a15f85f3-c5ee-4631-8aa4-1048d325d2ce"
}
上面的
access_token
即是返回的 jwt 令牌,可以看到这个令牌与我们之前的所获取的令牌是不一样的;由于其中包含了用户详情信息,所以比之前的令牌长很多。同样的,我们可以通过访问:http://localhost:3030/oauth/check_token 来验证令牌的信息。
在访问资源服务时,同样我们需要将令牌添加到请求头中,格式与前面一致。
前面介绍过 JWT 可以在令牌中自定义丰富的内容,我们可以通过实现
TokenEnhancer
接口添加自定义的 jwt 内容。如下:我们在 jwt 令牌中添加了一个"author", "akieay"
键值对。【注:以下修改皆为 授权服务 中的修改】
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> info = new HashMap<>();
info.put("author", "akieay");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
}
}
在
TokenConfig
声明JwtTokenEnhancer
并将其注入到 spring 容器。
/**
* JWT令牌增强器
*
* @return
*/
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
在
AuthorizationServer
中的 令牌管理服务中注入并指定 自定义的令牌增强器jwtTokenEnhance
@Resource
private JwtTokenEnhancer jwtTokenEnhancer;
/**
* 令牌管理服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
// 客户端信息服务
services.setClientDetailsService(clientDetailsService);
// 是否产生刷新令牌
services.setSupportRefreshToken(true);
// 令牌存储策略
services.setTokenStore(tokenStore);
// 令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
services.setTokenEnhancer(tokenEnhancerChain);
// 令牌默认有效期: 两小时
services.setAccessTokenValiditySeconds(2 * 60 * 60);
// 刷新令牌默认有效期: 3天
services.setRefreshTokenValiditySeconds(3 * 24 * 60 * 60);
return services;
}
重启 认证服务,并使用 密码模式 获取令牌。
访问:http://localhost:3030/oauth/check_token 解析令牌。
可以看到在 jwt 令牌中已经存在我们自定义的
"author", "akieay"
键值对。
上面我们自定义了jwt 令牌的信息,这里我们读取 jwt 令牌中的信息。
导入 pom 依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.5.1version>
dependency>
添加 jwt 解析处理业务类,这里提供了两种获取
token
的方式,一种是通过authentication
获取,一种是通过request
获取。
@GetMapping("/getJwtTokenInfo")
public JSON getJwtTokenInfo(Authentication authentication, HttpServletRequest request) {
Object details = authentication.getDetails();
String tokenValue = JSONUtil.parse(details).getByPath("tokenValue", String.class);
// String header = request.getHeader("Authorization");
// tokenValue = header.substring(TOKEN_PREFIX.length());
Claims body = Jwts.parser()
.setSigningKey(TokenConfig.getSigningKey().getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(tokenValue).getBody();
return JSONUtil.parse(body);
}
重启服务,访问:http://localhost:3040/v1/test/getJwtTokenInfo ,注意添加 jwt 令牌。可以看到我们正常获取到了 jwt 令牌中的信息,包含我们自定义的 jwt 信息。
与前面一样,当 JWT 令牌失效的时候,同样可以使用刷新令牌 获取新的 JWT 令牌【注意:刷新令牌要在有效期内才有效哦】。注意 client 对象需要有相应的权限哦,我们之前给定的 client 对象是有所有访问模式+刷新令牌的权限的。
刷新访问令牌。使用
postman
测试的时候,注意参数以及填充Authorizatian
认证信息。
刷新后,会得到新的访问令牌以及刷新令牌。
以上的 JWT令牌 演示都是基于
HS256
对称加密实现的,下面将介绍另一种加密方式RSA
非对称加密的实现。
一、openssl 生成公钥私钥
[root@localhost ~]# openssl genrsa -out jwt.pem 2048
Generating RSA private key, 2048 bit long modulus
....................................................................+++
..............................................+++
e is 65537 (0x10001)
[root@localhost ~]# openssl rsa -in jwt.pem
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA2RUYie5wTgkD96lE9UwkFq43uy7x/Q/4G3Mf85HfhD6CPhsv
.............................................................
V8axAoGAMxHRj2aQ5BI/ooonrYZ9JZYozCgKFk6Ls10dd/gOwWM6jbmqvQYgxKQR
Yjg8EkYf8q2XEecgYPoBM0lNIuacNUsvprygslym9bAza2B8F1rRExjqejRHESau
PlajLWso5VzxvEATo1ydreXi8ZTRptccoaaXDkp/1k38vbz5Oj0=
-----END RSA PRIVATE KEY-----
[root@localhost ~]# ls
anaconda-ks.cfg jwt.pem
[root@localhost ~]# openssl rsa -in jwt.pem -pubout
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2RUYie5wTgkD96lE9Uwk
..........................................................
QlG359/I4caCNytm2B5uTJmFm8fzz4Mx+I63F7eXMaj4wPPJsaHWhTCWx1l4RE1J
fwIDAQAB
-----END PUBLIC KEY-----
以上,使用
openssl genrsa -out jwt.pem 2048
生成秘钥,使用openssl rsa -in jwt.pem
获取私钥,使用openssl rsa -in jwt.pem -pubout
获取公钥。
二、修改认证服务,使用 RSA 非对称加密 JWT 令牌,具体修改如下:
rsa:
oauth2:
# openssl genrsa -out jwt.pem 2048
# openssl rsa -in jwt.pem
private-key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA2RUYie5wTgkD96lE9UwkFq43uy7x/Q/4G3Mf85HfhD6CPhsv
2xMWX8c9dLwWetspQyzYDxbLyb+R6PR351F0MuzlsKWkQDfTXAQQ92p6io5XOKEN
..........................................................
LuFjEwjPSeoLwVuG6WoQk33m4QfDhJ+0EGzWDt32yVAOept+EEOlCAlC3Xc7Gvz/
V8axAoGAMxHRj2aQ5BI/ooonrYZ9JZYozCgKFk6Ls10dd/gOwWM6jbmqvQYgxKQR
Yjg8EkYf8q2XEecgYPoBM0lNIuacNUsvprygslym9bAza2B8F1rRExjqejRHESau
PlajLWso5VzxvEATo1ydreXi8ZTRptccoaaXDkp/1k38vbz5Oj0=
-----END RSA PRIVATE KEY-----
# openssl rsa -in jwt.pem -pubout
public-key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2RUYie5wTgkD96lE9Uwk
........................................................
QlG359/I4caCNytm2B5uTJmFm8fzz4Mx+I63F7eXMaj4wPPJsaHWhTCWx1l4RE1J
fwIDAQAB
-----END PUBLIC KEY-----
/**
* RSA256 非对称加密签名私钥
*/
@Value("${rsa.oauth2.private-key}")
private String RsaPrivateKey;
/**
* RSA256 非对称加密签名公钥
*/
@Value("${rsa.oauth2.public-key}")
private String RsaPublicKey;
/**
* JWT访问令牌转换器
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// RSA非对称加密,资源服务器使用公钥解密
converter.setSigningKey(RsaPrivateKey);
converter.setVerifierKey(RsaPublicKey);
// // HS256对称秘钥,资源服务器使用该密钥来验证
// converter.setSigningKey(SIGNING_KEY);
return converter;
}
三、修改资源服务,使用 RSA 公钥解密 jwt 令牌。具体修改如下:
rsa:
oauth2:
# openssl rsa -in jwt.pem -pubout
public-key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2RUYie5wTgkD96lE9Uwk
........................................................
WK7Zyl5NLnXZR2e5lG/ZRpAy4hk3pTij9FbNXyGnacoXUlgFymXzdmFoYXLLw/zk
QlG359/I4caCNytm2B5uTJmFm8fzz4Mx+I63F7eXMaj4wPPJsaHWhTCWx1l4RE1J
fwIDAQAB
-----END PUBLIC KEY-----
/**
* RSA256 非对称加密签名公钥
*/
@Value("${rsa.oauth2.public-key}")
private String RsaPublicKey;
/**
* JWT访问令牌转换器
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// RSA非对称加密,资源服务器使用公钥解密
converter.setVerifierKey(RsaPublicKey);
// HS256对称秘钥,资源服务器使用该密钥来验证
// converter.setSigningKey(SIGNING_KEY);
return converter;
}
可以在 https://jwt.io/ 输入获取到的 jwt 令牌与 公钥进行验签,如下:
-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_id` varchar(18) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` blob NULL,
`refresh_token` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(30) DEFAULT NULL COMMENT '登录的用户名',
`clientId` varchar(18) DEFAULT NULL COMMENT '客户端ID',
`scope` varchar(128) DEFAULT NULL COMMENT '申请的权限',
`status` varchar(10) DEFAULT NULL COMMENT '状态(Approve或Deny)',
`expiresAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '过期时间',
`lastModifiedAt` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最终修改时间',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
)ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(18) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_ids` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`user_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_id` varchar(18) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` blob NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication` blob NULL,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
插入客户端信息
INSERT INTO `oauth_client_details` VALUES ('c1', '$2a$10$iy25Dllm1PSe08y3BgcpC.YqXrQzLxrqaVvWNkCMNEanprglElYWm', 'res1', 'all', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', NULL, 7200, 259200, NULL, NULL, '2020-12-01 23:54:03');
注意 客户端秘钥采用
BCryptPasswordEncoder
加密,存入数据库的 客户端秘钥需要加密,加密案例如下:
/**
* @author akieay
* @Date: 2020/11/24 22:41
*/
@RunWith(SpringRunner.class)
public class TestMain {
@Test
public void testBcrypt() {
String gensalt = BCrypt.gensalt();
String hashpw = BCrypt.hashpw("secret", gensalt);
System.out.println(gensalt + "\t" +hashpw);
}
}
在项目中,主要操作
oauth_client_details
表的类是JdbcClientDetailsService.java
,更多的细节请参考该类。也可以根据实际的需要,去扩展或修改该类的实现。
字段名 | 字段说明 |
---|---|
client_id | 客户端 Id,用于唯一标识每一个客户端 |
client_secret | 客户端秘钥 |
resource_ids | 客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: “device-resource,mobile-resource” |
scope | 指定客户端申请的权限范围,可选值包括 read、write、trust;若有多个权限范围用逗号(,)分隔,如: “read,write” |
authorized_grant_types | 指定客户端支持的 grant_type ,可选值包括:authorization_code、password、refresh_token、implicit、client_credentials 【注意其中 refresh_token 表示刷新访问令牌的支持】,若支持多个 grant_type 用逗号(,)分隔,如:“authorization_code,password” |
web_server_redirect_uri | 客户端的重定向 URI,可为空,当 grant_type 为 authorization_code 或 implicit 时, 在 OAuth2 的流程中会使用并检查与注册时填写的 redirect_uri 是否一致. |
authorities | 指定客户端所拥有的 Spring Security 的权限值,可选,若有多个权限值,用逗号(,)分隔, 如:“ROLE_UNITY,ROLE_USER” |
access_token_validity | 设定客户端的 access_token 的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12 ,即12小时). |
refresh_token_validity | 设定客户端的 refresh_token 的有效时间值(单位:秒),可选,若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30,即30天). |
additional_information | 该字段来存储关于客户端的一些其他信息,如客户端的国家、地区、注册时的IP地址等等. |
autoapprove | 设置用户是否自动 Approval 操作,默认值为 false, 可选值包括 true、false、 read、write 。该字段只适用于 grant_type=“authorization_code” 的情况,当用户登录成功后,若该值为 true 或支持的 scope 值,则会跳过用户 Approve 的页面,直接授权 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
注意:
authorities
中指定的权限主要用于客户端模式,其它模式会采用登录用户的权限。
该表用于在客户端系统中存储从服务端获取的 token 数据。对
oauth_client_token
表的主要操作在JdbcClientTokenServices.java
类中, 更多的细节请参考该类。
字段名 | 字段说明 |
---|---|
token_id | 从服务器端获取到的 access_token 的值. |
token | 这是一个二进制的字段,存储的数据是 OAuth2AccessToken.java 对象序列化后的二进制数据 |
authentication_id | 该字段具有唯一性,是根据当前的 username(如果有)、client_id 与 scope 通过 MD5 加密生成的 |
user_name | 登录时的用户名 |
client_id | 客户端 Id,用于唯一标识每一个客户端 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
在项目中,主要操作
oauth_access_token
表的对象是JdbcTokenStore.java
, 更多的细节请参考该类。
字段名 | 字段说明 |
---|---|
token_id | 该字段的值是将 access_token 的值通过MD5加密后存储的 |
token | 存储将 OAuth2AccessToken.java 对象序列化后的二进制数据,是真实的AccessToken的数据值 |
authentication_id | 该字段具有唯一性,是根据当前的 username(如果有)、client_id 与 scope 通过 MD5 加密生成的 |
user_name | 登录时的用户名,若客户端没有用户名(如 grant_type=“client_credentials” ),则该值等于client_id |
client_id | 客户端 Id,用于唯一标识每一个客户端 |
authentication | 存储将 OAuth2Authentication.java 对象序列化后的二进制数据 |
refresh_token | 该字段的值是将 refresh_token 的值通过 MD5 加密后存储的 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
在项目中,主要操作
oauth_refresh_token
表的对象是JdbcTokenStore.java
, (与操作oauth_access_token
表的对象一样);更多的细节请参考该类。如果客户端的 grant_type 不支持refresh_token
,则不会使用该表。
字段名 | 字段说明 |
---|---|
token_id | 该字段的值是将 refresh_token 的值通过 MD5 加密后存储的 |
token | 存储将 OAuth2RefreshToken.java 对象序列化后的二进制数据 |
authentication | 存储将 OAuth2Authentication.java 对象序列化后的二进制数据 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
在项目中,主要操作
oauth_code
表的对象是JdbcAuthorizationCodeServices.java
,更多的细节请参考该类。只有当 grant_type 为 “authorization_code” 时,该表中才会有数据产生; 其他的 grant_type 没有使用该表。
字段名 | 字段说明 |
---|---|
code | 存储服务端系统生成的 code 的值(未加密) |
authentication | 存储将 AuthorizationRequestHolder.java 对象序列化后的二进制数据 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
在项目中,主要操作
oauth_approvals
表的对象是JdbcApprovalStore
。存放用户授权 client 的信息,即当 client 的 grant type 支持 authorization_code 时才有记录。
字段名 | 字段说明 |
---|---|
userId | 登录的用户名 |
clientId | 客户端ID |
scope | 申请的权限 |
status | 状态(Approve 或 Deny) |
expiresAt | 过期时间 |
lastModifiedAt | 最终修改时间 |
createTime | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
修改
TokenConfig
配置,使其使用 jdbc 令牌存储策略。【实际生产中可以使用 JWT 令牌】,该修改主要与oauth_access_token
和oauth_refresh_token
表相关。
@Configuration
public class TokenConfig {
/**
* 令牌存储策略
*
* @return
*/
@Bean
public TokenStore tokenStore(DataSource dataSource) {
return new JdbcTokenStore(dataSource);
}
}
修改
AuthorizationServer
主要修改为:
- 修改客户端详情配置:主要修改
clientDetails()
和configure(ClientDetailsServiceConfigurer clients)
方法,将原有的从内存中获取客户端详情的操作改为了 从数据库中获取客户端详情【必要的修改】,也可以修改成 redis 中获取,具体方法自行查阅。主要与oauth_client_details
表相关。- 修改授权码模式配置:主要修改
authorizationCodeServices()
和configure(AuthorizationServerEndpointsConfigurer endpoints)
方法,并且需要注入ApprovalStore
存储客户端授权信息。主要与oauth_code
和oauth_approvals
表相关。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Resource
private TokenStore tokenStore;
@Resource
private DataSource dataSource;
@Resource
private AuthenticationManager authenticationManager;
// @Resource
// private JwtAccessTokenConverter jwtAccessTokenConverter;
// @Resource
// private JwtTokenEnhancer jwtTokenEnhancer;
@Resource
private PasswordEncoder passwordEncoder;
/**
* 客户端详情服务,使用数据库中的客户端信息
*
* @return
*/
@Bean
public ClientDetailsService clientDetails() {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService)clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
/**
* 客户端详情配置
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
/**
* 令牌管理服务
*
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
// 客户端信息服务
services.setClientDetailsService(clientDetails());
// 是否产生刷新令牌
services.setSupportRefreshToken(true);
// 令牌存储策略
services.setTokenStore(tokenStore);
// 令牌增强
// TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
// tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
// services.setTokenEnhancer(tokenEnhancerChain);
// 令牌默认有效期: 两小时
services.setAccessTokenValiditySeconds(2 * 60 * 60);
// 刷新令牌默认有效期: 3天
services.setRefreshTokenValiditySeconds(3 * 24 * 60 * 60);
return services;
}
/**
* 设置授权码模式用户授权 client 信息的存储方式
*
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
/**
* 设置授权码模式的授权码如何获取,从数据库获取
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* 令牌访问端点
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 认证管理器
.authenticationManager(authenticationManager)
// 授权 client 信息
.approvalStore(approvalStore())
// 授权码服务
.authorizationCodeServices(authorizationCodeServices())
// 令牌管理服务
.tokenServices(tokenServices())
// 配置授权端点 url
// .pathMapping("/oauth/authorize", "/auth/authorize")
// 允许 post 提交
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
/**
* 令牌访问端点安全策略
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.
// 公有密钥 /oauth/token_key 公开
tokenKeyAccess("permitAll()")
// 检测令牌 /oauth/check_token 公开
.checkTokenAccess("permitAll()")
// 允许表单认证(申请令牌)
.allowFormAuthenticationForClients();
}
}