新增 scope 等级划分,可指定哪些权限需要强制每次手动授权

This commit is contained in:
click33 2024-08-21 13:57:05 +08:00
parent 1bc59dc14c
commit 4aa4941598
16 changed files with 545 additions and 71 deletions

View File

@ -17,6 +17,7 @@ package cn.dev33.satoken.context.model;
import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaFoxUtil;
import java.util.List; import java.util.List;
@ -156,7 +157,25 @@ public interface SaRequest {
* @return / * @return /
*/ */
String getMethod(); 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 异步请求 * 判断此请求是否为 Ajax 异步请求
* @return / * @return /

View File

@ -678,4 +678,63 @@ public class SaFoxUtil {
return false; 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;
}
} }

View File

@ -57,7 +57,7 @@ public class SaOAuth2ServerController {
}; };
} }
// ---------- 开放相关资源接口 Client端根据 Access-Token 置换相关资源 ------------ // ---------- 开放相关资源接口 Client端根据 Access-Token 置换相关资源 ------------
// 获取 userinfo 信息昵称头像性别等等 // 获取 userinfo 信息昵称头像性别等等

View File

@ -15,6 +15,10 @@ sa-token:
enable-password: true enable-password: true
# 是否全局开启客户端模式 # 是否全局开启客户端模式
enable-client: true enable-client: true
# 定义哪些 scope 是高级权限,多个用逗号隔开
# higher-scope: openid,userid
# 定义哪些 scope 是低级权限,多个用逗号隔开
# lower-scope: userinfo
spring: spring:
# redis配置 # redis配置

View File

