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 8044a223..63bfbb60 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 @@ -106,14 +106,35 @@ public class SaFoxUtil { /** * 指定数组是否为null或者空数组 + *

该方法已过时,建议使用 isEmptyArray 方法

* @param / * @param array / * @return / */ + @Deprecated public static boolean isEmpty(T[] array) { + return isEmptyArray(array); + } + + /** + * 指定数组是否为null或者空数组 + * @param / + * @param array / + * @return / + */ + public static boolean isEmptyArray(T[] array) { return array == null || array.length == 0; } + /** + * 指定集合是否为null或者空数组 + * @param list / + * @return / + */ + public static boolean isEmptyList(List list) { + return list == null || list.isEmpty(); + } + /** * 比较两个对象是否相等 * @param a 第一个对象 @@ -626,6 +647,15 @@ public class SaFoxUtil { return new ArrayList<>(Arrays.asList(str)); } + /** + * String 集合转数组 + * @param list 集合 + * @return 数组 + */ + public static String[] toArray(List list) { + return list.toArray(new String[0]); + } + public static List logLevelList = Arrays.asList("", "trace", "debug", "info", "warn", "error", "fatal"); /** 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 391a22d6..94a95446 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 @@ -75,10 +75,10 @@ public class SaOAuth2ServerController { String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken); System.out.println("-------- 此Access-Token对应的账号id: " + loginId); - + // 校验 Access-Token 是否具有权限: userinfo SaOAuth2Util.checkScope(accessToken, "userinfo"); - + // 模拟账号信息 (真实环境需要查询数据库获取信息) Map map = new LinkedHashMap<>(); // map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client diff --git a/sa-token-doc/oauth2/oauth2-check-domain.md b/sa-token-doc/oauth2/oauth2-check-domain.md index a6e8faf6..841d3ff0 100644 --- a/sa-token-doc/oauth2/oauth2-check-domain.md +++ b/sa-token-doc/oauth2/oauth2-check-domain.md @@ -94,7 +94,7 @@ URL 没有通过校验,拒绝授权! - 反例:`http://*.sa-oauth-client.com/` *详见源码: [SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) -`checkAllowUrlListStaticMethod` 方法。* +`checkRedirectUriListNormalStaticMethod` 方法。* 参考:[github/issue/529](https://github.com/dromara/Sa-Token/issues/529) 感谢这位 `@m4ra7h0n` 用户反馈的漏洞。 diff --git a/sa-token-doc/oauth2/oauth2-dev.md b/sa-token-doc/oauth2/oauth2-dev.md index def72b9b..45f7097a 100644 --- a/sa-token-doc/oauth2/oauth2-dev.md +++ b/sa-token-doc/oauth2/oauth2-dev.md @@ -1,49 +1,118 @@ # Sa-Token-OAuth2 Server端 二次开发用到的所有函数说明 -官方示例只提供了基本的授权流程,以及userinfo资源的开放,如果您需要开放更多的接口,则二次开发时用到以下相关API方法 +官方示例只提供了基本的授权流程,以及 userinfo 资源的开放,如果您需要开放更多的接口,则二次开发时可能用到以下相关 API 方法 --- -## Sa-OAuth2 模块常用方法 +### Client 信息相关 ``` java -// 根据 id 获取 Client 信息, 如果 Client 为空,则抛出异常 +// 获取 ClientModel,根据 clientId +SaOAuth2Util.getClientModel(clientId); + +// 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 SaOAuth2Util.checkClientModel(clientId); -// 获取 Access-Token,如果Access-Token为空则抛出异常 +// 校验:clientId 与 clientSecret 是否正确 +SaOAuth2Util.checkClientSecret(clientId, clientSecret); + +// 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes +SaOAuth2Util.checkClientSecretAndScope(clientId, clientSecret, scopes); + +// 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false +SaOAuth2Util.isContractScope(clientId, scopes); + +// 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 +SaOAuth2Util.checkContractScope(clientId, scopes); + +// 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 +SaOAuth2Util.checkContractScope(clientModel, scopes); + +// 校验:该 Client 使用指定 url 作为回调地址,是否合法 +SaOAuth2Util.checkRedirectUri(clientId, url); + +// 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope +SaOAuth2Util.isGrantScope(loginId, clientId, scopes); +``` + + +### Access-Token 相关 +``` java +// 获取 AccessTokenModel,无效的 AccessToken 会返回 null +SaOAuth2Util.getAccessToken(accessToken); + +// 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 SaOAuth2Util.checkAccessToken(accessToken); -// 获取 Client-Token,如果Client-Token为空则抛出异常 -SaOAuth2Util.checkClientToken(clientToken); +// 获取 Access-Token,根据索引: clientId、loginId +SaOAuth2Util.getAccessTokenValue(clientId, loginId); + +// 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false +SaOAuth2Util.hasAccessTokenScope(accessToken, ...scopes); + +// 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 +SaOAuth2Util.checkAccessTokenScope(accessToken, ...scopes); // 获取 Access-Token 所代表的LoginId SaOAuth2Util.getLoginIdByAccessToken(accessToken); -// 校验:指定 Access-Token 是否具有指定 Scope -SaOAuth2Util.checkScope(accessToken, scopes); +// 获取 Access-Token 所代表的 clientId +SaOAuth2Util.getClientIdByAccessToken(accessToken); -// 根据 code码 生成 Access-Token -SaOAuth2Util.generateAccessToken(code); - -// 根据 Refresh-Token 生成一个新的 Access-Token -SaOAuth2Util.refreshAccessToken(refreshToken); - -// 构建 Client-Token -SaOAuth2Util.generateClientToken(clientId, scope); - -// 校验 Client-Token 是否含有指定 Scope -SaOAuth2Util.checkClientTokenScope(clientToken, scopes); - -// 回收 Access-Token +// 回收 Access-Token SaOAuth2Util.revokeAccessToken(accessToken); -// 持久化:用户授权记录 -SaOAuth2Util.saveGrantScope(clientId, loginId, scope); - -// 获取:Refresh-Token Model -SaOAuth2Util.getRefreshToken(refreshToken); +// 回收 Access-Token,根据索引: clientId、loginId +SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId); ``` -详情请参考源码:[码云:SaOAuth2Util.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java) + +### Refresh-Token 相关 +``` java +// 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null +SaOAuth2Util.getRefreshToken(refreshToken); + +// 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 +SaOAuth2Util.checkRefreshToken(refreshToken); + +// 获取 Refresh-Token,根据索引: clientId、loginId +SaOAuth2Util.getRefreshTokenValue(clientId, Object loginId); + +// 根据 RefreshToken 刷新出一个 AccessToken +SaOAuth2Util.refreshAccessToken(refreshToken); +``` + + +### Client-Token 相关 + +``` java +// 获取 ClientTokenModel,无效的 ClientToken 会返回 null +SaOAuth2Util.getClientToken(clientToken); + +// 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 +SaOAuth2Util.checkClientToken(clientToken); + +// 获取 ClientToken,根据索引: clientId +SaOAuth2Util.getClientTokenValue(clientId); + +// 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false +SaOAuth2Util.hasClientTokenScope(clientToken, ...scopes); + +// 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 +SaOAuth2Util.checkClientTokenScope(clientToken, ...scopes); + +// 回收 ClientToken +SaOAuth2Util.revokeClientToken(clientToken); + +// 回收 ClientToken,根据索引: clientId +SaOAuth2Util.revokeClientTokenByIndex(clientId); + +// 回收 PastToken,根据索引: clientId +SaOAuth2Util.revokePastTokenByIndex(clientId); +``` + +--- + +详情请参考源码:[码云:SaOAuth2Util.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java) diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java index 197dfde7..aa9fe197 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java @@ -85,12 +85,6 @@ public interface SaOAuth2DataGenerate { */ String buildImplicitRedirectUri(String redirectUri, String token, String state); - /** - * 回收 Access-Token - * @param accessToken Access-Token值 - */ - void revokeAccessToken(String accessToken); - /** * 检查 state 是否被重复使用 * @param state / diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java index 266342bf..33ad2055 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java @@ -126,7 +126,7 @@ public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate { // 删除旧 Refresh-Token dao.deleteRefreshToken(rt.refreshToken); - // 创建并保持新的 Refresh-Token + // 创建并保存新的 Refresh-Token rt = SaOAuth2Manager.getDataConverter().convertRefreshTokenToRefreshToken(rt); dao.saveRefreshToken(rt); dao.saveRefreshTokenIndex(rt); @@ -268,31 +268,6 @@ public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate { return url; } - /** - * 回收 Access-Token - * @param accessToken Access-Token值 - */ - @Override - public void revokeAccessToken(String accessToken) { - - SaOAuth2Dao dao = SaOAuth2Manager.getDao(); - - // 如果查不到任何东西, 直接返回 - AccessTokenModel at = dao.getAccessToken(accessToken); - if(at == null) { - return; - } - - // 删除 Access-Token - dao.deleteAccessToken(accessToken); - dao.deleteAccessTokenIndex(at.clientId, at.loginId); - - // 删除对应的 Refresh-Token - String refreshToken = dao.getRefreshTokenValue(at.clientId, at.loginId); - dao.deleteRefreshToken(refreshToken); - dao.deleteRefreshTokenIndex(at.clientId, at.loginId); - } - /** * 检查 state 是否被重复使用 * @param state / diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java index c8f93ef6..aa8815a7 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java @@ -17,7 +17,8 @@ package cn.dev33.satoken.oauth2.data.loader; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; -import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2ClientModelException; import cn.dev33.satoken.secure.SaSecureUtil; /** @@ -47,7 +48,9 @@ public interface SaOAuth2DataLoader { default SaClientModel getClientModelNotNull(String clientId) { SaClientModel clientModel = getClientModel(clientId); if(clientModel == null) { - throw new SaOAuth2Exception("未找到对应的 Client 信息"); + throw new SaOAuth2ClientModelException("无效 client_id: " + clientId) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30105); } return clientModel; } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java new file mode 100644 index 00000000..89b6ac65 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.oauth2.exception; + +/** + * 一个异常:代表 Access-Token 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2AccessTokenException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Access-Token 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2AccessTokenException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Access-Token 相关错误 + * @param message 异常描述 + */ + public SaOAuth2AccessTokenException(String message) { + super(message); + } + + /** + * 具体引起异常的 Access-Token 值 + */ + public String accessToken; + + public String getAccessToken() { + return accessToken; + } + + public SaOAuth2AccessTokenException setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2AccessTokenException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java new file mode 100644 index 00000000..cecc4141 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.oauth2.exception; + +/** + * 一个异常:代表 Access-Token Scope 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2AccessTokenScopeException extends SaOAuth2AccessTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Access-Token Scope 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2AccessTokenScopeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Access-Token Scope 相关错误 + * @param message 异常描述 + */ + public SaOAuth2AccessTokenScopeException(String message) { + super(message); + } + + /** + * 具体引起异常的 Access-Token 值 + */ + public String accessToken; + + /** + * 具体引起异常的 scope 值 + */ + public String scope; + + public String getAccessToken() { + return accessToken; + } + + public SaOAuth2AccessTokenScopeException setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public String getScope() { + return scope; + } + + public SaOAuth2AccessTokenScopeException setScope(String scope) { + this.scope = scope; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2AccessTokenScopeException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java new file mode 100644 index 00000000..59c36238 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.oauth2.exception; + +/** + * 一个异常:代表 ClientModel 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientModelException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 ClientModel 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientModelException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 ClientModel 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientModelException(String message) { + super(message); + } + + /** + * 具体引起异常的 ClientId 值 + */ + public String clientId; + + public String getClientId() { + return clientId; + } + + public SaOAuth2ClientModelException setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientModelException(message).setCode(code); + } + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param clientId 应用id + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, String clientId, int code) { + if(flag) { + throw new SaOAuth2ClientModelException(message).setClientId(clientId).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java new file mode 100644 index 00000000..ad1b2507 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.oauth2.exception; + +/** + * 一个异常:代表 ClientModel Scope 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientModelScopeException extends SaOAuth2ClientModelException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 ClientModel Scope 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientModelScopeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 ClientModel Scope 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientModelScopeException(String message) { + super(message); + } + + /** + * 具体引起异常的 ClientId 值 + */ + public String clientId; + + /** + * 具体引起异常的 scope 值 + */ + public String scope; + + public String getClientId() { + return clientId; + } + + public SaOAuth2ClientModelScopeException setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public String getScope() { + return scope; + } + + public SaOAuth2ClientModelScopeException setScope(String scope) { + this.scope = scope; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientModelScopeException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java new file mode 100644 index 00000000..2d69dbb8 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.oauth2.exception; + +/** + * 一个异常:代表 Client-Token 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientTokenException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Client-Token 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientTokenException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Client-Token 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientTokenException(String message) { + super(message); + } + + /** + * 具体引起异常的 Client-Token 值 + */ + public String clientToken; + + public String getClientToken() { + return clientToken; + } + + public SaOAuth2ClientTokenException setClientToken(String clientToken) { + this.clientToken = clientToken; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientTokenException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java new file mode 100644 index 00000000..987dea68 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.oauth2.exception; + +/** + * 一个异常:代表 Client-Token Scope 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientTokenScopeException extends SaOAuth2ClientTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Client-Token Scope 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientTokenScopeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Client-Token Scope 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientTokenScopeException(String message) { + super(message); + } + + /** + * 具体引起异常的 Client-Token 值 + */ + public String clientToken; + + /** + * 具体引起异常的 scope 值 + */ + public String scope; + + public String getClientToken() { + return clientToken; + } + + public SaOAuth2ClientTokenScopeException setClientToken(String clientToken) { + this.clientToken = clientToken; + return this; + } + + public String getScope() { + return scope; + } + + public SaOAuth2ClientTokenScopeException setScope(String scope) { + this.scope = scope; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientTokenScopeException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java index c1b96496..04b7d3cc 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java @@ -18,7 +18,7 @@ package cn.dev33.satoken.oauth2.exception; import cn.dev33.satoken.exception.SaTokenException; /** - * 一个异常:代表OAuth2认证流程错误 + * 一个异常:代表 OAuth2 认证流程错误 * * @author click33 * @since 1.33.0 @@ -31,7 +31,7 @@ public class SaOAuth2Exception extends SaTokenException { private static final long serialVersionUID = 6806129545290130114L; /** - * 一个异常:代表OAuth2认证流程错误 + * 一个异常:代表 OAuth2 认证流程错误 * @param cause 根异常原因 */ public SaOAuth2Exception(Throwable cause) { @@ -39,7 +39,7 @@ public class SaOAuth2Exception extends SaTokenException { } /** - * 一个异常:代表OAuth2认证流程错误 + * 一个异常:代表 OAuth2 认证流程错误 * @param message 异常描述 */ public SaOAuth2Exception(String message) { @@ -47,7 +47,7 @@ public class SaOAuth2Exception extends SaTokenException { } /** - * 如果flag==true,则抛出message异常 + * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java new file mode 100644 index 00000000..e110a31e --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.oauth2.exception; + +/** + * 一个异常:代表 Refresh-Token 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2RefreshTokenException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Refresh-Token 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2RefreshTokenException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Refresh-Token 相关错误 + * @param message 异常描述 + */ + public SaOAuth2RefreshTokenException(String message) { + super(message); + } + + /** + * 具体引起异常的 Refresh-Token 值 + */ + public String refreshToken; + + public String getRefreshToken() { + return refreshToken; + } + + public SaOAuth2RefreshTokenException setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2RefreshTokenException(message).setCode(code); + } + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param refreshToken refreshToken + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, String refreshToken, int code) { + if(flag) { + throw new SaOAuth2RefreshTokenException(message).setRefreshToken(refreshToken).setCode(code); + } + } + +} 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 552c46b4..f8e56867 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 @@ -37,6 +37,7 @@ import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import java.util.List; @@ -130,10 +131,10 @@ public class SaOAuth2ServerProcessor { RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, SaOAuth2Manager.getStpLogic().getLoginId()); // 4、校验:重定向域名是否合法 - oauth2Template.checkRightUrl(ra.clientId, ra.redirectUri); + oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri); // 5、校验:此次申请的Scope,该Client是否已经签约 - oauth2Template.checkContract(ra.clientId, ra.scopes); + oauth2Template.checkContractScope(ra.clientId, ra.scopes); // 6、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes); @@ -205,7 +206,7 @@ public class SaOAuth2ServerProcessor { oauth2Template.checkAccessTokenParam(clientId, clientSecret, accessToken); // 回收 Access-Token - SaOAuth2Manager.getDataGenerate().revokeAccessToken(accessToken); + oauth2Template.revokeAccessToken(accessToken); // 返回 return SaOAuth2Manager.getDataResolver().buildRevokeTokenReturnValue(); @@ -306,7 +307,7 @@ public class SaOAuth2ServerProcessor { List scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(Param.scope)); //校验 ClientScope - oauth2Template.checkContract(clientId, scopes); + oauth2Template.checkContractScope(clientId, scopes); // 校验 ClientSecret oauth2Template.checkClientSecret(clientId, clientSecret); 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 f9862b61..e35cef93 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 @@ -23,11 +23,10 @@ import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; -import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.exception.*; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; -import java.util.HashSet; import java.util.List; /** @@ -38,91 +37,206 @@ import java.util.List; */ public class SaOAuth2Template { - // ------------------- 数据加载 + // ----------------- ClientModel 相关 ----------------- /** - * 根据id获取Client信息 - * @param clientId 应用id - * @return ClientModel + * 获取 ClientModel,根据 clientId + * + * @param clientId / + * @return / */ public SaClientModel getClientModel(String clientId) { return SaOAuth2Manager.getDataLoader().getClientModel(clientId); } - // ------------------- 资源校验API /** - * 根据id获取Client信息, 如果Client为空,则抛出异常 - * @param clientId 应用id - * @return ClientModel + * 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 + * @param clientId / + * @return / */ public SaClientModel checkClientModel(String clientId) { SaClientModel clientModel = getClientModel(clientId); if(clientModel == null) { - throw new SaOAuth2Exception("无效client_id: " + clientId).setCode(SaOAuth2ErrorCode.CODE_30105); + throw new SaOAuth2ClientModelException("无效 client_id: " + clientId) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30105); } return clientModel; } + /** - * 获取 Access-Token,如果AccessToken为空则抛出异常 - * @param accessToken . - * @return . + * 校验:clientId 与 clientSecret 是否正确 + * @param clientId 应用id + * @param clientSecret 秘钥 + * @return SaClientModel对象 */ - public AccessTokenModel checkAccessToken(String accessToken) { - AccessTokenModel at = SaOAuth2Manager.getDao().getAccessToken(accessToken); - SaOAuth2Exception.throwBy(at == null, "无效access_token:" + accessToken, SaOAuth2ErrorCode.CODE_30106); - return at; + public SaClientModel checkClientSecret(String clientId, String clientSecret) { + SaClientModel cm = checkClientModel(clientId); + SaOAuth2ClientModelException.throwBy(cm.clientSecret == null || ! cm.clientSecret.equals(clientSecret), "无效client_secret: " + clientSecret, + clientId, SaOAuth2ErrorCode.CODE_30115); + return cm; } + /** - * 获取 Client-Token,如果ClientToken为空则抛出异常 - * @param clientToken . - * @return . + * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes + * @param clientId 应用id + * @param clientSecret 秘钥 + * @param scopes 权限 + * @return SaClientModel对象 */ - public ClientTokenModel checkClientToken(String clientToken) { - ClientTokenModel ct = SaOAuth2Manager.getDao().getClientToken(clientToken); - SaOAuth2Exception.throwBy(ct == null, "无效:client_token" + clientToken, SaOAuth2ErrorCode.CODE_30107); - return ct; + public SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List scopes) { + SaClientModel cm = checkClientSecret(clientId, clientSecret); + checkContractScope(cm, scopes); + return cm; } + /** - * 获取 Access-Token 所代表的LoginId - * @param accessToken Access-Token - * @return LoginId + * 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false + * @param clientId 应用id + * @param scopes 权限 + * @return / */ - public Object getLoginIdByAccessToken(String accessToken) { - return checkAccessToken(accessToken).loginId; - } - /** - * 校验:指定 Access-Token 是否具有指定 Scope - * @param accessToken Access-Token - * @param scopes 需要校验的权限列表 - */ - public void checkScope(String accessToken, String... scopes) { - if(scopes == null || scopes.length == 0) { - return; - } - AccessTokenModel at = checkAccessToken(accessToken); - List scopeList = at.scopes; - for (String scope : scopes) { - SaOAuth2Exception.throwBy( ! scopeList.contains(scope), "该 Access-Token 不具备 Scope:" + scope, SaOAuth2ErrorCode.CODE_30108); - } - } - /** - * 校验:指定 Client-Token 是否具有指定 Scope - * @param clientToken Client-Token - * @param scopes 需要校验的权限列表 - */ - public void checkClientTokenScope(String clientToken, String... scopes) { - if(scopes == null || scopes.length == 0) { - return; - } - ClientTokenModel ct = checkClientToken(clientToken); - List scopeList = ct.scopes; - for (String scope : scopes) { - SaOAuth2Exception.throwBy( ! scopeList.contains(scope), "该 Client-Token 不具备 Scope:" + scope, SaOAuth2ErrorCode.CODE_30109); + public boolean isContractScope(String clientId, List scopes) { + try { + checkContractScope(clientId, scopes); + return true; + } catch (SaOAuth2ClientModelException e) { + return false; } } + /** + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param clientId 应用id + * @param scopes 权限列表 + * @return / + */ + public SaClientModel checkContractScope(String clientId, List scopes) { + return checkContractScope(checkClientModel(clientId), scopes); + } - // ------------------- check 数据校验 + /** + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param cm 应用 + * @param scopes 权限列表 + * @return / + */ + public SaClientModel checkContractScope(SaClientModel cm, List scopes) { + if(SaFoxUtil.isEmptyList(scopes)) { + return cm; + } + for (String scope : scopes) { + if(! cm.contractScopes.contains(scope)) { + throw new SaOAuth2ClientModelScopeException("该 client 暂未签约 scope: " + scope) + .setClientId(cm.clientId) + .setScope(scope) + .setCode(SaOAuth2ErrorCode.CODE_30112); + } + } + return cm; + } + + // --------- redirect_uri 相关 + + /** + * 校验:该 Client 使用指定 url 作为回调地址,是否合法 + * @param clientId 应用id + * @param url 指定url + */ + public void checkRedirectUri(String clientId, String url) { + // 1、是否是一个有效的url + if( ! SaFoxUtil.isUrl(url)) { + throw new SaOAuth2ClientModelException("无效redirect_url:" + url) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30113); + } + + // 2、截取掉?后面的部分 + int qIndex = url.indexOf("?"); + if(qIndex != -1) { + url = url.substring(0, qIndex); + } + + // 3、不允许出现@字符 + if(url.contains("@")) { + // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect_url 参数绕过 AllowUrl 列表的校验 + // + // 举个例子 SaClientModel 配置: + // allow-url=http://sa-oauth-client.com* + // + // 开发者原意是为了允许 sa-oauth-client.com 下的所有地址都可以下放 code + // + // 但是如果攻击者精心构建一个url: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com@sa-token.cc + // + // 那么这个url就会绕过 allow-url 的校验,code 被下发到了第三方服务器地址: + // http://sa-token.cc/?code=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul + // + // 造成了 code 参数劫持 + // 所以此处需要禁止在 url 中出现 @ 字符 + // + // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com/@getInfo + // + // 但是为了安全起见,这么做还是有必要的 + throw new SaOAuth2ClientModelException("无效 redirect_url(不允许出现@字符):" + url) + .setClientId(clientId); + } + + // 4、是否在[允许地址列表]之中 + SaClientModel clientModel = checkClientModel(clientId); + checkRedirectUriListNormal(clientModel.allowRedirectUris); + if( ! SaStrategy.instance.hasElement.apply(clientModel.allowRedirectUris, url)) { + throw new SaOAuth2ClientModelException("非法 redirect_url: " + url) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30114); + } + } + + /** + * 校验配置的 allowRedirectUris 是否合规,如果不合规则抛出异常 + * @param redirectUriList 待校验的 allow-url 地址列表 + */ + public void checkRedirectUriListNormal(List redirectUriList){ + checkRedirectUriListNormalStaticMethod(redirectUriList); + } + + /** + * 校验配置的 allowRedirectUris 是否合规,如果不合规则抛出异常,静态方法内部实现 + * @param redirectUriList 待校验的 allow-url 地址列表 + */ + public static void checkRedirectUriListNormalStaticMethod(List redirectUriList){ + for (String url : redirectUriList) { + int index = url.indexOf("*"); + // 如果配置了 * 字符,则必须出现在最后一位,否则属于无效配置项 + if(index != -1 && index != url.length() - 1) { + // 为什么不允许 * 字符出现在中间位置呢,因为这有可能导致 redirect 参数绕过 allow-url 列表的校验 + // + // 举个例子 SaClientModel 配置: + // allow-url=http://*.sa-oauth-client.com/ + // + // 开发者原意是为了允许 sa-oauth-client.com 下的所有子域名都可以下放 ticket + // 例如:http://shop.sa-oauth-client.com/ + // + // 但是如果攻击者精心构建一个url: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-token.cc/a.sa-oauth-client.com/ + // + // 那么这个 url 就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: + // http://sa-token.cc/a.sa-oauth-client.com/?code=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx + // + // 造成了 ticket 参数劫持 + // 所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符(出现在末尾是没有问题的) + // + // 这么一刀切的做法,可能会导致正常场景下的子域名url也无法通过校验,例如: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://shop.sa-oauth2-client.com/ + // + // 但是为了安全起见,这么做还是有必要的 + throw new SaOAuth2Exception("无效的 allow-url 配置(*通配符只允许出现在最后一位):" + url); + } + } + } + + // --------- 授权相关 /** * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope @@ -131,10 +245,9 @@ public class SaOAuth2Template { * @param scopes 权限 * @return 是否已经授权 */ - public boolean isGrant(Object loginId, String clientId, List scopes) { - SaOAuth2Dao dao = SaOAuth2Manager.getDao(); - List grantScopeList = dao.getGrantScope(clientId, loginId); - return scopes.isEmpty() || new HashSet<>(grantScopeList).containsAll(scopes); + public boolean isGrantScope(Object loginId, String clientId, List scopes) { + List grantScopeList = SaOAuth2Manager.getDao().getGrantScope(clientId, loginId); + return SaFoxUtil.list1ContainList2AllElement(grantScopeList, scopes); } /** @@ -166,143 +279,12 @@ public class SaOAuth2Template { } // 根据近期授权记录,判断是否需要确认 - return !isGrant(loginId, clientId, scopes); + return !isGrantScope(loginId, clientId, scopes); } - /** - * 校验:该Client是否签约了指定的Scope - * @param clientId 应用id - * @param scopes 权限(多个用逗号隔开) - */ - public void checkContract(String clientId, List scopes) { - List clientScopeList = checkClientModel(clientId).contractScopes; - if( ! new HashSet<>(clientScopeList).containsAll(scopes)) { - throw new SaOAuth2Exception("请求的Scope暂未签约").setCode(SaOAuth2ErrorCode.CODE_30112); - } - } - /** - * 校验:该Client使用指定url作为回调地址,是否合法 - * @param clientId 应用id - * @param url 指定url - */ - public void checkRightUrl(String clientId, String url) { - // 1、是否是一个有效的url - if( ! SaFoxUtil.isUrl(url)) { - throw new SaOAuth2Exception("无效redirect_url:" + url).setCode(SaOAuth2ErrorCode.CODE_30113); - } - // 2、截取掉?后面的部分 - int qIndex = url.indexOf("?"); - if(qIndex != -1) { - url = url.substring(0, qIndex); - } + // --------- 请求数据校验相关 - // 3、不允许出现@字符 - if(url.contains("@")) { - // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect_url 参数绕过 AllowUrl 列表的校验 - // - // 举个例子 SaClientModel 配置: - // allow-url=http://sa-oauth-client.com* - // - // 开发者原意是为了允许 sa-oauth-client.com 下的所有地址都可以下放 code - // - // 但是如果攻击者精心构建一个url: - // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com@sa-token.cc - // - // 那么这个url就会绕过 allow-url 的校验,code 被下发到了第三方服务器地址: - // http://sa-token.cc/?code=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul - // - // 造成了 code 参数劫持 - // 所以此处需要禁止在 url 中出现 @ 字符 - // - // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: - // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com/@getInfo - // - // 但是为了安全起见,这么做还是有必要的 - throw new SaOAuth2Exception("无效 redirect_url(不允许出现@字符):" + url); - } - - // 4、是否在[允许地址列表]之中 - SaClientModel clientModel = checkClientModel(clientId); - checkAllowUrlList(clientModel.allowRedirectUris); - if( ! SaStrategy.instance.hasElement.apply(clientModel.allowRedirectUris, url)) { - throw new SaOAuth2Exception("非法 redirect_url: " + url).setCode(SaOAuth2ErrorCode.CODE_30114); - } - } - - /** - * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 - * @param allowUrlList 待校验的 allow-url 地址列表 - */ - public void checkAllowUrlList(List allowUrlList){ - checkAllowUrlListStaticMethod(allowUrlList); - } - - /** - * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 - * @param allowUrlList 待校验的 allow-url 地址列表 - */ - public static void checkAllowUrlListStaticMethod(List allowUrlList){ - for (String url : allowUrlList) { - int index = url.indexOf("*"); - // 如果配置了 * 字符,则必须出现在最后一位,否则属于无效配置项 - if(index != -1 && index != url.length() - 1) { - // 为什么不允许 * 字符出现在中间位置呢,因为这有可能导致 redirect 参数绕过 allow-url 列表的校验 - // - // 举个例子 SaClientModel 配置: - // allow-url=http://*.sa-oauth-client.com/ - // - // 开发者原意是为了允许 sa-oauth-client.com 下的所有子域名都可以下放 ticket - // 例如:http://shop.sa-oauth-client.com/ - // - // 但是如果攻击者精心构建一个url: - // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-token.cc/a.sa-oauth-client.com/ - // - // 那么这个 url 就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: - // http://sa-token.cc/a.sa-oauth-client.com/?code=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx - // - // 造成了 ticket 参数劫持 - // 所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符(出现在末尾是没有问题的) - // - // 这么一刀切的做法,可能会导致正常场景下的子域名url也无法通过校验,例如: - // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://shop.sa-oauth2-client.com/ - // - // 但是为了安全起见,这么做还是有必要的 - throw new SaOAuth2Exception("无效的 allow-url 配置(*通配符只允许出现在最后一位):" + url); - } - } - } - - /** - * 校验:clientId 与 clientSecret 是否正确 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @return SaClientModel对象 - */ - public SaClientModel checkClientSecret(String clientId, String clientSecret) { - SaClientModel cm = checkClientModel(clientId); - SaOAuth2Exception.throwBy(cm.clientSecret == null || ! cm.clientSecret.equals(clientSecret), - "无效client_secret: " + clientSecret, SaOAuth2ErrorCode.CODE_30115); - return cm; - } - /** - * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param scopes 权限 - * @return SaClientModel对象 - */ - public SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List scopes) { - // 先校验 clientSecret - SaClientModel cm = checkClientSecret(clientId, clientSecret); - // 再校验 是否签约 - List clientScopeList = cm.contractScopes; - if( ! new HashSet<>(clientScopeList).containsAll(scopes)) { - throw new SaOAuth2Exception("请求的Scope暂未签约").setCode(SaOAuth2ErrorCode.CODE_30116); - } - // 返回数据 - return cm; - } /** * 校验:使用 code 获取 token 时提供的参数校验 * @param code 授权码 @@ -317,23 +299,24 @@ public class SaOAuth2Template { // 校验:Code是否存在 CodeModel cm = dao.getCode(code); - SaOAuth2Exception.throwBy(cm == null, "无效code: " + code, SaOAuth2ErrorCode.CODE_30117); + SaOAuth2Exception.throwBy(cm == null, "无效 code: " + code, SaOAuth2ErrorCode.CODE_30117); // 校验:ClientId是否一致 - SaOAuth2Exception.throwBy( ! cm.clientId.equals(clientId), "无效client_id: " + clientId, SaOAuth2ErrorCode.CODE_30118); + SaOAuth2Exception.throwBy( ! cm.clientId.equals(clientId), "无效 client_id: " + clientId, SaOAuth2ErrorCode.CODE_30118); // 校验:Secret是否正确 String dbSecret = checkClientModel(clientId).clientSecret; - SaOAuth2Exception.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效client_secret: " + clientSecret, SaOAuth2ErrorCode.CODE_30119); + SaOAuth2Exception.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效 client_secret: " + clientSecret, SaOAuth2ErrorCode.CODE_30119); // 如果提供了redirectUri,则校验其是否与请求Code时提供的一致 if( ! SaFoxUtil.isEmpty(redirectUri)) { - SaOAuth2Exception.throwBy( ! redirectUri.equals(cm.redirectUri), "无效redirect_uri: " + redirectUri, SaOAuth2ErrorCode.CODE_30120); + SaOAuth2Exception.throwBy( ! redirectUri.equals(cm.redirectUri), "无效 redirect_uri: " + redirectUri, SaOAuth2ErrorCode.CODE_30120); } // 返回CodeModel return cm; } + /** * 校验:使用 Refresh-Token 刷新 Access-Token 时提供的参数校验 * @param clientId 应用id @@ -347,18 +330,20 @@ public class SaOAuth2Template { // 校验:Refresh-Token是否存在 RefreshTokenModel rt = dao.getRefreshToken(refreshToken); - SaOAuth2Exception.throwBy(rt == null, "无效refresh_token: " + refreshToken, SaOAuth2ErrorCode.CODE_30121); + SaOAuth2RefreshTokenException.throwBy(rt == null, "无效 refresh_token: " + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30121); // 校验:ClientId是否一致 - SaOAuth2Exception.throwBy( ! rt.clientId.equals(clientId), "无效client_id: " + clientId, SaOAuth2ErrorCode.CODE_30122); + SaOAuth2ClientModelException.throwBy( ! rt.clientId.equals(clientId), "无效 client_id: " + clientId, clientId, SaOAuth2ErrorCode.CODE_30122); // 校验:Secret是否正确 String dbSecret = checkClientModel(clientId).clientSecret; - SaOAuth2Exception.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效client_secret: " + clientSecret, SaOAuth2ErrorCode.CODE_30123); + SaOAuth2ClientModelException.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效client_secret: " + clientSecret, + clientId, SaOAuth2ErrorCode.CODE_30123); - // 返回Refresh-Token + // 返回 Refresh-Token return rt; } + /** * 校验:Access-Token、clientId、clientSecret 三者是否匹配成功 * @param clientId 应用id @@ -368,30 +353,271 @@ public class SaOAuth2Template { */ public AccessTokenModel checkAccessTokenParam(String clientId, String clientSecret, String accessToken) { AccessTokenModel at = checkAccessToken(accessToken); - SaOAuth2Exception.throwBy( ! at.clientId.equals(clientId), "无效client_id:" + clientId, SaOAuth2ErrorCode.CODE_30124); + SaOAuth2ClientModelException.throwBy( ! at.clientId.equals(clientId), "无效 client_id:" + clientId, clientId, SaOAuth2ErrorCode.CODE_30124); checkClientSecret(clientId, clientSecret); return at; } + // ----------------- Access-Token 相关 ----------------- + /** - * 回收指定的 ClientToken + * 获取 AccessTokenModel,无效的 AccessToken 会返回 null + * @param accessToken / + * @return / + */ + public AccessTokenModel getAccessToken(String accessToken) { + return SaOAuth2Manager.getDao().getAccessToken(accessToken); + } + + /** + * 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 + * @param accessToken / + * @return / + */ + public AccessTokenModel checkAccessToken(String accessToken) { + AccessTokenModel at = SaOAuth2Manager.getDao().getAccessToken(accessToken); + if(at == null) { + throw new SaOAuth2AccessTokenException("无效 access_token: " + accessToken) + .setAccessToken(accessToken) + .setCode(SaOAuth2ErrorCode.CODE_30106); + } + return at; + } + + /** + * 获取 Access-Token,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + * @return / + */ + public String getAccessTokenValue(String clientId, Object loginId) { + return SaOAuth2Manager.getDao().getAccessTokenValue(clientId, loginId); + } + + /** + * 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public boolean hasAccessTokenScope(String accessToken, String... scopes) { + try { + checkAccessTokenScope(accessToken, scopes); + return true; + } catch (SaOAuth2AccessTokenException e) { + return false; + } + } + + /** + * 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public void checkAccessTokenScope(String accessToken, String... scopes) { + if(SaFoxUtil.isEmptyArray(scopes)) { + return; + } + ClientTokenModel ct = checkClientToken(accessToken); + for (String scope : scopes) { + if(! ct.scopes.contains(scope)) { + throw new SaOAuth2AccessTokenScopeException("该 access_token 不具备 scope:" + scope) + .setAccessToken(accessToken) + .setScope(scope) + .setCode(SaOAuth2ErrorCode.CODE_30108); + } + } + } + + /** + * 获取 Access-Token 所代表的LoginId + * @param accessToken Access-Token + * @return LoginId + */ + public Object getLoginIdByAccessToken(String accessToken) { + return checkAccessToken(accessToken).loginId; + } + + /** + * 获取 Access-Token 所代表的 clientId + * @param accessToken Access-Token + * @return LoginId + */ + public Object getClientIdByAccessToken(String accessToken) { + return checkAccessToken(accessToken).clientId; + } + + /** + * 回收 Access-Token + * @param accessToken Access-Token值 + */ + public void revokeAccessToken(String accessToken) { + AccessTokenModel at = getAccessToken(accessToken); + if(at == null) { + return; + } + + // 删 at、索引 + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + dao.deleteAccessToken(accessToken); + dao.deleteAccessTokenIndex(at.clientId, at.loginId); + + // 删对应的 rt、索引 +// String rtValue = dao.getRefreshTokenValue(at.clientId, at.loginId); +// dao.deleteRefreshToken(rtValue); +// dao.deleteRefreshTokenIndex(at.clientId, at.loginId); + } + + /** + * 回收 Access-Token,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + */ + public void revokeAccessTokenByIndex(String clientId, Object loginId) { + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + // 删 at、删索引 + String accessToken = getAccessTokenValue(clientId, loginId); + if(accessToken != null) { + dao.deleteAccessToken(accessToken); + dao.deleteAccessTokenIndex(clientId, loginId); + } + } + + + // ----------------- Refresh-Token 相关 ----------------- + + /** + * 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null + * @param refreshToken / + * @return / + */ + public RefreshTokenModel getRefreshToken(String refreshToken) { + return SaOAuth2Manager.getDao().getRefreshToken(refreshToken); + } + + /** + * 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 + * @param refreshToken / + * @return / + */ + public RefreshTokenModel checkRefreshToken(String refreshToken) { + RefreshTokenModel rt = SaOAuth2Manager.getDao().getRefreshToken(refreshToken); + if(rt == null) { + throw new SaOAuth2RefreshTokenException("无效 refresh_token: " + refreshToken) + .setRefreshToken(refreshToken) + .setCode(SaOAuth2ErrorCode.CODE_30111); + } + return rt; + } + + /** + * 获取 Refresh-Token,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + * @return / + */ + public String getRefreshTokenValue(String clientId, Object loginId) { + return SaOAuth2Manager.getDao().getRefreshTokenValue(clientId, loginId); + } + + /** + * 根据 RefreshToken 刷新出一个 AccessToken + * @param refreshToken / + * @return / + */ + public AccessTokenModel refreshAccessToken(String refreshToken) { + return SaOAuth2Manager.getDataGenerate().refreshAccessToken(refreshToken); + } + + + // ----------------- Client-Token 相关 ----------------- + + /** + * 获取 ClientTokenModel,无效的 ClientToken 会返回 null + * @param clientToken / + * @return / + */ + public ClientTokenModel getClientToken(String clientToken) { + return SaOAuth2Manager.getDao().getClientToken(clientToken); + } + + /** + * 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 + * @param clientToken / + * @return / + */ + public ClientTokenModel checkClientToken(String clientToken) { + ClientTokenModel ct = getClientToken(clientToken); + if(ct == null) { + throw new SaOAuth2ClientTokenException("无效 client_token: " + clientToken) + .setClientToken(clientToken) + .setCode(SaOAuth2ErrorCode.CODE_30107); + } + return ct; + } + + /** + * 获取 ClientToken,根据索引: clientId + * @param clientId / + * @return / + */ + public String getClientTokenValue(String clientId) { + return SaOAuth2Manager.getDao().getClientTokenValue(clientId); + } + + /** + * 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public boolean hasClientTokenScope(String clientToken, String... scopes) { + try { + checkClientTokenScope(clientToken, scopes); + return true; + } catch (SaOAuth2ClientTokenException e) { + return false; + } + } + + /** + * 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public void checkClientTokenScope(String clientToken, String... scopes) { + if(SaFoxUtil.isEmptyArray(scopes)) { + return; + } + ClientTokenModel ct = checkClientToken(clientToken); + for (String scope : scopes) { + if(! ct.scopes.contains(scope)) { + throw new SaOAuth2ClientTokenScopeException("该 client_token 不具备 scope:" + scope) + .setClientToken(clientToken) + .setScope(scope) + .setCode(SaOAuth2ErrorCode.CODE_30109); + } + } + } + + /** + * 回收 ClientToken * * @param clientToken / */ public void revokeClientToken(String clientToken) { - SaOAuth2Dao dao = SaOAuth2Manager.getDao(); - ClientTokenModel ctModel = dao.getClientToken(clientToken); - if(ctModel == null) { + ClientTokenModel ct = getClientToken(clientToken); + if(ct == null) { return; } - // 删 ct、索引 + // 删 ct、删索引 + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); dao.deleteClientToken(clientToken); - dao.deleteClientTokenIndex(ctModel.clientId); + dao.deleteClientTokenIndex(ct.clientId); } /** - * 回收指定的 ClientToken,根据索引: clientId + * 回收 ClientToken,根据索引: clientId * * @param clientId / */ @@ -399,33 +625,30 @@ public class SaOAuth2Template { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); // 删 clientToken - String clientToken = dao.getClientTokenValue(clientId); + String clientToken = getClientTokenValue(clientId); if(clientToken != null) { dao.deleteClientToken(clientToken); dao.deleteClientTokenIndex(clientId); } + } + /** + * 回收 PastToken,根据索引: clientId + * + * @param clientId / + */ + public void revokePastTokenByIndex(String clientId) { + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); // 删 pastToken String pastToken = dao.getPastTokenValue(clientId); if(pastToken != null) { dao.deletePastToken(pastToken); dao.deletePastTokenIndex(clientId); } - } - - // ------------------- 包装其它 bean 的方法 - - /** - * 获取:Access-Token Model - * @param accessToken / - * @return / - */ - public AccessTokenModel getAccessToken(String accessToken) { - return SaOAuth2Manager.getDao().getAccessToken(accessToken); - } + // ----------------- 包装其它 bean 的方法 ----------------- /** * 持久化:用户授权记录 @@ -455,8 +678,4 @@ public class SaOAuth2Template { return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope); } - - - - } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java index 223d7dcb..4d667933 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java @@ -18,120 +18,52 @@ package cn.dev33.satoken.oauth2.template; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; -import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import java.util.List; /** - * Sa-Token-OAuth2 模块 工具类 + * Sa-Token OAuth2 模块 工具类 * * @author click33 * @since 1.23.0 */ public class SaOAuth2Util { - - // ------------------- 资源校验API - + + // ----------------- ClientModel 相关 ----------------- + /** - * 根据id获取Client信息, 如果Client为空,则抛出异常 - * @param clientId 应用id - * @return ClientModel + * 获取 ClientModel,根据 clientId + * + * @param clientId / + * @return / + */ + public static SaClientModel getClientModel(String clientId) { + return SaOAuth2Manager.getTemplate().getClientModel(clientId); + } + + /** + * 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 + * @param clientId / + * @return / */ public static SaClientModel checkClientModel(String clientId) { return SaOAuth2Manager.getTemplate().checkClientModel(clientId); } - - /** - * 获取 Access-Token,如果AccessToken为空则抛出异常 - * @param accessToken . - * @return . - */ - public static AccessTokenModel checkAccessToken(String accessToken) { - return SaOAuth2Manager.getTemplate().checkAccessToken(accessToken); - } - - /** - * 获取 Client-Token,如果ClientToken为空则抛出异常 - * @param clientToken . - * @return . - */ - public static ClientTokenModel checkClientToken(String clientToken) { - return SaOAuth2Manager.getTemplate().checkClientToken(clientToken); - } - - /** - * 获取 Access-Token 所代表的LoginId - * @param accessToken Access-Token - * @return LoginId - */ - public static Object getLoginIdByAccessToken(String accessToken) { - return SaOAuth2Manager.getTemplate().getLoginIdByAccessToken(accessToken); - } - - /** - * 校验:指定 Access-Token 是否具有指定 Scope - * @param accessToken Access-Token - * @param scopes 需要校验的权限列表 - */ - public static void checkScope(String accessToken, String... scopes) { - SaOAuth2Manager.getTemplate().checkScope(accessToken, scopes); - } - - /** - * 校验:指定 Client-Token 是否具有指定 Scope - * @param clientToken Client-Token - * @param scopes 需要校验的权限列表 - */ - public static void checkClientTokenScope(String clientToken, String... scopes) { - SaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scopes); - } - - // ------------------- 数据校验 - - /** - * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope - * @param loginId 账号id - * @param clientId 应用id - * @param scopes 权限 - * @return 是否已经授权 - */ - public static boolean isGrant(Object loginId, String clientId, List scopes) { - return SaOAuth2Manager.getTemplate().isGrant(loginId, clientId, scopes); - } - - /** - * 校验:该Client是否签约了指定的Scope - * @param clientId 应用id - * @param scopes 权限(多个用逗号隔开) - */ - public static void checkContract(String clientId, List scopes) { - SaOAuth2Manager.getTemplate().checkContract(clientId, scopes); - } - - /** - * 校验:该Client使用指定url作为回调地址,是否合法 - * @param clientId 应用id - * @param url 指定url - */ - public static void checkRightUrl(String clientId, String url) { - SaOAuth2Manager.getTemplate().checkRightUrl(clientId, url); - } - /** * 校验:clientId 与 clientSecret 是否正确 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @return SaClientModel对象 + * @param clientId 应用id + * @param clientSecret 秘钥 + * @return SaClientModel对象 */ public static SaClientModel checkClientSecret(String clientId, String clientSecret) { return SaOAuth2Manager.getTemplate().checkClientSecret(clientId, clientSecret); } - + /** - * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes + * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes * @param clientId 应用id * @param clientSecret 秘钥 * @param scopes 权限 @@ -140,43 +72,235 @@ public class SaOAuth2Util { public static SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List scopes) { return SaOAuth2Manager.getTemplate().checkClientSecretAndScope(clientId, clientSecret, scopes); } - + /** - * 校验:使用 code 获取 token 时提供的参数校验 - * @param code 授权码 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param redirectUri 重定向地址 - * @return CodeModel对象 + * 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false + * @param clientId 应用id + * @param scopes 权限 + * @return / */ - public static CodeModel checkGainTokenParam(String code, String clientId, String clientSecret, String redirectUri) { - return SaOAuth2Manager.getTemplate().checkGainTokenParam(code, clientId, clientSecret, redirectUri); + public static boolean isContractScope(String clientId, List scopes) { + return SaOAuth2Manager.getTemplate().isContractScope(clientId, scopes); } /** - * 校验:使用 Refresh-Token 刷新 Access-Token 时提供的参数校验 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param refreshToken Refresh-Token - * @return CodeModel对象 + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param clientId 应用id + * @param scopes 权限列表 + * @return / */ - public static RefreshTokenModel checkRefreshTokenParam(String clientId, String clientSecret, String refreshToken) { - return SaOAuth2Manager.getTemplate().checkRefreshTokenParam(clientId, clientSecret, refreshToken); - } - - /** - * 校验:Access-Token、clientId、clientSecret 三者是否匹配成功 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param accessToken Access-Token - * @return SaClientModel对象 - */ - public static AccessTokenModel checkAccessTokenParam(String clientId, String clientSecret, String accessToken) { - return SaOAuth2Manager.getTemplate().checkAccessTokenParam(clientId, clientSecret, accessToken); + public static SaClientModel checkContractScope(String clientId, List scopes) { + return SaOAuth2Manager.getTemplate().checkContractScope(clientId, scopes); } /** - * 回收指定的 ClientToken + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param cm 应用 + * @param scopes 权限列表 + * @return / + */ + public static SaClientModel checkContractScope(SaClientModel cm, List scopes) { + return SaOAuth2Manager.getTemplate().checkContractScope(cm, scopes); + } + + // --------- redirect_uri 相关 + + /** + * 校验:该 Client 使用指定 url 作为回调地址,是否合法 + * @param clientId 应用id + * @param url 指定url + */ + public static void checkRedirectUri(String clientId, String url) { + SaOAuth2Manager.getTemplate().checkRedirectUri(clientId, url); + } + + // --------- 授权相关 + + /** + * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope + * @param loginId 账号id + * @param clientId 应用id + * @param scopes 权限 + * @return 是否已经授权 + */ + public static boolean isGrantScope(Object loginId, String clientId, List scopes) { + return SaOAuth2Manager.getTemplate().isGrantScope(loginId, clientId, scopes); + } + + + // ----------------- Access-Token 相关 ----------------- + + /** + * 获取 AccessTokenModel,无效的 AccessToken 会返回 null + * @param accessToken / + * @return / + */ + public static AccessTokenModel getAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().getAccessToken(accessToken); + } + + /** + * 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 + * @param accessToken / + * @return / + */ + public static AccessTokenModel checkAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().checkAccessToken(accessToken); + } + + /** + * 获取 Access-Token,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + * @return / + */ + public static String getAccessTokenValue(String clientId, Object loginId) { + return SaOAuth2Manager.getTemplate().getAccessTokenValue(clientId, loginId); + } + + /** + * 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public static boolean hasAccessTokenScope(String accessToken, String... scopes) { + return SaOAuth2Manager.getTemplate().hasAccessTokenScope(accessToken, scopes); + } + + /** + * 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public static void checkAccessTokenScope(String accessToken, String... scopes) { + SaOAuth2Manager.getTemplate().checkAccessTokenScope(accessToken, scopes); + } + + /** + * 获取 Access-Token 所代表的LoginId + * @param accessToken Access-Token + * @return LoginId + */ + public static Object getLoginIdByAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().getLoginIdByAccessToken(accessToken); + } + + /** + * 获取 Access-Token 所代表的 clientId + * @param accessToken Access-Token + * @return LoginId + */ + public static Object getClientIdByAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().getClientIdByAccessToken(accessToken); + } + + /** + * 回收 Access-Token + * @param accessToken Access-Token值 + */ + public static void revokeAccessToken(String accessToken) { + SaOAuth2Manager.getTemplate().revokeAccessToken(accessToken); + } + + /** + * 回收 Access-Token,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + */ + public static void revokeAccessTokenByIndex(String clientId, Object loginId) { + SaOAuth2Manager.getTemplate().revokeAccessTokenByIndex(clientId, loginId); + } + + + // ----------------- Refresh-Token 相关 ----------------- + + /** + * 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null + * @param refreshToken / + * @return / + */ + public static RefreshTokenModel getRefreshToken(String refreshToken) { + return SaOAuth2Manager.getTemplate().getRefreshToken(refreshToken); + } + + /** + * 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 + * @param refreshToken / + * @return / + */ + public static RefreshTokenModel checkRefreshToken(String refreshToken) { + return SaOAuth2Manager.getTemplate().checkRefreshToken(refreshToken); + } + + /** + * 获取 Refresh-Token,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + * @return / + */ + public static String getRefreshTokenValue(String clientId, Object loginId) { + return SaOAuth2Manager.getTemplate().getRefreshTokenValue(clientId, loginId); + } + + /** + * 根据 RefreshToken 刷新出一个 AccessToken + * @param refreshToken / + * @return / + */ + public static AccessTokenModel refreshAccessToken(String refreshToken) { + return SaOAuth2Manager.getTemplate().refreshAccessToken(refreshToken); + } + + + // ----------------- Client-Token 相关 ----------------- + + /** + * 获取 ClientTokenModel,无效的 ClientToken 会返回 null + * @param clientToken / + * @return / + */ + public static ClientTokenModel getClientToken(String clientToken) { + return SaOAuth2Manager.getTemplate().getClientToken(clientToken); + } + + /** + * 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 + * @param clientToken / + * @return / + */ + public static ClientTokenModel checkClientToken(String clientToken) { + return SaOAuth2Manager.getTemplate().checkClientToken(clientToken); + } + + /** + * 获取 ClientToken,根据索引: clientId + * @param clientId / + * @return / + */ + public static String getClientTokenValue(String clientId) { + return SaOAuth2Manager.getTemplate().getClientTokenValue(clientId); + } + + /** + * 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public static boolean hasClientTokenScope(String clientToken, String... scopes) { + return SaOAuth2Manager.getTemplate().hasClientTokenScope(clientToken, scopes); + } + + /** + * 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public static void checkClientTokenScope(String clientToken, String... scopes) { + SaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scopes); + } + + /** + * 回收 ClientToken * * @param clientToken / */ @@ -185,7 +309,7 @@ public class SaOAuth2Util { } /** - * 回收指定的 ClientToken,根据索引:clientId + * 回收 ClientToken,根据索引: clientId * * @param clientId / */ @@ -193,65 +317,13 @@ public class SaOAuth2Util { SaOAuth2Manager.getTemplate().revokeClientTokenByIndex(clientId); } - // ------------------- save 数据 - /** - * 持久化:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - * @param scopes 权限列表 + * 回收 PastToken,根据索引: clientId + * + * @param clientId / */ - public static void saveGrantScope(String clientId, Object loginId, List scopes) { - SaOAuth2Manager.getTemplate().saveGrantScope(clientId, loginId, scopes); - } - - - // ------------------- get 数据 - - /** - * 获取:Code Model - * @param code . - * @return . - */ - public static CodeModel getCode(String code) { - return SaOAuth2Manager.getDao().getCode(code); - } - - /** - * 获取:Access-Token Model - * @param accessToken . - * @return . - */ - public static AccessTokenModel getAccessToken(String accessToken) { - return SaOAuth2Manager.getDao().getAccessToken(accessToken); - } - - /** - * 获取:Refresh-Token Model - * @param refreshToken . - * @return . - */ - public static RefreshTokenModel getRefreshToken(String refreshToken) { - return SaOAuth2Manager.getDao().getRefreshToken(refreshToken); - } - - /** - * 获取:Client-Token Model - * @param clientToken . - * @return . - */ - public static ClientTokenModel getClientToken(String clientToken) { - return SaOAuth2Manager.getDao().getClientToken(clientToken); - } - - /** - * 获取:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - * @return 权限 - */ - public static List getGrantScope(String clientId, Object loginId) { - return SaOAuth2Manager.getDao().getGrantScope(clientId, loginId); + public static void revokePastTokenByIndex(String clientId) { + SaOAuth2Manager.getTemplate().revokePastTokenByIndex(clientId); } }