diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java index 7c0c0fa9..8a0b3d45 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java @@ -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; @@ -156,7 +157,25 @@ public interface SaRequest { * @return / */ 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 / diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java index 530e1fbd..4994bde0 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java @@ -678,4 +678,63 @@ public class SaFoxUtil { return false; } + /** + * list1 是否完全包含 list2 中所有元素 + * @param list1 集合1 + * @param list2 集合2 + * @return / + */ + public static boolean list1ContainList2AllElement(List list1, List 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 list1, List 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 list1RemoveByList2(List list1, List list2){ + if(list1 == null) { + return null; + } + if(list1.isEmpty() || list2 == null || list2.isEmpty()) { + return new ArrayList<>(list1); + } + List listX = new ArrayList<>(list1); + for (String str : list2) { + listX.remove(str); + } + return listX; + } + } diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java index 58d75726..37261fd0 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java @@ -57,7 +57,7 @@ public class SaOAuth2ServerController { }; } - + // ---------- 开放相关资源接口: Client端根据 Access-Token ,置换相关资源 ------------ // 获取 userinfo 信息:昵称、头像、性别等等 diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml index 44f39cd0..073f9d06 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml @@ -15,6 +15,10 @@ sa-token: enable-password: true # 是否全局开启客户端模式 enable-client: true + # 定义哪些 scope 是高级权限,多个用逗号隔开 + # higher-scope: openid,userid + # 定义哪些 scope 是低级权限,多个用逗号隔开 + # lower-scope: userinfo spring: # redis配置 diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html index 7ede1714..cfeaf099 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html @@ -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) { diff --git a/sa-token-doc/_sidebar.md b/sa-token-doc/_sidebar.md index f742c13f..cdd29799 100644 --- a/sa-token-doc/_sidebar.md +++ b/sa-token-doc/_sidebar.md @@ -59,13 +59,14 @@ - [定制化登录页面与授权页面](/oauth2/oauth2-custom-login) - [自定义 API 路由 ](/oauth2/oauth2-custom-api) - [自定义 Scope 权限以处理器](/oauth2/oauth2-custom-scope-handler) - - [为 Scope 划分等级](/oauth2/7) - - - [平台中心模式开发](/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) - - + + + - - **微服务** - [分布式Session会话](/micro/dcs-session) diff --git a/sa-token-doc/oauth2/oauth2-apidoc.md b/sa-token-doc/oauth2/oauth2-apidoc.md index eedcdf04..a1b7b10f 100644 --- a/sa-token-doc/oauth2/oauth2-apidoc.md +++ b/sa-token-doc/oauth2/oauth2-apidoc.md @@ -43,6 +43,82 @@ redirect_uri?code={code}&state={state} 4. 每次授权产生新 `Code` 码,会导致旧 `Code` 码立即作废,即使旧 `Code` 码尚未使用。 +
+RestAPI 登录接口:/oauth2/doLogin + +如果用户在 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")` 来获取前端提交的其它参数。 + +
+ + +
+RestAPI 确认授权接口:/oauth2/doConfirm + +如果 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` 接口。 + +
+ + + ### 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} diff --git a/sa-token-doc/oauth2/oauth2-custom-scope-handler.md b/sa-token-doc/oauth2/oauth2-custom-scope-handler.md index 17e9548e..58207223 100644 --- a/sa-token-doc/oauth2/oauth2-custom-scope-handler.md +++ b/sa-token-doc/oauth2/oauth2-custom-scope-handler.md @@ -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 diff --git a/sa-token-doc/oauth2/oauth2-scope-level.md b/sa-token-doc/oauth2/oauth2-scope-level.md new file mode 100644 index 00000000..161065e7 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-scope-level.md @@ -0,0 +1,137 @@ +# OAuth2 - 为 Scope 划分等级 + + +### 1、划分等级 + +我们可以通过配置文件来为 scope 划分等级 + + + +``` yaml +# sa-token 配置 +sa-token: + # OAuth2.0 配置 + oauth2: + # 定义哪些 scope 是高级权限,多个用逗号隔开 + higher-scope: openid,userid + # 定义哪些 scope 是低级权限,多个用逗号隔开 + lower-scope: userinfo +``` + +``` properties +# 定义哪些 scope 是高级权限,多个用逗号隔开 +sa-token.oauth2.higher-scope=openid,userid +# 定义哪些 scope 是低级权限,多个用逗号隔开 +sa-token.oauth2.lower-scope=userinfo +``` + + +如上所示: +- 通过 `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 = "