@ -33,20 +33,31 @@
console.log('-----------'); console.log('-----------');
$.ajax({ $.ajax({
url: '/oauth2/doConfirm', url: '/oauth2/doConfirm',
method: "POST",
data: { data: {
client_id: getParam('client_id'), 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', dataType: 'json',
success: function(res) { success: function(res) {
if(res.code == 200) { console.log('res', res);
if(res.code === 200) {
layer.msg('授权成功!'); layer.msg('授权成功!');
setTimeout(function() { setTimeout(function() {
location.reload(true); if (res.redirect_uri) {
location.href = res.redirect_uri;
} else {
location.reload();
}
}, 800); }, 800);
} else { } else {
// 重定向至授权失败URL // 重定向至授权失败URL
layer.alert('授权失败!'); layer.alert('授权失败:' + res.msg);
} }
}, },
error: function(e) { error: function(e) {

View File

@ -59,13 +59,14 @@
- [定制化登录页面与授权页面](/oauth2/oauth2-custom-login) - [定制化登录页面与授权页面](/oauth2/oauth2-custom-login)
- [自定义 API 路由 ](/oauth2/oauth2-custom-api) - [自定义 API 路由 ](/oauth2/oauth2-custom-api)
- [自定义 Scope 权限以处理器](/oauth2/oauth2-custom-scope-handler) - [自定义 Scope 权限以处理器](/oauth2/oauth2-custom-scope-handler)
- [为 Scope 划分等级](/oauth2/7) - [为 Scope 划分等级](/oauth2/oauth2-scope-level)
<!-- - [前后端分离模式整合方案](/oauth2/4) --> - [自定义 grant_type](/oauth2/oauth2-custom-grant_type)
- [平台中心模式开发](/oauth2/5)
- [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking) - [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking)
- [OAuth2 代码 API 参考](/oauth2/oauth2-dev) - [OAuth2 代码 API 参考](/oauth2/oauth2-dev)
- [常见问题说明](/oauth2/8) - [常见问题说明](/oauth2/8)
- <!-- jwt风格token、使用注解校验权限、OAuth2整个流程参考、state参数详解 --> <!-- - [前后端分离模式整合方案](/oauth2/4) -->
<!-- - [平台中心模式开发](/oauth2/5) -->
- <!-- jwt风格token、使用注解校验权限、state参数详解 -->
- **微服务** - **微服务**
- [分布式Session会话](/micro/dcs-session) - [分布式Session会话](/micro/dcs-session)

View File

@ -43,6 +43,82 @@ redirect_uri?code={code}&state={state}
4. 每次授权产生新 `Code` 码,会导致旧 `Code` 码立即作废,即使旧 `Code` 码尚未使用。 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 ### 1.2、根据授权码获取 Access-Token
获得 `Code` 码后,我们可以通过以下接口,获取到用户的 `Access-Token`、`Refresh-Token` 等信息。 获得 `Code` 码后,我们可以通过以下接口,获取到用户的 `Access-Token`、`Refresh-Token` 等信息。
@ -138,7 +214,7 @@ http://{host}:{port}/oauth2/revoke
### 1.5、根据 Access-Token 获取相应用户的账号信息 ### 1.5、根据 Access-Token 获取相应用户的账号信息
注:此接口为官方仓库模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数 注:此接口非 OAuth2 标准协议接口,为官方仓库 demo 模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数
``` url ``` url
http://{host}:{port}/oauth2/userinfo?access_token={access_token} http://{host}:{port}/oauth2/userinfo?access_token={access_token}

View File

@ -139,7 +139,7 @@ http://sa-oauth-server.com:8000/oauth2/authorize
#### 3、code 换 access_token #### 3、code 换 access_token
3、访问上述链接后,得到 `code` 授权码,然后我们拿着 `code``access_token` 访问上述链接后,得到 `code` 授权码,然后我们拿着 `code``access_token`
``` url ``` url
http://sa-oauth-server.com:8000/oauth2/token http://sa-oauth-server.com:8000/oauth2/token
?grant_type=authorization_code ?grant_type=authorization_code

View 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;
};
```

View File

@ -119,10 +119,16 @@ public class SaOAuth2ServerController {
// 配置:确认授权时返回的 view // 配置:确认授权时返回的 view
cfg.confirmView = (clientId, scopes) -> { cfg.confirmView = (clientId, scopes) -> {
String scopeStr = SaFoxUtil.convertListToString(scopes); String scopeStr = SaFoxUtil.convertListToString(scopes);
String msg = "<p>应用 " + clientId + " 请求授权:" + scopeStr + "</p>" String yesCode =
+ "<p>请确认:<a href='/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "' target='_blank'> 确认授权 </a></p>" "fetch('/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "', {method: 'POST'})" +
+ "<p>确认之后刷新页面</p>"; ".then(res => res.json())" +
return msg; ".then(res => location.reload())";
String res = "<p>应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?</p>"
+ "<p>" +
" <button onclick=\"" + yesCode + "\">同意</button>" +
" <button onclick='history.back()'>拒绝</button>" +
"</p>";
return res;
}; };
} }

View File

@ -413,13 +413,14 @@ body {
background-color: #f4fdef; background-color: #f4fdef;
overflow: hidden; overflow: hidden;
max-height: 44px; max-height: 44px;
margin-bottom: 1em;
/* transition: all 1s; */ /* 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 { @keyframes slideDown {
0% { max-height: 44px; overflow: hidden; } 0% { max-height: 44px; overflow: hidden; }
99% { max-height: 1000px; overflow: hidden; } 99% { max-height: 1500px; overflow: hidden; }
100% { max-height: 1000px; overflow: auto; } 100% { max-height: 1500px; overflow: auto; }
} }
.main-box details summary{ .main-box details summary{
padding: 11px 14px; padding: 11px 14px;
@ -429,8 +430,9 @@ body {
} }
.main-box details pre{ .main-box details pre{
margin-left: 1em; 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;} .main-box details p{padding: 0 14px;}
/* 广告盒子 */ /* 广告盒子 */

View File

@ -66,7 +66,11 @@ public class SaOAuth2Config implements Serializable {
/** 默认 openid 生成算法中使用的摘要前缀 */ /** 默认 openid 生成算法中使用的摘要前缀 */
public String openidDigestPrefix = SaOAuth2Consts.OPENID_DEFAULT_DIGEST_PREFIX; public String openidDigestPrefix = SaOAuth2Consts.OPENID_DEFAULT_DIGEST_PREFIX;
/** 指定高级权限,多个用逗号隔开 */
public String higherScope;
/** 指定低级权限,多个用逗号隔开 */
public String lowerScope;
/** /**
* @return enableCode * @return enableCode
@ -77,9 +81,11 @@ public class SaOAuth2Config implements Serializable {
/** /**
* @param enableCode 要设置的 enableCode * @param enableCode 要设置的 enableCode
* @return /
*/ */
public void setEnableCode(Boolean enableCode) { public SaOAuth2Config setEnableCode(Boolean enableCode) {
this.enableCode = enableCode; this.enableCode = enableCode;
return this;
} }
/** /**
@ -91,9 +97,11 @@ public class SaOAuth2Config implements Serializable {
/** /**
* @param enableImplicit 要设置的 enableImplicit * @param enableImplicit 要设置的 enableImplicit
* @return /
*/ */
public void setEnableImplicit(Boolean enableImplicit) { public SaOAuth2Config setEnableImplicit(Boolean enableImplicit) {
this.enableImplicit = enableImplicit; this.enableImplicit = enableImplicit;
return this;
} }
/** /**
@ -106,8 +114,9 @@ public class SaOAuth2Config implements Serializable {
/** /**
* @param enablePassword 要设置的 enablePassword * @param enablePassword 要设置的 enablePassword
*/ */
public void setEnablePassword(Boolean enablePassword) { public SaOAuth2Config setEnablePassword(Boolean enablePassword) {
this.enablePassword = enablePassword; this.enablePassword = enablePassword;
return this;
} }
/** /**
@ -119,9 +128,11 @@ public class SaOAuth2Config implements Serializable {
/** /**
* @param enableClient 要设置的 enableClient * @param enableClient 要设置的 enableClient
* @return /
*/ */
public void setEnableClient(Boolean enableClient) { public SaOAuth2Config setEnableClient(Boolean enableClient) {
this.enableClient = enableClient; this.enableClient = enableClient;
return this;
} }
/** /**
@ -133,9 +144,11 @@ public class SaOAuth2Config implements Serializable {
/** /**
* @param isNewRefresh 要设置的 isNewRefresh * @param isNewRefresh 要设置的 isNewRefresh
* @return /
*/ */
public void setIsNewRefresh(Boolean isNewRefresh) { public SaOAuth2Config setIsNewRefresh(Boolean isNewRefresh) {
this.isNewRefresh = isNewRefresh; this.isNewRefresh = isNewRefresh;
return this;
} }
/** /**
@ -229,13 +242,53 @@ public class SaOAuth2Config implements Serializable {
* @param openidDigestPrefix 要设置的 openidDigestPrefix * @param openidDigestPrefix 要设置的 openidDigestPrefix
* @return 对象自身 * @return 对象自身
*/ */
public SaOAuth2Config setOpenidMd5Prefix(String openidDigestPrefix) { public SaOAuth2Config setOpenidDigestPrefix(String openidDigestPrefix) {
this.openidDigestPrefix = openidDigestPrefix; this.openidDigestPrefix = openidDigestPrefix;
return this; return this;
} }
/**
// -------------------- SaOAuth2Handle 所有回调函数 -------------------- * 获取 指定高级权限多个用逗号隔开
*
* @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 所有回调函数 --------------------
/** /**
* OAuth-Server端未登录时返回的View * OAuth-Server端未登录时返回的View
@ -254,19 +307,20 @@ public class SaOAuth2Config implements Serializable {
@Override @Override
public String toString() { public String toString() {
return "SaOAuth2Config [" + return "SaOAuth2Config{" +
"enableCode=" + enableCode "enableCode=" + enableCode +
+ ", enableImplicit=" + enableImplicit ", enableImplicit=" + enableImplicit +
+ ", enablePassword=" + enablePassword ", enablePassword=" + enablePassword +
+ ", enableClient=" + enableClient ", enableClient=" + enableClient +
+ ", isNewRefresh=" + isNewRefresh ", isNewRefresh=" + isNewRefresh +
+ ", codeTimeout=" + codeTimeout ", codeTimeout=" + codeTimeout +
+ ", accessTokenTimeout=" + accessTokenTimeout ", accessTokenTimeout=" + accessTokenTimeout +
+ ", refreshTokenTimeout=" + refreshTokenTimeout ", refreshTokenTimeout=" + refreshTokenTimeout +
+ ", clientTokenTimeout=" + clientTokenTimeout ", clientTokenTimeout=" + clientTokenTimeout +
+ ", pastClientTokenTimeout=" + pastClientTokenTimeout ", pastClientTokenTimeout=" + pastClientTokenTimeout +
+ ", openidDigestPrefix=" + openidDigestPrefix ", openidDigestPrefix='" + openidDigestPrefix +
+"]"; ", higherScope='" + higherScope +
", lowerScope='" + lowerScope +
'}';
} }
} }

View File

@ -57,6 +57,7 @@ public class SaOAuth2Consts {
public static String password = "password"; public static String password = "password";
public static String name = "name"; public static String name = "name";
public static String pwd = "pwd"; public static String pwd = "pwd";
public static String build_redirect_uri = "build_redirect_uri";
} }
/** /**

View File

@ -112,5 +112,8 @@ public interface SaOAuth2ErrorCode {
/** 暂未开放凭证式模式 */ /** 暂未开放凭证式模式 */
int CODE_30134 = 30134; int CODE_30134 = 30134;
/** 无效的请求 Method */
int CODE_30141 = 30141;
} }

View File

@ -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.error.SaOAuth2ErrorCode;
import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;
import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import cn.dev33.satoken.oauth2.template.SaOAuth2Template;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult; import cn.dev33.satoken.util.SaResult;
@ -124,28 +125,7 @@ public class SaOAuth2ServerProcessor {
String responseType = req.getParamNotNull(Param.response_type); String responseType = req.getParamNotNull(Param.response_type);
// 1先判断是否开启了指定的授权模式 // 1先判断是否开启了指定的授权模式
// 模式一Code授权码 checkAuthorizeResponseType(responseType, req, cfg);
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);
}
// 2如果尚未登录, 则先去登录 // 2如果尚未登录, 则先去登录
if( ! getStpLogic().isLogin()) { if( ! getStpLogic().isLogin()) {
@ -162,8 +142,8 @@ public class SaOAuth2ServerProcessor {
oauth2Template.checkContract(ra.clientId, ra.scopes); oauth2Template.checkContract(ra.clientId, ra.scopes);
// 6判断如果此次申请的Scope该用户尚未授权则转到授权页面 // 6判断如果此次申请的Scope该用户尚未授权则转到授权页面
boolean isGrant = oauth2Template.isGrant(ra.loginId, ra.clientId, ra.scopes); boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);
if( ! isGrant) { if(isNeedCarefulConfirm) {
return cfg.confirmView.apply(ra.clientId, ra.scopes); return cfg.confirmView.apply(ra.clientId, ra.scopes);
} }
@ -306,7 +286,7 @@ public class SaOAuth2ServerProcessor {
SaRequest req = SaHolder.getRequest(); SaRequest req = SaHolder.getRequest();
SaOAuth2Config cfg = SaOAuth2Manager.getConfig(); 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,13 +296,51 @@ public class SaOAuth2ServerProcessor {
public Object doConfirm() { public Object doConfirm() {
// 获取变量 // 获取变量
SaRequest req = SaHolder.getRequest(); SaRequest req = SaHolder.getRequest();
String clientId = req.getParamNotNull(Param.client_id); String clientId = req.getParamNotNull(Param.client_id);
Object loginId = getStpLogic().getLoginId();
String scope = req.getParamNotNull(Param.scope); String scope = req.getParamNotNull(Param.scope);
List<String> scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(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); oauth2Template.saveGrantScope(clientId, loginId, scopes);
return SaResult.ok();
// 判断所需的返回结果模式
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);
} }
/** /**
@ -408,6 +426,9 @@ public class SaOAuth2ServerProcessor {
return SaOAuth2Manager.getDataResolver().buildClientTokenReturnValue(ct); return SaOAuth2Manager.getDataResolver().buildClientTokenReturnValue(ct);
} }
// ----------- 代码块封装 --------------
/** /**
* 根据当前请求提交的 client_id 参数获取 SaClientModel 对象 * 根据当前请求提交的 client_id 参数获取 SaClientModel 对象
* @return / * @return /
@ -417,6 +438,34 @@ public class SaOAuth2ServerProcessor {
return oauth2Template.checkClientModel(clientIdAndSecret.clientId); 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);
}
}
/** /**
* 获取底层使用的会话对象 * 获取底层使用的会话对象
* *

View File

@ -123,6 +123,7 @@ public class SaOAuth2Template {
// ------------------- check 数据校验 // ------------------- check 数据校验
/** /**
* 判断指定 loginId 是否对一个 Client 授权给了指定 Scope * 判断指定 loginId 是否对一个 Client 授权给了指定 Scope
* @param loginId 账号id * @param loginId 账号id
@ -135,6 +136,39 @@ public class SaOAuth2Template {
List<String> grantScopeList = dao.getGrantScope(clientId, loginId); List<String> grantScopeList = dao.getGrantScope(clientId, loginId);
return scopes.isEmpty() || new HashSet<>(grantScopeList).containsAll(scopes); 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 * 校验该Client是否签约了指定的Scope
* @param clientId 应用id * @param clientId 应用id
@ -362,6 +396,24 @@ public class SaOAuth2Template {
SaOAuth2Manager.getDao().saveGrantScope(clientId, loginId, scopes); 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);
}