Commit aa8fb4acf0fda7b71acc2bdcacbdf3bef398a210
1 parent
ce60ec08
搭建 oauth/authorize 的初步逻辑
Showing
11 changed files
with
252 additions
and
52 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java
| 1 | package cn.iocoder.yudao.framework.common.util.string; | 1 | package cn.iocoder.yudao.framework.common.util.string; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.util.ObjectUtil; | 3 | +import cn.hutool.core.util.ArrayUtil; |
| 4 | import cn.hutool.core.util.StrUtil; | 4 | import cn.hutool.core.util.StrUtil; |
| 5 | 5 | ||
| 6 | -import java.util.Map; | 6 | +import java.util.Collection; |
| 7 | 7 | ||
| 8 | /** | 8 | /** |
| 9 | * 字符串工具类 | 9 | * 字符串工具类 |
| @@ -17,21 +17,24 @@ public class StrUtils { | @@ -17,21 +17,24 @@ public class StrUtils { | ||
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | /** | 19 | /** |
| 20 | - * 指定字符串的 | ||
| 21 | - * @param str | ||
| 22 | - * @param replaceMap | ||
| 23 | - * @return | 20 | + * 给定字符串是否以任何一个字符串开始 |
| 21 | + * 给定字符串和数组为空都返回 false | ||
| 22 | + * | ||
| 23 | + * @param str 给定字符串 | ||
| 24 | + * @param prefixes 需要检测的开始字符串 | ||
| 25 | + * @since 3.0.6 | ||
| 24 | */ | 26 | */ |
| 25 | - public static String replace(String str, Map<String, String> replaceMap) { | ||
| 26 | - assert StrUtil.isNotBlank(str); | ||
| 27 | - if (ObjectUtil.isEmpty(replaceMap)) { | ||
| 28 | - return str; | 27 | + public static boolean startWithAny(String str, Collection<String> prefixes) { |
| 28 | + if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { | ||
| 29 | + return false; | ||
| 29 | } | 30 | } |
| 30 | - String result = null; | ||
| 31 | - for (String key : replaceMap.keySet()) { | ||
| 32 | - result = str.replace(key, replaceMap.get(key)); | 31 | + |
| 32 | + for (CharSequence suffix : prefixes) { | ||
| 33 | + if (StrUtil.startWith(str, suffix, false)) { | ||
| 34 | + return true; | ||
| 35 | + } | ||
| 33 | } | 36 | } |
| 34 | - return result; | 37 | + return false; |
| 35 | } | 38 | } |
| 36 | 39 | ||
| 37 | } | 40 | } |
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
| @@ -126,5 +126,9 @@ public interface ErrorCodeConstants { | @@ -126,5 +126,9 @@ public interface ErrorCodeConstants { | ||
| 126 | // ========== 系统敏感词 1002020000 ========= | 126 | // ========== 系统敏感词 1002020000 ========= |
| 127 | ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在"); | 127 | ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在"); |
| 128 | ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在"); | 128 | ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在"); |
| 129 | + ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用"); | ||
| 130 | + ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型"); | ||
| 131 | + ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大"); | ||
| 132 | + ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020004, "重定向地址不匹配"); | ||
| 129 | 133 | ||
| 130 | } | 134 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2Controller.java
| 1 | package cn.iocoder.yudao.module.system.controller.admin.auth; | 1 | package cn.iocoder.yudao.module.system.controller.admin.auth; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.util.ObjectUtil; | ||
| 3 | import cn.hutool.core.util.StrUtil; | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | 5 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 6 | import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; | 8 | import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; |
| 9 | +import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum; | ||
| 10 | +import cn.iocoder.yudao.module.system.service.auth.OAuth2ApproveService; | ||
| 11 | +import cn.iocoder.yudao.module.system.service.auth.OAuth2ClientService; | ||
| 7 | import io.swagger.annotations.Api; | 12 | import io.swagger.annotations.Api; |
| 8 | import io.swagger.annotations.ApiImplicitParam; | 13 | import io.swagger.annotations.ApiImplicitParam; |
| 9 | import io.swagger.annotations.ApiImplicitParams; | 14 | import io.swagger.annotations.ApiImplicitParams; |
| 10 | import io.swagger.annotations.ApiOperation; | 15 | import io.swagger.annotations.ApiOperation; |
| 11 | import lombok.extern.slf4j.Slf4j; | 16 | import lombok.extern.slf4j.Slf4j; |
| 12 | import org.springframework.validation.annotation.Validated; | 17 | import org.springframework.validation.annotation.Validated; |
| 13 | -import org.springframework.web.bind.annotation.PostMapping; | ||
| 14 | -import org.springframework.web.bind.annotation.RequestMapping; | ||
| 15 | -import org.springframework.web.bind.annotation.RequestParam; | ||
| 16 | -import org.springframework.web.bind.annotation.RestController; | 18 | +import org.springframework.web.bind.annotation.*; |
| 17 | 19 | ||
| 20 | +import javax.annotation.Resource; | ||
| 21 | +import java.util.Collections; | ||
| 22 | +import java.util.List; | ||
| 23 | +import java.util.Map; | ||
| 24 | + | ||
| 25 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | ||
| 18 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; | 26 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; |
| 27 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 28 | +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | ||
| 19 | 29 | ||
| 20 | @Api(tags = "管理后台 - OAuth2.0 授权") | 30 | @Api(tags = "管理后台 - OAuth2.0 授权") |
| 21 | @RestController | 31 | @RestController |
| @@ -32,37 +42,105 @@ public class OAuth2Controller { | @@ -32,37 +42,105 @@ public class OAuth2Controller { | ||
| 32 | 42 | ||
| 33 | // GET oauth/authorize AuthorizationEndpoint | 43 | // GET oauth/authorize AuthorizationEndpoint |
| 34 | 44 | ||
| 35 | - @PostMapping("/authorize") | ||
| 36 | - @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被调用") | 45 | + @Resource |
| 46 | + private OAuth2ClientService oauth2ClientService; | ||
| 47 | + @Resource | ||
| 48 | + private OAuth2ApproveService oauth2ApproveService; | ||
| 49 | + | ||
| 50 | + @GetMapping("/authorize") | ||
| 51 | + @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") | ||
| 37 | @ApiImplicitParams({ | 52 | @ApiImplicitParams({ |
| 38 | @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class), | 53 | @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class), |
| 39 | @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class), | 54 | @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class), |
| 40 | - @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 多个使用逗号分隔 | 55 | + @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 多个使用空格分隔 |
| 41 | @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), | 56 | @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), |
| 42 | @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class) | 57 | @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class) |
| 43 | }) | 58 | }) |
| 44 | - @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | ||
| 45 | - // 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 | ||
| 46 | - public CommonResult<String> authorize(@RequestParam("response_type") String responseType, | 59 | + // 情况一:满足自动授权,则走类似 approveOrDeny 的逻辑,最终返回是 CommonResult<String> |
| 60 | + // 情况二:如果不满足自动授权,则返回授权相关的展示信息,最终返回是 CommonResult<OAuth2AuthorizeRespInfo> | ||
| 61 | + public CommonResult<Object> authorize(@RequestParam("response_type") String responseType, | ||
| 47 | @RequestParam("client_id") String clientId, | 62 | @RequestParam("client_id") String clientId, |
| 48 | @RequestParam(value = "scope", required = false) String scope, | 63 | @RequestParam(value = "scope", required = false) String scope, |
| 49 | @RequestParam("redirect_uri") String redirectUri, | 64 | @RequestParam("redirect_uri") String redirectUri, |
| 50 | @RequestParam(value = "state", required = false) String state) { | 65 | @RequestParam(value = "state", required = false) String state) { |
| 66 | + List<String> scopes = StrUtil.split(scope, ' '); | ||
| 51 | // 0. 校验用户已经登录。通过 Spring Security 实现 | 67 | // 0. 校验用户已经登录。通过 Spring Security 实现 |
| 52 | 68 | ||
| 53 | // 1.1 校验 responseType 是否满足 code 或者 token 值 | 69 | // 1.1 校验 responseType 是否满足 code 或者 token 值 |
| 54 | - if (!StrUtil.equalsAny(responseType, "code", "token")) { | ||
| 55 | - throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token"); | 70 | + OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); |
| 71 | + // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 | ||
| 72 | + oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes, redirectUri); | ||
| 73 | + | ||
| 74 | + // 2. 判断是否满足自动授权(满足) | ||
| 75 | + boolean approved = oauth2ApproveService.checkForPreApproval(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes); | ||
| 76 | + if (approved) { | ||
| 77 | + // 2.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 | ||
| 78 | + if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { | ||
| 79 | + return success(getAuthorizationCodeRedirect()); | ||
| 80 | + } | ||
| 81 | + return success(getImplicitGrantRedirect()); | ||
| 56 | } | 82 | } |
| 57 | - // 1.2 校验 redirectUri 重定向域名是否合法 | ||
| 58 | 83 | ||
| 59 | - // 1.3 校验 scope 是否在 Client 授权范围内 | 84 | + // 3. 不满足自动授权,则返回授权相关的展示信息 |
| 85 | + return null; | ||
| 86 | + } | ||
| 60 | 87 | ||
| 61 | - // 2.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 | 88 | + @PostMapping("/authorize") |
| 89 | + @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【提交】调用") | ||
| 90 | + @ApiImplicitParams({ | ||
| 91 | + @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class), | ||
| 92 | + @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class), | ||
| 93 | + @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 使用 Map<String, Boolean> 格式,Spring MVC 暂时不支持这么接收参数 | ||
| 94 | + @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), | ||
| 95 | + @ApiImplicitParam(name = "approved", value = "用户是否接受", example = "true", dataTypeClass = Boolean.class), // 该参数为 null 时,会基于用户是否已经授权过,进行自动判断 | ||
| 96 | + @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class) | ||
| 97 | + }) | ||
| 98 | + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | ||
| 99 | + // 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 | ||
| 100 | + public CommonResult<String> approveOrDeny(@RequestParam("response_type") String responseType, | ||
| 101 | + @RequestParam("client_id") String clientId, | ||
| 102 | + @RequestParam(value = "scope", required = false) String scope, | ||
| 103 | + @RequestParam("redirect_uri") String redirectUri, | ||
| 104 | + @RequestParam("approved") Boolean approved, | ||
| 105 | + @RequestParam(value = "state", required = false) String state) { | ||
| 106 | + @SuppressWarnings("unchecked") | ||
| 107 | + Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class); | ||
| 108 | + scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); | ||
| 109 | + // TODO 芋艿:针对 approved + scopes 在看看 spring security 的实现 | ||
| 110 | + // 0. 校验用户已经登录。通过 Spring Security 实现 | ||
| 111 | + | ||
| 112 | + // 1.1 校验 responseType 是否满足 code 或者 token 值 | ||
| 113 | + OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); | ||
| 114 | + // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 | ||
| 115 | + oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); | ||
| 116 | + | ||
| 117 | + // 2.1 | ||
| 118 | + // 2.2 | ||
| 119 | + | ||
| 120 | + // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 | ||
| 121 | + if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { | ||
| 122 | + return success(getAuthorizationCodeRedirect()); | ||
| 123 | + } | ||
| 124 | + // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 | ||
| 125 | + return success(getImplicitGrantRedirect()); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { | ||
| 129 | + if (StrUtil.equals(responseType, "code")) { | ||
| 130 | + return OAuth2GrantTypeEnum.AUTHORIZATION_CODE; | ||
| 131 | + } | ||
| 132 | + if (StrUtil.equalsAny(responseType, "token")) { | ||
| 133 | + return OAuth2GrantTypeEnum.IMPLICIT; | ||
| 134 | + } | ||
| 135 | + throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token"); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + private String getImplicitGrantRedirect() { | ||
| 139 | + return ""; | ||
| 140 | + } | ||
| 62 | 141 | ||
| 63 | - // 2.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 | ||
| 64 | - // TODO 需要确认,是否要 refreshToken 生成 | ||
| 65 | - return CommonResult.success(""); | 142 | + private String getAuthorizationCodeRedirect() { |
| 143 | + return ""; | ||
| 66 | } | 144 | } |
| 67 | 145 | ||
| 68 | } | 146 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2CodeDO.java
| @@ -3,18 +3,21 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth; | @@ -3,18 +3,21 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth; | ||
| 3 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | 3 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | import com.baomidou.mybatisplus.annotation.KeySequence; | 5 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 6 | +import com.baomidou.mybatisplus.annotation.TableField; | ||
| 6 | import com.baomidou.mybatisplus.annotation.TableName; | 7 | import com.baomidou.mybatisplus.annotation.TableName; |
| 8 | +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; | ||
| 7 | import lombok.Data; | 9 | import lombok.Data; |
| 8 | import lombok.EqualsAndHashCode; | 10 | import lombok.EqualsAndHashCode; |
| 9 | 11 | ||
| 10 | import java.util.Date; | 12 | import java.util.Date; |
| 13 | +import java.util.List; | ||
| 11 | 14 | ||
| 12 | /** | 15 | /** |
| 13 | * OAuth2 授权码 DO | 16 | * OAuth2 授权码 DO |
| 14 | * | 17 | * |
| 15 | * @author 芋道源码 | 18 | * @author 芋道源码 |
| 16 | */ | 19 | */ |
| 17 | -@TableName("system_oauth2_code") | 20 | +@TableName(value = "system_oauth2_code", autoResultMap = true) |
| 18 | @KeySequence("system_oauth2_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 | 21 | @KeySequence("system_oauth2_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 |
| 19 | @Data | 22 | @Data |
| 20 | @EqualsAndHashCode(callSuper = true) | 23 | @EqualsAndHashCode(callSuper = true) |
| @@ -41,22 +44,25 @@ public class OAuth2CodeDO extends BaseDO { | @@ -41,22 +44,25 @@ public class OAuth2CodeDO extends BaseDO { | ||
| 41 | /** | 44 | /** |
| 42 | * 客户端编号 | 45 | * 客户端编号 |
| 43 | * | 46 | * |
| 44 | - * 关联 {@link OAuth2ClientDO#getId()} | 47 | + * 关联 {@link OAuth2ClientDO#getClientId()} |
| 45 | */ | 48 | */ |
| 46 | private String clientId; | 49 | private String clientId; |
| 47 | /** | 50 | /** |
| 48 | - * 刷新令牌 | ||
| 49 | - * | ||
| 50 | - * 关联 {@link OAuth2RefreshTokenDO#getRefreshToken()} | 51 | + * 授权范围 |
| 51 | */ | 52 | */ |
| 52 | - private String refreshToken; | 53 | + @TableField(typeHandler = JacksonTypeHandler.class) |
| 54 | + private List<String> scopes; | ||
| 53 | /** | 55 | /** |
| 54 | - * 过期时间 | 56 | + * 重定向地址 |
| 55 | */ | 57 | */ |
| 56 | - private Date expiresTime; | 58 | + private String redirectUri; |
| 57 | /** | 59 | /** |
| 58 | - * 创建 IP | 60 | + * 状态 |
| 59 | */ | 61 | */ |
| 60 | - private String createIp; | 62 | + private String state; |
| 63 | + /** | ||
| 64 | + * 过期时间 | ||
| 65 | + */ | ||
| 66 | + private Date expiresTime; | ||
| 61 | 67 | ||
| 62 | } | 68 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminOAuth2Service.java
| 1 | package cn.iocoder.yudao.module.system.service.auth; | 1 | package cn.iocoder.yudao.module.system.service.auth; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; | ||
| 4 | + | ||
| 5 | +import java.util.Collection; | ||
| 6 | + | ||
| 3 | /** | 7 | /** |
| 4 | * 管理后台的 OAuth2 Service 接口 | 8 | * 管理后台的 OAuth2 Service 接口 |
| 5 | * | 9 | * |
| @@ -11,4 +15,14 @@ package cn.iocoder.yudao.module.system.service.auth; | @@ -11,4 +15,14 @@ package cn.iocoder.yudao.module.system.service.auth; | ||
| 11 | * @author 芋道源码 | 15 | * @author 芋道源码 |
| 12 | */ | 16 | */ |
| 13 | public interface AdminOAuth2Service { | 17 | public interface AdminOAuth2Service { |
| 18 | + | ||
| 19 | + // ImplicitTokenGranter | ||
| 20 | + OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, | ||
| 21 | + String clientId, Collection<String> scopes); | ||
| 22 | + | ||
| 23 | + // AuthorizationCodeTokenGranter | ||
| 24 | + String grantAuthorizationCode(Long userId, Integer userType, | ||
| 25 | + String clientId, Collection<String> scopes, | ||
| 26 | + String redirectUri, String state); | ||
| 27 | + | ||
| 14 | } | 28 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ApproveService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.system.service.auth; | ||
| 2 | + | ||
| 3 | +import java.util.Collection; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * OAuth2 批准 Service 接口 | ||
| 7 | + * | ||
| 8 | + * 从功能上,和 Spring Security OAuth 的 ApprovalStoreUserApprovalHandler 的功能,记录用户针对指定客户端的授权,减少手动确定。 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +public interface OAuth2ApproveService { | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * 获得指定用户,针对指定客户端的指定授权,是否通过 | ||
| 16 | + * | ||
| 17 | + * 参考 ApprovalStoreUserApprovalHandler 的 checkForPreApproval 方法 | ||
| 18 | + * | ||
| 19 | + * @param userId 用户编号 | ||
| 20 | + * @param userType 用户类型 | ||
| 21 | + * @param clientId 客户端编号 | ||
| 22 | + * @param scopes 授权范围 | ||
| 23 | + * @return 是否授权通过 | ||
| 24 | + */ | ||
| 25 | + boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection<String> scopes); | ||
| 26 | + | ||
| 27 | +} |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ApproveServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.system.service.auth; | ||
| 2 | + | ||
| 3 | +import org.springframework.stereotype.Service; | ||
| 4 | + | ||
| 5 | +import java.util.Collection; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * OAuth2 批准 Service 实现类 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +@Service | ||
| 13 | +public class OAuth2ApproveServiceImpl implements OAuth2ApproveService { | ||
| 14 | + | ||
| 15 | + @Override | ||
| 16 | + public boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection<String> scopes) { | ||
| 17 | + return false; | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | +} |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientService.java
| @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2Clie | @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2Clie | ||
| 7 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; | 7 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; |
| 8 | 8 | ||
| 9 | import javax.validation.Valid; | 9 | import javax.validation.Valid; |
| 10 | +import java.util.Collection; | ||
| 10 | 11 | ||
| 11 | /** | 12 | /** |
| 12 | * OAuth2.0 Client Service 接口 | 13 | * OAuth2.0 Client Service 接口 |
| @@ -63,9 +64,24 @@ public interface OAuth2ClientService { | @@ -63,9 +64,24 @@ public interface OAuth2ClientService { | ||
| 63 | /** | 64 | /** |
| 64 | * 从缓存中,校验客户端是否合法 | 65 | * 从缓存中,校验客户端是否合法 |
| 65 | * | 66 | * |
| 67 | + * @return 客户端 | ||
| 68 | + */ | ||
| 69 | + default OAuth2ClientDO validOAuthClientFromCache(String clientId) { | ||
| 70 | + return validOAuthClientFromCache(clientId, null, null, null); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 从缓存中,校验客户端是否合法 | ||
| 75 | + * | ||
| 76 | + * 非空时,进行校验 | ||
| 77 | + * | ||
| 66 | * @param clientId 客户端编号 | 78 | * @param clientId 客户端编号 |
| 79 | + * @param authorizedGrantType 授权方式 | ||
| 80 | + * @param scopes 授权范围 | ||
| 81 | + * @param redirectUri 重定向地址 | ||
| 67 | * @return 客户端 | 82 | * @return 客户端 |
| 68 | */ | 83 | */ |
| 69 | - OAuth2ClientDO validOAuthClientFromCache(String clientId); | 84 | + OAuth2ClientDO validOAuthClientFromCache(String clientId, String authorizedGrantType, |
| 85 | + Collection<String> scopes, String redirectUri); | ||
| 70 | 86 | ||
| 71 | } | 87 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImpl.java
| 1 | package cn.iocoder.yudao.module.system.service.auth; | 1 | package cn.iocoder.yudao.module.system.service.auth; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 7 | +import cn.iocoder.yudao.framework.common.util.string.StrUtils; | ||
| 5 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO; | 8 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO; |
| 6 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO; | 9 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO; |
| 7 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO; | 10 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO; |
| @@ -18,15 +21,12 @@ import org.springframework.validation.annotation.Validated; | @@ -18,15 +21,12 @@ import org.springframework.validation.annotation.Validated; | ||
| 18 | 21 | ||
| 19 | import javax.annotation.PostConstruct; | 22 | import javax.annotation.PostConstruct; |
| 20 | import javax.annotation.Resource; | 23 | import javax.annotation.Resource; |
| 21 | -import java.util.Date; | ||
| 22 | -import java.util.List; | ||
| 23 | -import java.util.Map; | 24 | +import java.util.*; |
| 24 | 25 | ||
| 25 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 26 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 26 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; | 27 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; |
| 27 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; | 28 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; |
| 28 | -import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_EXISTS; | ||
| 29 | -import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS; | 29 | +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; |
| 30 | 30 | ||
| 31 | /** | 31 | /** |
| 32 | * OAuth2.0 Client Service 实现类 | 32 | * OAuth2.0 Client Service 实现类 |
| @@ -175,8 +175,29 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService { | @@ -175,8 +175,29 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService { | ||
| 175 | } | 175 | } |
| 176 | 176 | ||
| 177 | @Override | 177 | @Override |
| 178 | - public OAuth2ClientDO validOAuthClientFromCache(String clientId) { | ||
| 179 | - return clientCache.get(clientId); | 178 | + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String authorizedGrantType, Collection<String> scopes, String redirectUri) { |
| 179 | + // 校验客户端存在、且开启 | ||
| 180 | + OAuth2ClientDO client = clientCache.get(clientId); | ||
| 181 | + if (client == null) { | ||
| 182 | + throw exception(OAUTH2_CLIENT_EXISTS); | ||
| 183 | + } | ||
| 184 | + if (Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { | ||
| 185 | + throw exception(OAUTH2_CLIENT_DISABLE); | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + // 校验授权方式 | ||
| 189 | + if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) { | ||
| 190 | + throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); | ||
| 191 | + } | ||
| 192 | + // 校验授权范围 | ||
| 193 | + if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScopes(), scopes)) { | ||
| 194 | + throw exception(OAUTH2_CLIENT_SCOPE_OVER); | ||
| 195 | + } | ||
| 196 | + // 校验回调地址 | ||
| 197 | + if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) { | ||
| 198 | + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH); | ||
| 199 | + } | ||
| 200 | + return client; | ||
| 180 | } | 201 | } |
| 181 | 202 | ||
| 182 | } | 203 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2CodeService.java
yudao-ui-admin/src/api/login.js
| @@ -122,6 +122,14 @@ export function authorize() { | @@ -122,6 +122,14 @@ export function authorize() { | ||
| 122 | response_type: 'code', | 122 | response_type: 'code', |
| 123 | client_id: 'test', | 123 | client_id: 'test', |
| 124 | redirect_uri: 'https://www.iocoder.cn', | 124 | redirect_uri: 'https://www.iocoder.cn', |
| 125 | + // scopes: { | ||
| 126 | + // read: true, | ||
| 127 | + // write: false | ||
| 128 | + // } | ||
| 129 | + scope: { | ||
| 130 | + read: true, | ||
| 131 | + write: false | ||
| 132 | + } | ||
| 125 | }, | 133 | }, |
| 126 | method: 'post' | 134 | method: 'post' |
| 127 | }) | 135 | }) |