应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?

" + + "

" + + " " + + " " + + "

"; + return res; +}; +``` \ No newline at end of file diff --git a/sa-token-doc/oauth2/oauth2-server.md b/sa-token-doc/oauth2/oauth2-server.md index ad4a1724..de684040 100644 --- a/sa-token-doc/oauth2/oauth2-server.md +++ b/sa-token-doc/oauth2/oauth2-server.md @@ -119,10 +119,16 @@ public class SaOAuth2ServerController { // 配置:确认授权时返回的 view cfg.confirmView = (clientId, scopes) -> { String scopeStr = SaFoxUtil.convertListToString(scopes); - String msg = "

应用 " + clientId + " 请求授权:" + scopeStr + "

" - + "

请确认: 确认授权

" - + "

确认之后刷新页面

"; - return msg; + String yesCode = + "fetch('/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "', {method: 'POST'})" + + ".then(res => res.json())" + + ".then(res => location.reload())"; + String res = "

应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?

" + + "

" + + " " + + " " + + "

"; + return res; }; } diff --git a/sa-token-doc/static/doc.css b/sa-token-doc/static/doc.css index 7806a12d..fe7b8c93 100644 --- a/sa-token-doc/static/doc.css +++ b/sa-token-doc/static/doc.css @@ -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;} /* 广告盒子 */ diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java index 41cab136..0bb39073 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java @@ -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,13 +242,53 @@ public class SaOAuth2Config implements Serializable { * @param openidDigestPrefix 要设置的 openidDigestPrefix * @return 对象自身 */ - public SaOAuth2Config setOpenidMd5Prefix(String openidDigestPrefix) { + public SaOAuth2Config setOpenidDigestPrefix(String openidDigestPrefix) { this.openidDigestPrefix = openidDigestPrefix; 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 @@ -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 + + '}'; } - } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java index 9311a0a3..1ecd5425 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java @@ -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"; } /** diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java index fa15e0d4..b6f3cbd7 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java @@ -112,5 +112,8 @@ public interface SaOAuth2ErrorCode { /** 暂未开放凭证式模式 */ int CODE_30134 = 30134; - + + /** 无效的请求 Method */ + int CODE_30141 = 30141; + } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java index 55857340..e5d3af01 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java @@ -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,13 +296,51 @@ 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 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); - 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); } + + // ----------- 代码块封装 -------------- + /** * 根据当前请求提交的 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); + } + } + /** * 获取底层使用的会话对象 * diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java index 73422fae..3062f525 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java @@ -123,6 +123,7 @@ public class SaOAuth2Template { // ------------------- check 数据校验 + /** * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope * @param loginId 账号id @@ -135,6 +136,39 @@ public class SaOAuth2Template { List 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 scopes) { + // 如果请求的权限为空,则不需要确认 + if(scopes == null || scopes.isEmpty()) { + return false; + } + + // 如果包含高级权限,则必须手动确认授权 + List higherScopeList = getHigherScopeList(); + if(SaFoxUtil.list1ContainList2AnyElement(scopes, higherScopeList)) { + return true; + } + + // 如果包含低级权限,则先将低级权限剔除掉 + List 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 getHigherScopeList() { + String higherScope = SaOAuth2Manager.getConfig().getHigherScope(); + return SaOAuth2Manager.getDataConverter().convertScopeStringToList(higherScope); + } + + /** + * 获取低级权限列表 + * @return / + */ + public List getLowerScopeList() { + String lowerScope = SaOAuth2Manager.getConfig().getLowerScope(); + return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope); + } +