文章目录
给自己主要是了解其思路,从而得到自己的想法,去实现自己的一套权限校验认证模块。实际上本文档基于Spring-cloud-oauth2来编写,与spring-security-oauth2有些许出入,以后续文章为主,主要是提供大致描述
一、附带的sql
oauth_client_details (授权码)
主要操作
oauth_client_details
表的类是JdbcClientDetailsService.java
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL COMMENT '客户端标识',
`resource_ids` varchar(255) DEFAULT NULL COMMENT '接入资源列表',
`client_secret` varchar(255) DEFAULT NULL COMMENT '客户端秘钥',
`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` longtext,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`archived` tinyint(4) DEFAULT NULL,
`trusted` tinyint(4) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='接入客户端信息';
字段名 | 类型 | 描述 |
---|---|---|
client_id | varchar | 主键,必须唯一,不能为空. 用于唯一标识每一个客户端(client); |
resource_ids | varchar | 客户端所能访问的资源id集合,多个资源时用逗号(,) |
client_secret | varchar | 用于指定客户端(client)的访问密匙 |
scope | varchar | 指定客户端申请的权限范围,可选值包括read,write,trust,all(可自定义);若有多个权限范围用逗号(,)分隔 |
authorized_grant_types | varchar | 就是5种授权模式:authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔, |
web_server_redirect_uri | varchar | 重定向地址,在oauth中会校验 |
authorities | varchar | 指定客户端所拥有的权限值,可选, 若有多个权限值,用逗号(,)分隔。若授权模式中不需要账户密码的建议设;若授权模式需要账户密码的,可以不设立 |
access_token_validity | int | 设定客户端的access_token的有效时间值(单位:秒) 【默认12小时】 |
refresh_token_validity | int | 设定客户端的refresh_token的有效时间值(单位:秒)【默认30天】 |
additional_information | longtext | 可以额外附带的信息,若赋值,则需要json规范 |
create_time | timestamp | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
archived | tinyint | 标识是否存档,默认为0 |
trusted | tinyint | 标识是否受信任,默认0(0-不信任,1-信任) |
autoapprove | varchar | 设置用户是否自动Approval操作,通常只在authorization_code 模式有效 |
oauth_code(支持授权码获取accessToken)
CREATE TABLE IF NOT EXISTS `oauth_code` (
`code` VARCHAR(256) NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
oauth_access_token(余下方式使用)
对
oauth_client_token
表的主要操作在JdbcClientTokenServices.java
,实际上未使用到
CREATE TABLE IF NOT EXISTS `oauth_access_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) NULL DEFAULT NULL,
`client_id` VARCHAR(256) NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL,
`refresh_token` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
字段名 | 类型 | 描述 |
---|---|---|
token_id | varchar | 从服务器端获取到的access_token 的值. |
token | BLOB | 这是一个二进制的字段, 存储的数据OAuth2AccessToken.java 对象序列化后的二进制数据. |
authentication_id | varchar | 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultClientKeyGenerator.java 类. |
user_name | varchar | 登录时的用户名 |
client_id | varchar | 主键,必须唯一,不能为空. 用于唯一标识每一个客户端(client); |
authentication | varchar | 存储将OAuth2Authentication.java 对象序列化后的二进制数据. |
refresh_token | varchar | 该字段的值是将refresh_token 的值通过MD5加密后存储的. |
oauth_refresh_token(余下方式使用)
CREATE TABLE IF NOT EXISTS `oauth_refresh_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
字段名 | 类型 | 描述 |
---|---|---|
token_id | varchar | 从服务器端获取到的access_token 的值. |
token | BLOB | 这是一个二进制的字段, 存储的数据OAuth2AccessToken.java 对象序列化后的二进制数据. |
authentication | varchar | 存储将OAuth2Authentication.java 对象序列化后的二进制数据. |
二、AuthorizationServerConfigurer(授权服务配置)
总体围绕着 AuthorizationServerConfigurer的实现进行设计,我们需要自行实现其三个抽象方法,若需要启用Oauth2授权,则需要加入
@EnableAuthorizationServer
(这个注释 位置不限定)
实际上实现类`OAuth2AuthorizationServerConfiguration` 可以当做用于参考自定义实现时使用
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
1、ClientDetailsServiceConfigurer (配置客户端详情)
可以选择客户端模式:通常有
内存模式
与jdbc模式
配置用户资源,说白了就是校验的资源
其中需要包含主要参数,鉴权时会根据参数得到不一样的结果。
# client_id: 也就是校验唯一值,通常针对客户
# client_secret: 加密方式(通常设置方法,可自行加盐)
# resource_ids: 针对资源的唯一值
#authorized_grant_types: 支持授权的方式,包含如下5种
- authorization_code 授权码方式 【需要授权页面】
- password 密码模式 【使用账户密码来发令牌】
- client_credentials 客户凭证模式 【验证 client_id 与 client_scret(也就是约定的密钥)即可】
- implicit 简化模式(也有隐藏式)【直接发令牌】
- refresh_token 刷新令牌
#scopes: 代表授权范围
如果需要可以直接 all
#autoApprove : 如果为true 会自动跳过授权过程
#redirect_uri:顾名思义的跳转地址
2、AuthorizationServerSecurityConfigurer(配置令牌安全约束)
一般我们会在这里配置几个参数
①allowFormAuthenticationForClients(允许表单验证)
②passwordEncoder(设置oauth_client_details加密方式)
通常为 默认的BCryptPasswordEncoder()
③tokenKeyAccess (查询token信息管理)
若默认则拒绝通过access_token查询token信息 ,默认为:denyAll()
可选值: isAuthenticated(挡住匿名者)、isFullyAuthenticated(挡住匿名者和记住我)、permitAll(允许所有)、denyAll(拒绝所有)
这里引用jdbcStore的话需要重写一下 `readAccessToken`方法,否则会出 `Failed to find access token for token XX`错误。实际上已经从jdbc中获取了值
... extends JdbcTokenStore
@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken accessToken = null;
try {
accessToken = new DefaultOAuth2AccessToken(tokenValue);
} catch (EmptyResultDataAccessException e) {
if (LOG.isInfoEnabled()) {
LOG.info("Failed to find access token for token " + tokenValue);
}
} catch (IllegalArgumentException e) {
LOG.warn("Failed to deserialize access token for " + tokenValue, e);
removeAccessToken(tokenValue);
}
return accessToken;
}
④checkTokenAccess(校验token是否可用)
若默认则拒绝通过access_token查询token信息 ,默认为:denyAll()
可选值: isAuthenticated(挡住匿名者)、isFullyAuthenticated(挡住匿名者和记住我)、permitAll(允许所有)、denyAll(拒绝所有)
其中basic auth 使用对应 client-id 与 client-secret即可
3、AuthorizationServerEndpointsConfigurer(配置令牌端点服务)
一般来说我们需要配置如下几个参数
①authenticationManager(密码模式服务)
如果不需要兼容 password 这种授权模式的话,可以不配置
②authorizationCodeServices(授权码模式服务)
如果不配置的话,其实默认是走内存模型的
通常我们会配合jdbc来可视化这些数据。也就是oauth_code表
主要为
authorization_code
模式服务,获取access_token用到
// 可以参考类AuthorizationServerEndpointsConfigurer
private AuthorizationCodeServices authorizationCodeServices() {
if (authorizationCodeServices == null) {
authorizationCodeServices = new InMemoryAuthorizationCodeServices();
}
return authorizationCodeServices;
}
③tokenServices (令牌管理服务)
如何控制令牌存放,已经超时,可刷新等问题
@Autowired
private ClientDetailsService clientDetailsService;//基于1选择的客户端模式会自动生成jdbc或内存的具体实现
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setSupportRefreshToken(true);//是否支持刷新令牌
services.setTokenStore(tokenStore); //token的存放模式
services.setAccessTokenValiditySeconds(7200); //控制超时时间
services.setRefreshTokenValiditySeconds(259200);//控制刷新令牌的时间
return services;
}
TokenStore参数
这个实际上可以有 `5 种方式实现`,参考TokenStore实现类
1、JdbcTokenStore 直接基于jdbc存贮
2、JwkTokenStore 直接拓展支持了JSON Web Key (JWK)、JSON Web Token (JWT)与JSON Web Signature (JWS)
3、InMemoryTokenStore 基于内存管理
4、RedisTokenStore 基于redis管理
5、JwtTokenStore 基于jwt 存储,难以限定时间,需要自己让其变得无状态
④allowedTokenEndpointRequestMethods
就很直接发放令牌的方式,当然最好以post请求最好,当然你也可以填其他
allowedTokenEndpointRequestMethods(HttpMethod.POST);
⑤userDetailsService(密码服务校验用)
可以直接设置进来,也可以直接 实现
UserDetailsService
类,效果是一致的校验所需授权客户端传入的参数,说白了就是帐号密码
⑥pathMapping(替换默认授权路径)
第一个为默认端点url地址,第二个为自定义的端点url地址
# IDEA 启动的话可以在Endpoints的mapping下查看到具体类路径
/oauth/authorize :授权的端点
/oauth/token :令牌的端点。
/oauth/confirm_access :用户确认授权提交端点(也就是自定义的授权页面端点)。
/oauth/error :授权服务错误信息端点。
/oauth/check_token :用于资源服务访问的令牌解析端点。
/oauth/token_key :如果使用JWT令牌的话,这个端点可以提供公有密匙。
三、授权模式
(1)授权码模式(authorization_code)
access_token 只能通过
authorization_code
的方式获取真正的access_token①获取授权码 ②根据授权码去获取真正的access_token
获取授权码
接口地址: ${ip}:{port}/oauth/authorize
请求方式: Get
字段名 | 描述 |
---|---|
client_id | 改值必须和配置在clients中的值保持一致 |
response_type | 固定传值code 表示使用授权码模式进行认证 |
scope | 改值必须配置的clients中的值一致 |
redirect_uri | 获取code之后重定向的地址,必须和配置的clients一致 |
获取accessToken
接口地址: ${ip}:{port}/oauth/token
请求方式: Post
字段名 | 描述 |
---|---|
client_id | 改值必须和配置在clients中的值保持一致 |
client_secret | 未加密前的密钥,默认使用Bcrypt方法 |
code | 第一步中获取的授权码 |
grant_type | 固定传值authorization_code 表示使用授权码获取accessToken |
redirect_uri | 获取code之后重定向的地址,必须和配置的clients一致 |
{
"access_token": "2d4ca73a-b55c-44c6-9888-07c688df484d",
"token_type": "bearer",
"refresh_token": "9506e65a-2415-4a14-b9d0-4ab953d681af",
"expires_in": 29,
"scope": "all"
}
(2)简化模式(implicit)
撤销授权码,直接把access_token放到浏览器地址上,与授权码模式只是
response_type
不一样
接口地址: ${ip}:{port}/oauth/authorize
请求方式: Get
字段名 | 描述 |
---|---|
client_id | 改值必须和配置在clients中的值保持一致 |
response_type | 固定传值token 表示使用授权码模式进行认证 |
scope | 改值必须配置的clients中的值一致 |
redirect_uri | 获取code之后重定向的地址,必须和配置的clients一致 |
(3)密码模式 (password)
用户密码帐号直接包含在请求上,通常需要开发环境在能掌控范围内
接口地址: ${ip}:{port}/oauth/token
请求方式: Post
字段名 | 描述 |
---|---|
client_id | 改值必须和配置在clients中的值保持一致 |
client_secret | 改值必须和配置在clients中的值保持一致 |
grant_type | 在密码模式下,该值固定为password |
username | 用户名 |
password | 密码 |
{
"access_token": "e8c49c64-e419-426d-aecc-c38a9c9c236e",
"token_type": "bearer",
"refresh_token": "4801611a-3cc8-46f3-9b0e-b26b858adde0",
"expires_in": 29,
"scope": "all"
}
(4)客户端模式(client_credentials)
通常为给对接方提供,双方百分百安全的情况下比较稳妥,因为会直接发令牌
接口地址: ${ip}:{port}/oauth/token
请求方式: Post
字段名 | 描述 |
---|---|
client_id | 改值必须和配置在clients中的值保持一致 |
client_secret | 改值必须和配置在clients中的值保持一致 |
grant_type | 在密码模式下,该值固定为client_credentials |
{
"access_token": "ffe20306-723c-4007-a595-70a9df4f4eb8",
"token_type": "bearer",
"expires_in": 29,
"scope": "all"
}
刷新密钥(refresh_token)
通常需要一个
refresh_token
才能执行这个方法
接口地址: ${ip}:{port}/oauth/token
请求方式: Post
字段名 | 描述 |
---|---|
client_id | 该值必须和配置在clients中的值保持一致 |
client_secret | 该值必须和配置在clients中的值保持一致 |
grant_type | 如果想根据refresh_token换新的token,这里固定传值refresh_token |
refresh_token | 未失效的refresh_token |
{
"access_token": "3974646c-f764-493e-ba99-64fe6cbc11a3",
"token_type": "bearer",
"refresh_token": "2d266ae6-b620-4243-baf0-5f1974709015",
"expires_in": 29,
"scope": "all"
}
四、定义权限控制(配置url的权限控制)
构造一个实现
WebSecurityConfigurerAdapter
的类,利用**@configuration** 交由spring管理
// WebSecurityConfig
//安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() //主要避开所有请求校验头中含有token的问题
.authorizeRequests()
.antMatchers("/login*", "/css/*").permitAll()
.anyRequest().authenticated()
/*登陆相关*/
.and()
.formLogin()//允许表单提交登陆
.loginPage("/login.html")
.loginProcessingUrl("/login")
/*退出相关*/
.and()
.logout()
.permitAll();
}
1、url限定权限登入
①根据角色
//下列中都皆校验 需要 ADMIN 角色,其中 仅有hasAuthority及衍生hasAnyAuthority不会在校验前给你角色默认增加 ROLE_ 前缀
.antMatchers("/test/admin").hasRole("ADMIN")
.antMatchers("/test/admin").access("hasRole('ROLE_NORMAL')")
.antMatchers("/test/admin").hasAuthority("ADMIN")
②根据ip
.antMatchers("/test/admin").hasIpAddress("192.168.2.212")
五、常用权限注解(3个)
实际运用上,我们需要对资源进行管理,搭配自己设定的权限内容进行限定(也就是authorities),说到底也是整合了spring-security的方式
@Secured()
在任意配置类上增加@EnableGlobalMethodSecurity(securedEnabled = true),角色上需要附带前缀
ROLE_
,否则直接不予辨认
//AbstractAccessDecisionManager类可以参考,org.springframework.security.access.vote.AbstractAccessDecisionManager
//参考 supports 方法,最终会调用默认值前缀验证匹配 `ROLE_`
eg: @Secured(“ROLE_user”),必须包含 ROLE_ 前缀,多选则 @Secured({“ROLE_user”,“ROLE_test”})
@PreAuthorize() (我常用)
在任意配置类上增加 @EnableGlobalMethodSecurity(prePostEnabled = true)
即可支持 对应
org.springframework.security.access.prepost
下的所有注解
只需要注释到所需要校验的资源上即可,另外其支持SpEL表达式,对应spring-security表达式的位置`org.springframework.security.access.expression.SecurityExpressionRoot`
通常使用到的可能就 hasRole()及 hasAuthority()
这里要注意,除非你的角色中本身存在 `ROLE_` 前缀,否则还是使用hasAuthority 替代 hasRole,我目前找不到对应配置能直接改默认前缀,除非重写
eg: @PreAuthorize(“hasRole(‘user’)”) 等同于@PreAuthorize(“hasRole(‘ROLE_user’)”) 相当于 @PreAuthorize(“hasAuthority(‘ROLE_user’)”)
@RolesAllowed()
在任意配置类上增加@EnableGlobalMethodSecurity(jsr250Enabled = true)
即可支持对应
javax.annotation.security
下的所有注解这里与
@PreAuthorize
一致,会默认增加前缀ROLE_
eg: @RolesAllowed(“user”) 等于@RolesAllowed(“ROLE_user”) ,相当于校验 ROLE_user