mirror of
https://gitee.com/dromara/sa-token.git
synced 2024-12-02 03:47:50 +08:00
新增 scope 等级划分,可指定哪些权限需要强制每次手动授权
This commit is contained in:
parent
1bc59dc14c
commit
4aa4941598
@ -17,6 +17,7 @@ package cn.dev33.satoken.context.model;
|
||||
|
||||
import cn.dev33.satoken.error.SaErrorCode;
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
import java.util.List;
|
||||
@ -157,6 +158,24 @@ public interface SaRequest {
|
||||
*/
|
||||
String getMethod();
|
||||
|
||||
/**
|
||||
* 返回当前请求 Method 是否为指定值
|
||||
* @param method method
|
||||
* @return /
|
||||
*/
|
||||
default boolean isMethod(String method) {
|
||||
return getMethod().equals(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前请求 Method 是否为指定值
|
||||
* @param method method
|
||||
* @return /
|
||||
*/
|
||||
default boolean isMethod(SaHttpMethod method) {
|
||||
return getMethod().equals(method.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断此请求是否为 Ajax 异步请求
|
||||
* @return /
|
||||
|
@ -678,4 +678,63 @@ public class SaFoxUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* list1 是否完全包含 list2 中所有元素
|
||||
* @param list1 集合1
|
||||
* @param list2 集合2
|
||||
* @return /
|
||||
*/
|
||||
public static boolean list1ContainList2AllElement(List<String> list1, List<String> list2){
|
||||
if(list2 == null || list2.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if(list1 == null || list1.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String str : list2) {
|
||||
if(!list1.contains(str)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* list1 是否包含 list2 中任意一个元素
|
||||
* @param list1 集合1
|
||||
* @param list2 集合2
|
||||
* @return /
|
||||
*/
|
||||
public static boolean list1ContainList2AnyElement(List<String> list1, List<String> list2){
|
||||
if(list1 == null || list1.isEmpty() || list2 == null || list2.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String str : list2) {
|
||||
if(list1.contains(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 list1 中剔除 list2 所包含的元素 (克隆副本操作,不影响 list1)
|
||||
* @param list1 集合1
|
||||
* @param list2 集合2
|
||||
* @return /
|
||||
*/
|
||||
public static List<String> list1RemoveByList2(List<String> list1, List<String> list2){
|
||||
if(list1 == null) {
|
||||
return null;
|
||||
}
|
||||
if(list1.isEmpty() || list2 == null || list2.isEmpty()) {
|
||||
return new ArrayList<>(list1);
|
||||
}
|
||||
List<String> listX = new ArrayList<>(list1);
|
||||
for (String str : list2) {
|
||||
listX.remove(str);
|
||||
}
|
||||
return listX;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ sa-token:
|
||||
enable-password: true
|
||||
# 是否全局开启客户端模式
|
||||
enable-client: true
|
||||
# 定义哪些 scope 是高级权限,多个用逗号隔开
|
||||
# higher-scope: openid,userid
|
||||
# 定义哪些 scope 是低级权限,多个用逗号隔开
|
||||
# lower-scope: userinfo
|
||||
|
||||
spring:
|
||||
# redis配置
|
||||
|
@ -33,20 +33,31 @@
|
||||
console.log('-----------');
|
||||
$.ajax({
|
||||
url: '/oauth2/doConfirm',
|
||||
method: "POST",
|
||||
data: {
|
||||
client_id: getParam('client_id'),
|
||||
scope: getParam('scope')
|
||||
scope: getParam('scope'),
|
||||
// 以下四个参数必须一起出现
|
||||
build_redirect_uri: true,
|
||||
response_type: getParam('response_type'),
|
||||
redirect_uri: getParam('redirect_uri'),
|
||||
state: getParam('state'),
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.code == 200) {
|
||||
console.log('res:', res);
|
||||
if(res.code === 200) {
|
||||
layer.msg('授权成功!');
|
||||
setTimeout(function() {
|
||||
location.reload(true);
|
||||
if (res.redirect_uri) {
|
||||
location.href = res.redirect_uri;
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}, 800);
|
||||
} else {
|
||||
// 重定向至授权失败URL
|
||||
layer.alert('授权失败!');
|
||||
layer.alert('授权失败:' + res.msg);
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
|
@ -59,13 +59,14 @@
|
||||
- [定制化登录页面与授权页面](/oauth2/oauth2-custom-login)
|
||||
- [自定义 API 路由 ](/oauth2/oauth2-custom-api)
|
||||
- [自定义 Scope 权限以处理器](/oauth2/oauth2-custom-scope-handler)
|
||||
- [为 Scope 划分等级](/oauth2/7)
|
||||
<!-- - [前后端分离模式整合方案](/oauth2/4) -->
|
||||
- [平台中心模式开发](/oauth2/5)
|
||||
- [为 Scope 划分等级](/oauth2/oauth2-scope-level)
|
||||
- [自定义 grant_type](/oauth2/oauth2-custom-grant_type)
|
||||
- [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking)
|
||||
- [OAuth2 代码 API 参考](/oauth2/oauth2-dev)
|
||||
- [常见问题说明](/oauth2/8)
|
||||
- <!-- jwt风格token、使用注解校验权限、OAuth2整个流程参考、state参数详解 -->
|
||||
<!-- - [前后端分离模式整合方案](/oauth2/4) -->
|
||||
<!-- - [平台中心模式开发](/oauth2/5) -->
|
||||
- <!-- jwt风格token、使用注解校验权限、state参数详解 -->
|
||||
|
||||
- **微服务**
|
||||
- [分布式Session会话](/micro/dcs-session)
|
||||
|
@ -43,6 +43,82 @@ redirect_uri?code={code}&state={state}
|
||||
4. 每次授权产生新 `Code` 码,会导致旧 `Code` 码立即作废,即使旧 `Code` 码尚未使用。
|
||||
|
||||
|
||||
<details>
|
||||
<summary>RestAPI 登录接口:/oauth2/doLogin</summary>
|
||||
|
||||
如果用户在 OAuth-Server 端尚未登录,则会被阻塞在登录界面,开始登录,需要在页面上调用`/oauth2/doLogin`完成登录(此接口非 OAuth2 标准协议接口)
|
||||
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/doLogin
|
||||
?name={name}
|
||||
&pwd={pwd}
|
||||
```
|
||||
参数详解:
|
||||
|
||||
| 参数 | 是否必填 | 说明 |
|
||||
| :-------- | :-------- | :-------- |
|
||||
| name | 否 | 账号 |
|
||||
| pwd | 否 | 密码 |
|
||||
|
||||
访问此接口将进入自定义的 `cfg.doLoginHandle` 函数开始登录,你只要在此函数内调用 `StpUtil.login(xxx)` 即代表登录成功。
|
||||
|
||||
另外需要注意:此接口并非只能携带 `name`、`pwd` 参数,因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>RestAPI 确认授权接口:/oauth2/doConfirm</summary>
|
||||
|
||||
如果 oauth-client 端申请的 scope 在 OAuth-Server 端需要用户手动确认授权,则会被阻塞在授权界面,
|
||||
需要在页面上调用`/oauth2/doConfirm`完成授权(此接口非 OAuth2 标准协议接口)
|
||||
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/doConfirm
|
||||
?client={value}
|
||||
&scope={value}
|
||||
&build_redirect_uri={true|false}
|
||||
&response_type={value}
|
||||
&redirect_uri={value}
|
||||
&state={value}
|
||||
```
|
||||
参数详解:
|
||||
|
||||
| 参数 | 是否必填 | 说明 |
|
||||
| :-------- | :-------- | :-------- |
|
||||
| client_id | 是 | 应用 id |
|
||||
| scope | 是 | 具体确认的权限,多个用逗号(或空格)隔开 |
|
||||
| build_redirect_uri | 否 | 是否立即构建 `redirect_uri` 授权地址,取值:true | false |
|
||||
| response_type | 否 | 取 url 上的 `response_type` 参数来提交 |
|
||||
| redirect_uri | 否 | 取 url 上的 `redirect_uri` 参数来提交 |
|
||||
| state | 否 | 取 url 上的 `state` 参数来提交 |
|
||||
|
||||
此接口有两种调用方式,一种只提供 `client_id`、`scope` 两个参数,此时返回结果代表是否确认授权成功:
|
||||
``` js
|
||||
{
|
||||
code: 200,
|
||||
msg: 'ok',
|
||||
data: null,
|
||||
}
|
||||
```
|
||||
|
||||
一种是指定 `build_redirect_uri: true`,并同时提供 `client_id`、`scope`、`response_type`、`redirect_uri`、`state` 全部参数,
|
||||
此时返回结果包括最终的 code 授权地址:
|
||||
``` js
|
||||
{
|
||||
code: 200,
|
||||
msg: 'ok',
|
||||
data: null,
|
||||
redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC'
|
||||
}
|
||||
```
|
||||
|
||||
前端在 ajax 回调函数中直接使用 `location.href=res.redirect_uri` 跳转即可,无需再重复访问 `/oauth2/authorize` 接口。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
### 1.2、根据授权码获取 Access-Token
|
||||
获得 `Code` 码后,我们可以通过以下接口,获取到用户的 `Access-Token`、`Refresh-Token` 等信息。
|
||||
|
||||
@ -138,7 +214,7 @@ http://{host}:{port}/oauth2/revoke
|
||||
|
||||
|
||||
### 1.5、根据 Access-Token 获取相应用户的账号信息
|
||||
注:此接口为官方仓库模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数
|
||||
注:此接口非 OAuth2 标准协议接口,为官方仓库 demo 模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数
|
||||
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/userinfo?access_token={access_token}
|
||||
|
@ -139,7 +139,7 @@ http://sa-oauth-server.com:8000/oauth2/authorize
|
||||
|
||||
|
||||
#### 3、code 换 access_token
|
||||
3、访问上述链接后,得到 `code` 授权码,然后我们拿着 `code` 换 `access_token`
|
||||
访问上述链接后,得到 `code` 授权码,然后我们拿着 `code` 换 `access_token`
|
||||
``` url
|
||||
http://sa-oauth-server.com:8000/oauth2/token
|
||||
?grant_type=authorization_code
|
||||
|
137
sa-token-doc/oauth2/oauth2-scope-level.md
Normal file
137
sa-token-doc/oauth2/oauth2-scope-level.md
Normal file
@ -0,0 +1,137 @@
|
||||
# OAuth2 - 为 Scope 划分等级
|
||||
|
||||
|
||||
### 1、划分等级
|
||||
|
||||
我们可以通过配置文件来为 scope 划分等级
|
||||
|
||||
<!---------------------------- tabs:start ---------------------------->
|
||||
<!------------- tab:yaml 风格 ------------->
|
||||
``` yaml
|
||||
# sa-token 配置
|
||||
sa-token:
|
||||
# OAuth2.0 配置
|
||||
oauth2:
|
||||
# 定义哪些 scope 是高级权限,多个用逗号隔开
|
||||
higher-scope: openid,userid
|
||||
# 定义哪些 scope 是低级权限,多个用逗号隔开
|
||||
lower-scope: userinfo
|
||||
```
|
||||
<!------------- tab:properties 风格 ------------->
|
||||
``` properties
|
||||
# 定义哪些 scope 是高级权限,多个用逗号隔开
|
||||
sa-token.oauth2.higher-scope=openid,userid
|
||||
# 定义哪些 scope 是低级权限,多个用逗号隔开
|
||||
sa-token.oauth2.lower-scope=userinfo
|
||||
```
|
||||
<!---------------------------- tabs:end ---------------------------->
|
||||
|
||||
如上所示:
|
||||
- 通过 `sa-token.oauth2.higher-scope` 配置项指定的 `scope` 将变成 **高级权限**。
|
||||
- 通过 `sa-token.oauth2.lower-scope` 配置项指定的 `scope` 将变成 **低级权限**。
|
||||
- 其它未指定的 `scope` 将默认为 **一般权限**。
|
||||
|
||||
不同的权限等级其差异主要表现在:oauth2-client 授权时是否需要用户手动确认授权。
|
||||
|
||||
| 权限等级 | 申请授权时表现 |
|
||||
| :-------- | :-------- |
|
||||
| 高级权限 | 申请授权时:每次都需要用户手动点击确认授权按钮,才会下放 code 授权码 |
|
||||
| 一般权限 | 申请授权时:如果申请的 scope 用户近期授权过,则静默授权,如果近期未授权过,则需要手动点击确认授权按钮 |
|
||||
| 低级权限 | 申请授权时:不需要用户手动点击确认授权,程序自动完成静默授权 |
|
||||
|
||||
|
||||
### 2、详细举例
|
||||
|
||||
1、如下例子,oauth2-client 申请的 `openid` 权限为**高级权限**,每次都需要用户手动点击确认授权按钮,才会下放 code 授权码。
|
||||
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/authorize
|
||||
?response_type=code
|
||||
&client_id=1001
|
||||
&redirect_uri=http://sa-oauth-client.com:8002/
|
||||
&scope=openid
|
||||
```
|
||||
|
||||
2、如下例子,oauth2-client 申请的 `userinfo` 权限为**低级权限**,此时不需要用户手动点击确认授权,程序自动完成静默授权。
|
||||
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/authorize
|
||||
?response_type=code
|
||||
&client_id=1001
|
||||
&redirect_uri=http://sa-oauth-client.com:8002/
|
||||
&scope=userinfo
|
||||
```
|
||||
|
||||
3、如下例子,oauth2-client 申请的 `fans_list` 权限为**一般权限**,首次申请时,需要用户手动点击确认授权,第二次再申请则是静默授权。
|
||||
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/authorize
|
||||
?response_type=code
|
||||
&client_id=1001
|
||||
&redirect_uri=http://sa-oauth-client.com:8002/
|
||||
&scope=fans_list
|
||||
```
|
||||
|
||||
4、如下例子,oauth2-client 申请的 `openid,userid,userinfo,fans_list` 权限同时包括 **高级权限**、**低级权限**、**一般权限**:
|
||||
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/authorize
|
||||
?response_type=code
|
||||
&client_id=1001
|
||||
&redirect_uri=http://sa-oauth-client.com:8002/
|
||||
&scope=openid,userid,userinfo,fans_list
|
||||
```
|
||||
|
||||
此时是否需要用户手动点击确认授权按钮?具体规则表现为:
|
||||
- 如果请求的 scope 列表包括高级权限,则必须用户手动点击确认授权。
|
||||
- 如果 scope 列表不包括高级权限,则将 scope 列表中的所有低级权限剔除。
|
||||
- 剔除后的 list 大小如果为零,则直接静默授权通过。
|
||||
- 剔除后的 list 大小如果不为零,则判断剩余的这些 scope 是否全部已近期授权过:
|
||||
- 如果是,则静默授权。
|
||||
- 如果否,则需要用户手动点击确认授权。
|
||||
|
||||
|
||||
### 3、申请高级权限时 `/oauth2/authorize` 无法通过验证
|
||||
|
||||
由于申请高级权限时,每次都必须用户手动点击确认授权,`/oauth2/authorize` 路由接口是无法完成权限验证操作的。
|
||||
|
||||
此时需要将构建 `redirect_uri` 的动作提前,在 `/oauth2/doConfirm` 确认授权接口时额外追加 `build_redirect_uri: true` 等参数:
|
||||
``` url
|
||||
http://{host}:{port}/oauth2/doConfirm
|
||||
?client={value}
|
||||
&scope={value}
|
||||
&build_redirect_uri=true
|
||||
&response_type={value}
|
||||
&redirect_uri={value}
|
||||
&state={value}
|
||||
```
|
||||
|
||||
返回结果示例:
|
||||
``` js
|
||||
{
|
||||
code: 200,
|
||||
msg: 'ok',
|
||||
data: null,
|
||||
redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC'
|
||||
}
|
||||
```
|
||||
|
||||
其中 `redirect_uri` 参数为授权挂载code地址,直接在 ajax 回调函数中使用 `location.href=res.redirect_uri` 跳转即可。
|
||||
|
||||
自定义确认授权视图修改参考:
|
||||
``` java
|
||||
// 授权确认视图
|
||||
cfg.confirmView = (clientId, scopes)->{
|
||||
String scopeStr = SaFoxUtil.convertListToString(scopes);
|
||||
String yesCode =
|
||||
"fetch('/oauth2/doConfirm' + location.search + '&build_redirect_uri=true', {method: 'POST'})" +
|
||||
".then(res => res.json())" +
|
||||
".then(res => location.href=res.redirect_uri)";
|
||||
String res = "<p>应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?</p>"
|
||||
+ "<p>" +
|
||||
" <button onclick=\"" + yesCode + "\">同意</button>" +
|
||||
" <button onclick='history.back()'>拒绝</button>" +
|
||||
"</p>";
|
||||
return res;
|
||||
};
|
||||
```
|
@ -119,10 +119,16 @@ public class SaOAuth2ServerController {
|
||||
// 配置:确认授权时返回的 view
|
||||
cfg.confirmView = (clientId, scopes) -> {
|
||||
String scopeStr = SaFoxUtil.convertListToString(scopes);
|
||||
String msg = "<p>应用 " + clientId + " 请求授权:" + scopeStr + "</p>"
|
||||
+ "<p>请确认:<a href='/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "' target='_blank'> 确认授权 </a></p>"
|
||||
+ "<p>确认之后刷新页面</p>";
|
||||
return msg;
|
||||
String yesCode =
|
||||
"fetch('/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "', {method: 'POST'})" +
|
||||
".then(res => res.json())" +
|
||||
".then(res => location.reload())";
|
||||
String res = "<p>应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?</p>"
|
||||
+ "<p>" +
|
||||
" <button onclick=\"" + yesCode + "\">同意</button>" +
|
||||
" <button onclick='history.back()'>拒绝</button>" +
|
||||
"</p>";
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -413,13 +413,14 @@ body {
|
||||
background-color: #f4fdef;
|
||||
overflow: hidden;
|
||||
max-height: 44px;
|
||||
margin-bottom: 1em;
|
||||
/* transition: all 1s; */
|
||||
}
|
||||
.main-box details[open]{ /* max-height: 1000px; */ overflow: auto; animation: slideDown 0.4s linear both;}
|
||||
.main-box details[open]{ /* max-height: 1000px; */ overflow: auto; animation: slideDown 0.6s linear both;}
|
||||
@keyframes slideDown {
|
||||
0% { max-height: 44px; overflow: hidden; }
|
||||
99% { max-height: 1000px; overflow: hidden; }
|
||||
100% { max-height: 1000px; overflow: auto; }
|
||||
99% { max-height: 1500px; overflow: hidden; }
|
||||
100% { max-height: 1500px; overflow: auto; }
|
||||
}
|
||||
.main-box details summary{
|
||||
padding: 11px 14px;
|
||||
@ -429,8 +430,9 @@ body {
|
||||
}
|
||||
.main-box details pre{
|
||||
margin-left: 1em;
|
||||
margin-right: 14px;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.main-box details table{margin-left: 1em !important; margin-right: 1em; width: auto;}
|
||||
.main-box details p{padding: 0 14px;}
|
||||
|
||||
/* 广告盒子 */
|
||||
|
@ -66,7 +66,11 @@ public class SaOAuth2Config implements Serializable {
|
||||
/** 默认 openid 生成算法中使用的摘要前缀 */
|
||||
public String openidDigestPrefix = SaOAuth2Consts.OPENID_DEFAULT_DIGEST_PREFIX;
|
||||
|
||||
/** 指定高级权限,多个用逗号隔开 */
|
||||
public String higherScope;
|
||||
|
||||
/** 指定低级权限,多个用逗号隔开 */
|
||||
public String lowerScope;
|
||||
|
||||
/**
|
||||
* @return enableCode
|
||||
@ -77,9 +81,11 @@ public class SaOAuth2Config implements Serializable {
|
||||
|
||||
/**
|
||||
* @param enableCode 要设置的 enableCode
|
||||
* @return /
|
||||
*/
|
||||
public void setEnableCode(Boolean enableCode) {
|
||||
public SaOAuth2Config setEnableCode(Boolean enableCode) {
|
||||
this.enableCode = enableCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,9 +97,11 @@ public class SaOAuth2Config implements Serializable {
|
||||
|
||||
/**
|
||||
* @param enableImplicit 要设置的 enableImplicit
|
||||
* @return /
|
||||
*/
|
||||
public void setEnableImplicit(Boolean enableImplicit) {
|
||||
public SaOAuth2Config setEnableImplicit(Boolean enableImplicit) {
|
||||
this.enableImplicit = enableImplicit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,8 +114,9 @@ public class SaOAuth2Config implements Serializable {
|
||||
/**
|
||||
* @param enablePassword 要设置的 enablePassword
|
||||
*/
|
||||
public void setEnablePassword(Boolean enablePassword) {
|
||||
public SaOAuth2Config setEnablePassword(Boolean enablePassword) {
|
||||
this.enablePassword = enablePassword;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,9 +128,11 @@ public class SaOAuth2Config implements Serializable {
|
||||
|
||||
/**
|
||||
* @param enableClient 要设置的 enableClient
|
||||
* @return /
|
||||
*/
|
||||
public void setEnableClient(Boolean enableClient) {
|
||||
public SaOAuth2Config setEnableClient(Boolean enableClient) {
|
||||
this.enableClient = enableClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,9 +144,11 @@ public class SaOAuth2Config implements Serializable {
|
||||
|
||||
/**
|
||||
* @param isNewRefresh 要设置的 isNewRefresh
|
||||
* @return /
|
||||
*/
|
||||
public void setIsNewRefresh(Boolean isNewRefresh) {
|
||||
public SaOAuth2Config setIsNewRefresh(Boolean isNewRefresh) {
|
||||
this.isNewRefresh = isNewRefresh;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,11 +242,51 @@ public class SaOAuth2Config implements Serializable {
|
||||
* @param openidDigestPrefix 要设置的 openidDigestPrefix
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaOAuth2Config setOpenidMd5Prefix(String openidDigestPrefix) {
|
||||
public SaOAuth2Config setOpenidDigestPrefix(String openidDigestPrefix) {
|
||||
this.openidDigestPrefix = openidDigestPrefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 指定高级权限,多个用逗号隔开
|
||||
*
|
||||
* @return higherScope 指定高级权限,多个用逗号隔开
|
||||
*/
|
||||
public String getHigherScope() {
|
||||
return this.higherScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 指定高级权限,多个用逗号隔开
|
||||
*
|
||||
* @param higherScope 指定高级权限,多个用逗号隔开
|
||||
* @return /
|
||||
*/
|
||||
public SaOAuth2Config setHigherScope(String higherScope) {
|
||||
this.higherScope = higherScope;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 指定低级权限,多个用逗号隔开
|
||||
*
|
||||
* @return lowerScope 指定低级权限,多个用逗号隔开
|
||||
*/
|
||||
public String getLowerScope() {
|
||||
return this.lowerScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 指定低级权限,多个用逗号隔开
|
||||
*
|
||||
* @param lowerScope 指定低级权限,多个用逗号隔开
|
||||
* @return /
|
||||
*/
|
||||
public SaOAuth2Config setLowerScope(String lowerScope) {
|
||||
this.lowerScope = lowerScope;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// -------------------- SaOAuth2Handle 所有回调函数 --------------------
|
||||
|
||||
@ -254,19 +307,20 @@ public class SaOAuth2Config implements Serializable {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaOAuth2Config [" +
|
||||
"enableCode=" + enableCode
|
||||
+ ", enableImplicit=" + enableImplicit
|
||||
+ ", enablePassword=" + enablePassword
|
||||
+ ", enableClient=" + enableClient
|
||||
+ ", isNewRefresh=" + isNewRefresh
|
||||
+ ", codeTimeout=" + codeTimeout
|
||||
+ ", accessTokenTimeout=" + accessTokenTimeout
|
||||
+ ", refreshTokenTimeout=" + refreshTokenTimeout
|
||||
+ ", clientTokenTimeout=" + clientTokenTimeout
|
||||
+ ", pastClientTokenTimeout=" + pastClientTokenTimeout
|
||||
+ ", openidDigestPrefix=" + openidDigestPrefix
|
||||
+"]";
|
||||
return "SaOAuth2Config{" +
|
||||
"enableCode=" + enableCode +
|
||||
", enableImplicit=" + enableImplicit +
|
||||
", enablePassword=" + enablePassword +
|
||||
", enableClient=" + enableClient +
|
||||
", isNewRefresh=" + isNewRefresh +
|
||||
", codeTimeout=" + codeTimeout +
|
||||
", accessTokenTimeout=" + accessTokenTimeout +
|
||||
", refreshTokenTimeout=" + refreshTokenTimeout +
|
||||
", clientTokenTimeout=" + clientTokenTimeout +
|
||||
", pastClientTokenTimeout=" + pastClientTokenTimeout +
|
||||
", openidDigestPrefix='" + openidDigestPrefix +
|
||||
", higherScope='" + higherScope +
|
||||
", lowerScope='" + lowerScope +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ public class SaOAuth2Consts {
|
||||
public static String password = "password";
|
||||
public static String name = "name";
|
||||
public static String pwd = "pwd";
|
||||
public static String build_redirect_uri = "build_redirect_uri";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,4 +113,7 @@ public interface SaOAuth2ErrorCode {
|
||||
/** 暂未开放凭证式模式 */
|
||||
int CODE_30134 = 30134;
|
||||
|
||||
/** 无效的请求 Method */
|
||||
int CODE_30141 = 30141;
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;
|
||||
import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;
|
||||
import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;
|
||||
import cn.dev33.satoken.oauth2.template.SaOAuth2Template;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
@ -124,28 +125,7 @@ public class SaOAuth2ServerProcessor {
|
||||
String responseType = req.getParamNotNull(Param.response_type);
|
||||
|
||||
// 1、先判断是否开启了指定的授权模式
|
||||
// 模式一:Code授权码
|
||||
if(responseType.equals(ResponseType.code)) {
|
||||
if(!cfg.enableCode) {
|
||||
throwErrorSystemNotEnableModel();
|
||||
}
|
||||
if(!currClientModel().enableCode) {
|
||||
throwErrorClientNotEnableModel();
|
||||
}
|
||||
}
|
||||
// 模式二:隐藏式
|
||||
else if(responseType.equals(ResponseType.token)) {
|
||||
if(!cfg.enableImplicit) {
|
||||
throwErrorSystemNotEnableModel();
|
||||
}
|
||||
if(!currClientModel().enableImplicit) {
|
||||
throwErrorClientNotEnableModel();
|
||||
}
|
||||
}
|
||||
// 其它
|
||||
else {
|
||||
throw new SaOAuth2Exception("无效 response_type: " + req.getParam(Param.response_type)).setCode(SaOAuth2ErrorCode.CODE_30125);
|
||||
}
|
||||
checkAuthorizeResponseType(responseType, req, cfg);
|
||||
|
||||
// 2、如果尚未登录, 则先去登录
|
||||
if( ! getStpLogic().isLogin()) {
|
||||
@ -162,8 +142,8 @@ public class SaOAuth2ServerProcessor {
|
||||
oauth2Template.checkContract(ra.clientId, ra.scopes);
|
||||
|
||||
// 6、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面
|
||||
boolean isGrant = oauth2Template.isGrant(ra.loginId, ra.clientId, ra.scopes);
|
||||
if( ! isGrant) {
|
||||
boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);
|
||||
if(isNeedCarefulConfirm) {
|
||||
return cfg.confirmView.apply(ra.clientId, ra.scopes);
|
||||
}
|
||||
|
||||
@ -306,7 +286,7 @@ public class SaOAuth2ServerProcessor {
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaOAuth2Config cfg = SaOAuth2Manager.getConfig();
|
||||
|
||||
return cfg.doLoginHandle.apply(req.getParamNotNull(Param.name), req.getParamNotNull(Param.pwd));
|
||||
return cfg.doLoginHandle.apply(req.getParam(Param.name), req.getParam(Param.pwd));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -316,15 +296,53 @@ public class SaOAuth2ServerProcessor {
|
||||
public Object doConfirm() {
|
||||
// 获取变量
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
|
||||
String clientId = req.getParamNotNull(Param.client_id);
|
||||
Object loginId = getStpLogic().getLoginId();
|
||||
String scope = req.getParamNotNull(Param.scope);
|
||||
List<String> scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(scope);
|
||||
Object loginId = getStpLogic().getLoginId();
|
||||
SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();
|
||||
|
||||
// 此请求只允许 POST 方式
|
||||
if(!req.isMethod(SaHttpMethod.POST)) {
|
||||
throw new SaOAuth2Exception("无效请求方式:" + req.getMethod()).setCode(SaOAuth2ErrorCode.CODE_30141);
|
||||
}
|
||||
|
||||
// 确认授权
|
||||
oauth2Template.saveGrantScope(clientId, loginId, scopes);
|
||||
|
||||
// 判断所需的返回结果模式
|
||||
boolean buildRedirectUri = req.isParam(Param.build_redirect_uri, "true");
|
||||
|
||||
// -------- 情况1:只返回确认结果即可
|
||||
if( ! buildRedirectUri ) {
|
||||
oauth2Template.saveGrantScope(clientId, loginId, scopes);
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// -------- 情况2:需要返回最终的 redirect_uri 地址
|
||||
|
||||
// s3、构建请求 Model
|
||||
RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);
|
||||
|
||||
// 7、判断授权类型,构建不同的重定向地址
|
||||
// 如果是 授权码式,则:开始重定向授权,下放code
|
||||
if(ResponseType.code.equals(ra.responseType)) {
|
||||
CodeModel codeModel = dataGenerate.generateCode(ra);
|
||||
String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);
|
||||
return SaResult.ok().set(Param.redirect_uri, redirectUri);
|
||||
}
|
||||
|
||||
// 如果是 隐藏式,则:开始重定向授权,下放 token
|
||||
if(ResponseType.token.equals(ra.responseType)) {
|
||||
AccessTokenModel at = dataGenerate.generateAccessToken(ra, false);
|
||||
String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);
|
||||
return SaResult.ok().set(Param.redirect_uri, redirectUri);
|
||||
}
|
||||
|
||||
// 默认返回
|
||||
throw new SaOAuth2Exception("无效response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);
|
||||
}
|
||||
|
||||
/**
|
||||
* 模式三:密码式
|
||||
* @return 处理结果
|
||||
@ -408,6 +426,9 @@ public class SaOAuth2ServerProcessor {
|
||||
return SaOAuth2Manager.getDataResolver().buildClientTokenReturnValue(ct);
|
||||
}
|
||||
|
||||
|
||||
// ----------- 代码块封装 --------------
|
||||
|
||||
/**
|
||||
* 根据当前请求提交的 client_id 参数获取 SaClientModel 对象
|
||||
* @return /
|
||||
@ -417,6 +438,34 @@ public class SaOAuth2ServerProcessor {
|
||||
return oauth2Template.checkClientModel(clientIdAndSecret.clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 authorize 路由的 ResponseType 参数
|
||||
*/
|
||||
public void checkAuthorizeResponseType(String responseType, SaRequest req, SaOAuth2Config cfg) {
|
||||
// 模式一:Code授权码
|
||||
if(responseType.equals(ResponseType.code)) {
|
||||
if(!cfg.enableCode) {
|
||||
throwErrorSystemNotEnableModel();
|
||||
}
|
||||
if(!currClientModel().enableCode) {
|
||||
throwErrorClientNotEnableModel();
|
||||
}
|
||||
}
|
||||
// 模式二:隐藏式
|
||||
else if(responseType.equals(ResponseType.token)) {
|
||||
if(!cfg.enableImplicit) {
|
||||
throwErrorSystemNotEnableModel();
|
||||
}
|
||||
if(!currClientModel().enableImplicit) {
|
||||
throwErrorClientNotEnableModel();
|
||||
}
|
||||
}
|
||||
// 其它
|
||||
else {
|
||||
throw new SaOAuth2Exception("无效 response_type: " + req.getParam(Param.response_type)).setCode(SaOAuth2ErrorCode.CODE_30125);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取底层使用的会话对象
|
||||
*
|
||||
|
@ -123,6 +123,7 @@ public class SaOAuth2Template {
|
||||
|
||||
|
||||
// ------------------- check 数据校验
|
||||
|
||||
/**
|
||||
* 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope
|
||||
* @param loginId 账号id
|
||||
@ -135,6 +136,39 @@ public class SaOAuth2Template {
|
||||
List<String> grantScopeList = dao.getGrantScope(clientId, loginId);
|
||||
return scopes.isEmpty() || new HashSet<>(grantScopeList).containsAll(scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断:指定 loginId 在指定 Client 请求指定 Scope 时,是否需要手动确认授权
|
||||
* @param loginId 账号id
|
||||
* @param clientId 应用id
|
||||
* @param scopes 权限
|
||||
* @return 是否已经授权
|
||||
*/
|
||||
public boolean isNeedCarefulConfirm(Object loginId, String clientId, List<String> scopes) {
|
||||
// 如果请求的权限为空,则不需要确认
|
||||
if(scopes == null || scopes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果包含高级权限,则必须手动确认授权
|
||||
List<String> higherScopeList = getHigherScopeList();
|
||||
if(SaFoxUtil.list1ContainList2AnyElement(scopes, higherScopeList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果包含低级权限,则先将低级权限剔除掉
|
||||
List<String> lowerScopeList = getLowerScopeList();
|
||||
scopes = SaFoxUtil.list1RemoveByList2(scopes, lowerScopeList);
|
||||
|
||||
// 如果剔除后的权限为空,则不需要确认
|
||||
if(scopes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据近期授权记录,判断是否需要确认
|
||||
return !isGrant(loginId, clientId, scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验:该Client是否签约了指定的Scope
|
||||
* @param clientId 应用id
|
||||
@ -362,6 +396,24 @@ public class SaOAuth2Template {
|
||||
SaOAuth2Manager.getDao().saveGrantScope(clientId, loginId, scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高级权限列表
|
||||
* @return /
|
||||
*/
|
||||
public List<String> getHigherScopeList() {
|
||||
String higherScope = SaOAuth2Manager.getConfig().getHigherScope();
|
||||
return SaOAuth2Manager.getDataConverter().convertScopeStringToList(higherScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取低级权限列表
|
||||
* @return /
|
||||
*/
|
||||
public List<String> getLowerScopeList() {
|
||||
String lowerScope = SaOAuth2Manager.getConfig().getLowerScope();
|
||||
return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user