Commit 82cf4e1775beeddbd0a08066e4414289a593f364

Authored by 芋道源码
Committed by Gitee
2 parents 14d4d8ca 66855135

!176 基于 OAuth2.0 实现 SSO 单点登录

Merge pull request !176 from 芋道源码/feature/1.6.2
Showing 81 changed files with 2117 additions and 249 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
... ... @@ -68,6 +68,13 @@ public class CollectionUtils {
68 68 return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
69 69 }
70 70  
  71 + public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
  72 + if (CollUtil.isEmpty(from)) {
  73 + return new HashSet<>();
  74 + }
  75 + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
  76 + }
  77 +
71 78 public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
72 79 if (CollUtil.isEmpty(from)) {
73 80 return new HashMap<>();
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
1 1 package cn.iocoder.yudao.framework.common.util.http;
2 2  
  3 +import cn.hutool.core.codec.Base64;
3 4 import cn.hutool.core.map.TableMap;
4 5 import cn.hutool.core.net.url.UrlBuilder;
5 6 import cn.hutool.core.util.ReflectUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import org.springframework.util.StringUtils;
  9 +import org.springframework.web.util.UriComponents;
  10 +import org.springframework.web.util.UriComponentsBuilder;
6 11  
  12 +import javax.servlet.http.HttpServletRequest;
  13 +import java.net.URI;
7 14 import java.nio.charset.Charset;
  15 +import java.util.Map;
8 16  
9 17 /**
10 18 * HTTP 工具类
... ... @@ -25,4 +33,94 @@ public class HttpUtils {
25 33 return builder.build();
26 34 }
27 35  
  36 + private String append(String base, Map<String, ?> query, boolean fragment) {
  37 + return append(base, query, null, fragment);
  38 + }
  39 +
  40 + /**
  41 + * 拼接 URL
  42 + *
  43 + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
  44 + *
  45 + * @param base 基础 URL
  46 + * @param query 查询参数
  47 + * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
  48 + * @param fragment URL 的 fragment,即拼接到 # 中
  49 + * @return 拼接后的 URL
  50 + */
  51 + public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
  52 + UriComponentsBuilder template = UriComponentsBuilder.newInstance();
  53 + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
  54 + URI redirectUri;
  55 + try {
  56 + // assume it's encoded to start with (if it came in over the wire)
  57 + redirectUri = builder.build(true).toUri();
  58 + } catch (Exception e) {
  59 + // ... but allow client registrations to contain hard-coded non-encoded values
  60 + redirectUri = builder.build().toUri();
  61 + builder = UriComponentsBuilder.fromUri(redirectUri);
  62 + }
  63 + template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
  64 + .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
  65 +
  66 + if (fragment) {
  67 + StringBuilder values = new StringBuilder();
  68 + if (redirectUri.getFragment() != null) {
  69 + String append = redirectUri.getFragment();
  70 + values.append(append);
  71 + }
  72 + for (String key : query.keySet()) {
  73 + if (values.length() > 0) {
  74 + values.append("&");
  75 + }
  76 + String name = key;
  77 + if (keys != null && keys.containsKey(key)) {
  78 + name = keys.get(key);
  79 + }
  80 + values.append(name).append("={").append(key).append("}");
  81 + }
  82 + if (values.length() > 0) {
  83 + template.fragment(values.toString());
  84 + }
  85 + UriComponents encoded = template.build().expand(query).encode();
  86 + builder.fragment(encoded.getFragment());
  87 + } else {
  88 + for (String key : query.keySet()) {
  89 + String name = key;
  90 + if (keys != null && keys.containsKey(key)) {
  91 + name = keys.get(key);
  92 + }
  93 + template.queryParam(name, "{" + key + "}");
  94 + }
  95 + template.fragment(redirectUri.getFragment());
  96 + UriComponents encoded = template.build().expand(query).encode();
  97 + builder.query(encoded.getQuery());
  98 + }
  99 + return builder.build().toUriString();
  100 + }
  101 +
  102 + public static String[] obtainBasicAuthorization(HttpServletRequest request) {
  103 + String clientId;
  104 + String clientSecret;
  105 + // 先从 Header 中获取
  106 + String authorization = request.getHeader("Authorization");
  107 + authorization = StrUtil.subAfter(authorization, "Basic ", true);
  108 + if (StringUtils.hasText(authorization)) {
  109 + authorization = Base64.decodeStr(authorization);
  110 + clientId = StrUtil.subBefore(authorization, ":", false);
  111 + clientSecret = StrUtil.subAfter(authorization, ":", false);
  112 + // 再从 Param 中获取
  113 + } else {
  114 + clientId = request.getParameter("client_id");
  115 + clientSecret = request.getParameter("client_secret");
  116 + }
  117 +
  118 + // 如果两者非空,则返回
  119 + if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
  120 + return new String[]{clientId, clientSecret};
  121 + }
  122 + return null;
  123 + }
  124 +
  125 +
28 126 }
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java
1 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 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 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-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
... ... @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
6 6 import lombok.Data;
7 7  
8 8 import java.util.HashMap;
  9 +import java.util.List;
9 10 import java.util.Map;
10 11  
11 12 /**
... ... @@ -30,6 +31,10 @@ public class LoginUser {
30 31 * 租户编号
31 32 */
32 33 private Long tenantId;
  34 + /**
  35 + * 授权范围
  36 + */
  37 + private List<String> scopes;
33 38  
34 39 // ========== 上下文 ==========
35 40 /**
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
... ... @@ -79,7 +79,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
79 79 }
80 80 // 构建登录用户
81 81 return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
82   - .setTenantId(accessToken.getTenantId());
  82 + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
83 83 } catch (ServiceException serviceException) {
84 84 // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
85 85 return null;
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java
... ... @@ -41,4 +41,19 @@ public interface SecurityFrameworkService {
41 41 */
42 42 boolean hasAnyRoles(String... roles);
43 43  
  44 + /**
  45 + * 判断是否有授权
  46 + *
  47 + * @param scope 授权
  48 + * @return 是否
  49 + */
  50 + boolean hasScope(String scope);
  51 +
  52 + /**
  53 + * 判断是否有授权范围,任一一个即可
  54 + *
  55 + * @param scope 授权范围数组
  56 + * @return 是否
  57 + */
  58 + boolean hasAnyScopes(String... scope);
44 59 }
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java
1 1 package cn.iocoder.yudao.framework.security.core.service;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  5 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
3 6 import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
4 7 import lombok.AllArgsConstructor;
5 8  
  9 +import java.util.Arrays;
  10 +
6 11 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
7 12  
8 13 /**
... ... @@ -35,4 +40,18 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
35 40 return permissionApi.hasAnyRoles(getLoginUserId(), roles);
36 41 }
37 42  
  43 + @Override
  44 + public boolean hasScope(String scope) {
  45 + return hasAnyScopes(scope);
  46 + }
  47 +
  48 + @Override
  49 + public boolean hasAnyScopes(String... scope) {
  50 + LoginUser user = SecurityFrameworkUtils.getLoginUser();
  51 + if (user == null) {
  52 + return false;
  53 + }
  54 + return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope));
  55 + }
  56 +
38 57 }
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
... ... @@ -20,6 +20,8 @@ import java.util.Collections;
20 20 */
21 21 public class SecurityFrameworkUtils {
22 22  
  23 + public static final String AUTHORIZATION_BEARER = "Bearer";
  24 +
23 25 private SecurityFrameworkUtils() {}
24 26  
25 27 /**
... ... @@ -34,7 +36,7 @@ public class SecurityFrameworkUtils {
34 36 if (!StringUtils.hasText(authorization)) {
35 37 return null;
36 38 }
37   - int index = authorization.indexOf("Bearer ");
  39 + int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
38 40 if (index == -1) { // 未找到
39 41 return null;
40 42 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.api.auth.dto;
3 3 import lombok.Data;
4 4  
5 5 import java.io.Serializable;
  6 +import java.util.List;
6 7  
7 8 /**
8 9 * OAuth2.0 访问令牌的校验 Response DTO
... ... @@ -24,5 +25,9 @@ public class OAuth2AccessTokenCheckRespDTO implements Serializable {
24 25 * 租户编号
25 26 */
26 27 private Long tenantId;
  28 + /**
  29 + * 授权范围的数组
  30 + */
  31 + private List<String> scopes;
27 32  
28 33 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java
... ... @@ -6,6 +6,7 @@ import lombok.Data;
6 6  
7 7 import javax.validation.constraints.NotNull;
8 8 import java.io.Serializable;
  9 +import java.util.List;
9 10  
10 11 /**
11 12 * OAuth2.0 访问令牌创建 Request DTO
... ... @@ -31,5 +32,9 @@ public class OAuth2AccessTokenCreateReqDTO implements Serializable {
31 32 */
32 33 @NotNull(message = "客户端编号不能为空")
33 34 private String clientId;
  35 + /**
  36 + * 授权范围
  37 + */
  38 + private List<String> scopes;
34 39  
35 40 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
... ... @@ -123,8 +123,23 @@ public interface ErrorCodeConstants {
123 123 ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1002019000, "系统敏感词在所有标签中都不存在");
124 124 ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1002019001, "系统敏感词已在标签中存在");
125 125  
126   - // ========== 系统敏感词 1002020000 =========
  126 + // ========== OAuth2 客户端 1002020000 =========
127 127 ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在");
128 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(1002020005, "无效 redirect_uri: {}");
  133 + ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1002020006, "无效 client_secret: {}");
  134 +
  135 + // ========== OAuth2 授权 1002021000 =========
  136 + ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1002021000, "client_id 不匹配");
  137 + ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1002021001, "redirect_uri 不匹配");
  138 + ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1002021002, "state 不匹配");
  139 + ErrorCode OAUTH2_GRANT_CODE_NOT_EXISTS = new ErrorCode(1002021003, "code 不存在");
  140 +
  141 + // ========== OAuth2 授权 1002022000 =========
  142 + ErrorCode OAUTH2_CODE_NOT_EXISTS = new ErrorCode(1002022000, "code 不存在");
  143 + ErrorCode OAUTH2_CODE_EXPIRE = new ErrorCode(1002022000, "code 已过期");
129 144  
130 145 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java
1 1 package cn.iocoder.yudao.module.system.enums.auth;
2 2  
  3 +import cn.hutool.core.util.ArrayUtil;
3 4 import lombok.AllArgsConstructor;
4 5 import lombok.Getter;
5 6  
... ... @@ -21,4 +22,8 @@ public enum OAuth2GrantTypeEnum {
21 22  
22 23 private final String grantType;
23 24  
  25 + public static OAuth2GrantTypeEnum getByGranType(String grantType) {
  26 + return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values());
  27 + }
  28 +
24 29 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java
... ... @@ -4,8 +4,8 @@ import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO
4 4 import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO;
5 5 import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO;
6 6 import cn.iocoder.yudao.module.system.convert.auth.OAuth2TokenConvert;
7   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
8   -import cn.iocoder.yudao.module.system.service.auth.OAuth2TokenService;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  8 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
9 9 import org.springframework.stereotype.Service;
10 10  
11 11 import javax.annotation.Resource;
... ... @@ -24,7 +24,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi {
24 24 @Override
25 25 public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) {
26 26 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(
27   - reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId());
  27 + reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes());
28 28 return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO);
29 29 }
30 30  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
... ... @@ -6,7 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 6 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
7 7 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
8 8 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
9   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
  9 +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
10 10 import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
11 11 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
12 12 import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2Controller.java deleted
1   -package cn.iocoder.yudao.module.system.controller.admin.auth;
2   -
3   -import io.swagger.annotations.Api;
4   -import lombok.extern.slf4j.Slf4j;
5   -import org.springframework.validation.annotation.Validated;
6   -import org.springframework.web.bind.annotation.RequestMapping;
7   -import org.springframework.web.bind.annotation.RestController;
8   -
9   -@Api(tags = "管理后台 - OAuth2.0 授权")
10   -@RestController
11   -@RequestMapping("/system/oauth2")
12   -@Validated
13   -@Slf4j
14   -public class OAuth2Controller {
15   -
16   -// POST oauth/token TokenEndpoint:Password、Implicit、Code、Refresh Token
17   -
18   -// POST oauth/check_token CheckTokenEndpoint
19   -
20   -// DELETE oauth/token ConsumerTokenServices#revokeToken
21   -
22   -// GET oauth/authorize AuthorizationEndpoint
23   -
24   -}
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthLoginReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthLoginRespVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginRespVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthMenuRespVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthMenuRespVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthPermissionInfoRespVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthSmsLoginReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthSmsSendReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
4 4 import cn.iocoder.yudao.framework.common.validation.Mobile;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthSocialBindLoginReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialBindLoginReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
4 4 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthSocialQuickLoginReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialQuickLoginReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.auth.vo;
2 2  
3 3 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
4 4 import cn.iocoder.yudao.framework.common.validation.InEnum;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2ClientController.http renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2ClientController.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -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;
7   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientRespVO;
8   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO;
  5 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO;
  7 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO;
  8 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO;
9 9 import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert;
10   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
11   -import cn.iocoder.yudao.module.system.service.auth.OAuth2ClientService;
  10 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
  11 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
12 12 import io.swagger.annotations.Api;
13 13 import io.swagger.annotations.ApiImplicitParam;
14 14 import io.swagger.annotations.ApiOperation;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http 0 → 100644
  1 +### 请求 /system/oauth2/authorize 接口 => 成功
  2 +GET {{baseUrl}}/system/oauth2/authorize?clientId=default
  3 +Authorization: Bearer {{token}}
  4 +tenant-id: {{adminTenentId}}
  5 +
  6 +### 请求 /system/oauth2/authorize + token 接口 => 成功
  7 +POST {{baseUrl}}/system/oauth2/authorize
  8 +Content-Type: application/x-www-form-urlencoded
  9 +Authorization: Bearer {{token}}
  10 +tenant-id: {{adminTenentId}}
  11 +
  12 +response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true
  13 +
  14 +### 请求 /system/oauth2/authorize + code 接口 => 成功
  15 +POST {{baseUrl}}/system/oauth2/authorize
  16 +Content-Type: application/x-www-form-urlencoded
  17 +Authorization: Bearer {{token}}
  18 +tenant-id: {{adminTenentId}}
  19 +
  20 +response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false
  21 +
  22 +### 请求 /system/oauth2/token + code 接口 => 成功
  23 +POST {{baseUrl}}/system/oauth2/token
  24 +Content-Type: application/x-www-form-urlencoded
  25 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
  26 +tenant-id: {{adminTenentId}}
  27 +
  28 +grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a
  29 +
  30 +### 请求 /system/oauth2/token + password 接口 => 成功
  31 +POST {{baseUrl}}/system/oauth2/token
  32 +Content-Type: application/x-www-form-urlencoded
  33 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
  34 +tenant-id: {{adminTenentId}}
  35 +
  36 +grant_type=password&username=admin&password=admin123&scope=user.read
  37 +
  38 +### 请求 /system/oauth2/token + refresh_token 接口 => 成功
  39 +POST {{baseUrl}}/system/oauth2/token
  40 +Content-Type: application/x-www-form-urlencoded
  41 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
  42 +tenant-id: {{adminTenentId}}
  43 +
  44 +grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588
  45 +
  46 +### 请求 /system/oauth2/token + DELETE 接口 => 成功
  47 +DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596
  48 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
  49 +tenant-id: {{adminTenentId}}
  50 +
  51 +### 请求 /system/oauth2/check-token 接口 => 成功
  52 +POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106
  53 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
  54 +tenant-id: {{adminTenentId}}
  55 +
  56 +### 请求 /system/oauth2/user/get 接口 => 成功
  57 +GET {{baseUrl}}/system/oauth2/user/get
  58 +Authorization: Bearer 9502bd7a768a4ade920b90f41e2efd5c
  59 +tenant-id: {{adminTenentId}}
  60 +
  61 +### 请求 /system/oauth2/user/update 接口 => 成功
  62 +PUT {{baseUrl}}/system/oauth2/user/update
  63 +Content-Type: application/json
  64 +Authorization: Bearer 9502bd7a768a4ade920b90f41e2efd5c
  65 +tenant-id: {{adminTenentId}}
  66 +
  67 +{
  68 + "nickname": "芋道源码"
  69 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.ArrayUtil;
  6 +import cn.hutool.core.util.ObjectUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  9 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  10 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  11 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  12 +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
  13 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
  14 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
  15 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
  16 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
  17 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserUpdateReqVO;
  18 +import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert;
  19 +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
  20 +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
  21 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  22 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
  23 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
  24 +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
  25 +import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
  26 +import cn.iocoder.yudao.module.system.service.dept.DeptService;
  27 +import cn.iocoder.yudao.module.system.service.dept.PostService;
  28 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService;
  29 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
  30 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService;
  31 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
  32 +import cn.iocoder.yudao.module.system.service.user.AdminUserService;
  33 +import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
  34 +import io.swagger.annotations.Api;
  35 +import io.swagger.annotations.ApiImplicitParam;
  36 +import io.swagger.annotations.ApiImplicitParams;
  37 +import io.swagger.annotations.ApiOperation;
  38 +import lombok.extern.slf4j.Slf4j;
  39 +import org.springframework.security.access.prepost.PreAuthorize;
  40 +import org.springframework.validation.annotation.Validated;
  41 +import org.springframework.web.bind.annotation.*;
  42 +
  43 +import javax.annotation.Resource;
  44 +import javax.servlet.http.HttpServletRequest;
  45 +import javax.validation.Valid;
  46 +import java.util.Collections;
  47 +import java.util.List;
  48 +import java.util.Map;
  49 +
  50 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
  51 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
  52 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  53 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  54 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  55 +
  56 +/**
  57 + * 提供给外部应用调用为主
  58 + *
  59 + * 一般来说,管理后台的 /system-api/* 是不直接提供给外部应用使用,主要是外部应用能够访问的数据与接口是有限的,而管理后台的 RBAC 无法很好的控制。
  60 + * 参考大量的开放平台,都是独立的一套 OpenAPI,对应到【本系统】就是在 Controller 下新建 open 包,实现 /open-api/* 接口,然后通过 scope 进行控制。
  61 + * 另外,一个公司如果有多个管理后台,它们 client_id 产生的 access token 相互之间是无法互通的,即无法访问它们系统的 API 接口,直到两个 client_id 产生信任授权。
  62 + *
  63 + * 考虑到【本系统】暂时不想做的过于复杂,默认只有获取到 access token 之后,可以访问【本系统】管理后台的 /system-api/* 所有接口,除非手动添加 scope 控制。
  64 + * scope 的使用示例,可见当前类的 getUserInfo 和 updateUserInfo 方法,上面有 @PreAuthorize("@ss.hasScope('user.read')") 和 @PreAuthorize("@ss.hasScope('user.write')") 注解
  65 + *
  66 + * @author 芋道源码
  67 + */
  68 +@Api(tags = "管理后台 - OAuth2.0 授权")
  69 +@RestController
  70 +@RequestMapping("/system/oauth2")
  71 +@Validated
  72 +@Slf4j
  73 +public class OAuth2OpenController {
  74 +
  75 + @Resource
  76 + private OAuth2GrantService oauth2GrantService;
  77 + @Resource
  78 + private OAuth2ClientService oauth2ClientService;
  79 + @Resource
  80 + private OAuth2ApproveService oauth2ApproveService;
  81 + @Resource
  82 + private OAuth2TokenService oauth2TokenService;
  83 +
  84 + /**
  85 + * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法
  86 + *
  87 + * 授权码 authorization_code 模式时:code + redirectUri + state 参数
  88 + * 密码 password 模式时:username + password + scope 参数
  89 + * 刷新 refresh_token 模式时:refreshToken 参数
  90 + * 客户端 client_credentials 模式:scope 参数
  91 + * 简化 implicit 模式时:不支持
  92 + *
  93 + * 注意,默认需要传递 client_id + client_secret 参数
  94 + */
  95 + @PostMapping("/token")
  96 + @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
  97 + @ApiImplicitParams({
  98 + @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class),
  99 + @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class),
  100 + @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class),
  101 + @ApiImplicitParam(name = "username", example = "tudou", dataTypeClass = String.class),
  102 + @ApiImplicitParam(name = "password", example = "cai", dataTypeClass = String.class), // 多个使用空格分隔
  103 + @ApiImplicitParam(name = "scope", example = "user_info", dataTypeClass = String.class)
  104 + })
  105 + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  106 + public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request,
  107 + @RequestParam("grant_type") String grantType,
  108 + @RequestParam(value = "code", required = false) String code, // 授权码模式
  109 + @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式
  110 + @RequestParam(value = "state", required = false) String state, // 授权码模式
  111 + @RequestParam(value = "username", required = false) String username, // 密码模式
  112 + @RequestParam(value = "password", required = false) String password, // 密码模式
  113 + @RequestParam(value = "scope", required = false) String scope, // 密码模式
  114 + @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
  115 + List<String> scopes = OAuth2Utils.buildScopes(scope);
  116 + // 授权类型
  117 + OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
  118 + if (grantTypeEnum == null) {
  119 + throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
  120 + }
  121 + if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
  122 + throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
  123 + }
  124 +
  125 + // 校验客户端
  126 + String[] clientIdAndSecret = obtainBasicAuthorization(request);
  127 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
  128 + grantType, scopes, redirectUri);
  129 +
  130 + // 根据授权模式,获取访问令牌
  131 + OAuth2AccessTokenDO accessTokenDO;
  132 + switch (grantTypeEnum) {
  133 + case AUTHORIZATION_CODE:
  134 + accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state);
  135 + break;
  136 + case PASSWORD:
  137 + accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes);
  138 + break;
  139 + case CLIENT_CREDENTIALS:
  140 + accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes);
  141 + break;
  142 + case REFRESH_TOKEN:
  143 + accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId());
  144 + break;
  145 + default:
  146 + throw new IllegalArgumentException("未知授权类型:" + grantType);
  147 + }
  148 + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
  149 + return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO));
  150 + }
  151 +
  152 + @DeleteMapping("/token")
  153 + @ApiOperation(value = "删除访问令牌")
  154 + @ApiImplicitParam(name = "token", required = true, value = "访问令牌", example = "biu", dataTypeClass = String.class)
  155 + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  156 + public CommonResult<Boolean> revokeToken(HttpServletRequest request,
  157 + @RequestParam("token") String token) {
  158 + // 校验客户端
  159 + String[] clientIdAndSecret = obtainBasicAuthorization(request);
  160 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
  161 + null, null, null);
  162 +
  163 + // 删除访问令牌
  164 + return success(oauth2GrantService.revokeToken(client.getClientId(), token));
  165 + }
  166 +
  167 + /**
  168 + * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法
  169 + */
  170 + @PostMapping("/check-token")
  171 + @ApiOperation(value = "校验访问令牌")
  172 + @ApiImplicitParam(name = "token", required = true, value = "访问令牌", example = "biu", dataTypeClass = String.class)
  173 + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  174 + public CommonResult<OAuth2OpenCheckTokenRespVO> checkToken(HttpServletRequest request,
  175 + @RequestParam("token") String token) {
  176 + // 校验客户端
  177 + String[] clientIdAndSecret = obtainBasicAuthorization(request);
  178 + oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
  179 + null, null, null);
  180 +
  181 + // 校验令牌
  182 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(token);
  183 + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
  184 + return success(OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO));
  185 + }
  186 +
  187 + /**
  188 + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法
  189 + */
  190 + @GetMapping("/authorize")
  191 + @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
  192 + @ApiImplicitParam(name = "clientId", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class)
  193 + public CommonResult<OAuth2OpenAuthorizeInfoRespVO> authorize(@RequestParam("clientId") String clientId) {
  194 + // 0. 校验用户已经登录。通过 Spring Security 实现
  195 +
  196 + // 1. 获得 Client 客户端的信息
  197 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
  198 + null, null, null);
  199 + // 2. 获得用户已经授权的信息
  200 + List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
  201 + // 拼接返回
  202 + return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
  203 + }
  204 +
  205 + /**
  206 + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法
  207 + *
  208 + * 场景一:【自动授权 autoApprove = true】
  209 + * 刚进入 sso.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权
  210 + * 场景二:【手动授权 autoApprove = false】
  211 + * 在 sso.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false
  212 + *
  213 + * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理
  214 + */
  215 + @PostMapping("/authorize")
  216 + @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【提交】调用")
  217 + @ApiImplicitParams({
  218 + @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class),
  219 + @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class),
  220 + @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 使用 Map<String, Boolean> 格式,Spring MVC 暂时不支持这么接收参数
  221 + @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class),
  222 + @ApiImplicitParam(name = "autoApprove", required = true, value = "用户是否接受", example = "true", dataTypeClass = Boolean.class),
  223 + @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class)
  224 + })
  225 + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  226 + public CommonResult<String> approveOrDeny(@RequestParam("response_type") String responseType,
  227 + @RequestParam("client_id") String clientId,
  228 + @RequestParam(value = "scope", required = false) String scope,
  229 + @RequestParam("redirect_uri") String redirectUri,
  230 + @RequestParam(value = "auto_approve") Boolean autoApprove,
  231 + @RequestParam(value = "state", required = false) String state) {
  232 + @SuppressWarnings("unchecked")
  233 + Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
  234 + scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
  235 + // TODO 芋艿:针对 approved + scopes 在看看 spring security 的实现
  236 + // 0. 校验用户已经登录。通过 Spring Security 实现
  237 +
  238 + // 1.1 校验 responseType 是否满足 code 或者 token 值
  239 + OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);
  240 + // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
  241 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
  242 + grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
  243 +
  244 + // 2.1 假设 approved 为 null,说明是场景一
  245 + if (Boolean.TRUE.equals(autoApprove)) {
  246 + // 如果无法自动授权通过,则返回空 url,前端不进行跳转
  247 + if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) {
  248 + return success(null);
  249 + }
  250 + } else { // 2.2 假设 approved 非 null,说明是场景二
  251 + // 如果计算后不通过,则跳转一个错误链接
  252 + if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) {
  253 + return success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state,
  254 + "access_denied", "User denied access"));
  255 + }
  256 + }
  257 +
  258 + // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向
  259 + List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue);
  260 + if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
  261 + return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
  262 + }
  263 + // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向
  264 + return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
  265 + }
  266 +
  267 + private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) {
  268 + if (StrUtil.equals(responseType, "code")) {
  269 + return OAuth2GrantTypeEnum.AUTHORIZATION_CODE;
  270 + }
  271 + if (StrUtil.equalsAny(responseType, "token")) {
  272 + return OAuth2GrantTypeEnum.IMPLICIT;
  273 + }
  274 + throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token");
  275 + }
  276 +
  277 + private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client,
  278 + List<String> scopes, String redirectUri, String state) {
  279 + // 1. 创建 access token 访问令牌
  280 + OAuth2AccessTokenDO accessTokenDO = oauth2GrantService.grantImplicit(userId, getUserType(), client.getClientId(), scopes);
  281 + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
  282 + // 2. 拼接重定向的 URL
  283 + // noinspection unchecked
  284 + return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(),
  285 + scopes, JsonUtils.parseObject(client.getAdditionalInformation(), Map.class));
  286 + }
  287 +
  288 + private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client,
  289 + List<String> scopes, String redirectUri, String state) {
  290 + // 1. 创建 code 授权码
  291 + String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId,getUserType(), client.getClientId(), scopes,
  292 + redirectUri, state);
  293 + // 2. 拼接重定向的 URL
  294 + return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state);
  295 + }
  296 +
  297 + private Integer getUserType() {
  298 + return UserTypeEnum.ADMIN.getValue();
  299 + }
  300 +
  301 + private String[] obtainBasicAuthorization(HttpServletRequest request) {
  302 + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request);
  303 + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) {
  304 + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递");
  305 + }
  306 + return clientIdAndSecret;
  307 + }
  308 +
  309 + // ============ 用户操作的示例,展示 scope 的使用 ============
  310 +
  311 + @Resource
  312 + private AdminUserService userService;
  313 + @Resource
  314 + private DeptService deptService;
  315 + @Resource
  316 + private PostService postService;
  317 +
  318 + @GetMapping("/user/get")
  319 + @ApiOperation("获得用户基本信息")
  320 + @PreAuthorize("@ss.hasScope('user.read')")
  321 + public CommonResult<OAuth2OpenUserInfoRespVO> getUserInfo() {
  322 + // 获得用户基本信息
  323 + AdminUserDO user = userService.getUser(getLoginUserId());
  324 + OAuth2OpenUserInfoRespVO resp = OAuth2OpenConvert.INSTANCE.convert(user);
  325 + // 获得部门信息
  326 + if (user.getDeptId() != null) {
  327 + DeptDO dept = deptService.getDept(user.getDeptId());
  328 + resp.setDept(OAuth2OpenConvert.INSTANCE.convert(dept));
  329 + }
  330 + // 获得岗位信息
  331 + if (CollUtil.isNotEmpty(user.getPostIds())) {
  332 + List<PostDO> posts = postService.getPosts(user.getPostIds());
  333 + resp.setPosts(OAuth2OpenConvert.INSTANCE.convertList(posts));
  334 + }
  335 + return success(resp);
  336 + }
  337 +
  338 + @PutMapping("/user/update")
  339 + @ApiOperation("更新用户基本信息")
  340 + @PreAuthorize("@ss.hasScope('user.write')")
  341 + public CommonResult<Boolean> updateUserInfo(@Valid @RequestBody OAuth2OpenUserUpdateReqVO reqVO) {
  342 + // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。
  343 + // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做
  344 + userService.updateUserProfile(getLoginUserId(), OAuth2OpenConvert.INSTANCE.convert(reqVO));
  345 + return success(true);
  346 + }
  347 +
  348 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2TokenController.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2TokenController.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO;
6   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenRespVO;
  5 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO;
7 7 import cn.iocoder.yudao.module.system.convert.auth.OAuth2TokenConvert;
8   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  8 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
9 9 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
10 10 import cn.iocoder.yudao.module.system.service.auth.AdminAuthService;
11   -import cn.iocoder.yudao.module.system.service.auth.OAuth2TokenService;
  11 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
12 12 import io.swagger.annotations.Api;
13 13 import io.swagger.annotations.ApiImplicitParam;
14 14 import io.swagger.annotations.ApiOperation;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientBaseVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
... ... @@ -18,47 +18,43 @@ import java.util.List;
18 18 @Data
19 19 public class OAuth2ClientBaseVO {
20 20  
21   - @ApiModelProperty(value = "客户端编号", required = true)
  21 + @ApiModelProperty(value = "客户端编号", required = true, example = "tudou")
22 22 @NotNull(message = "客户端编号不能为空")
23 23 private String clientId;
24 24  
25   - @ApiModelProperty(value = "客户端密钥", required = true)
  25 + @ApiModelProperty(value = "客户端密钥", required = true, example = "fan")
26 26 @NotNull(message = "客户端密钥不能为空")
27 27 private String secret;
28 28  
29   - @ApiModelProperty(value = "应用名", required = true)
  29 + @ApiModelProperty(value = "应用名", required = true, example = "土豆")
30 30 @NotNull(message = "应用名不能为空")
31 31 private String name;
32 32  
33   - @ApiModelProperty(value = "应用图标", required = true)
  33 + @ApiModelProperty(value = "应用图标", required = true, example = "https://www.iocoder.cn/xx.png")
34 34 @NotNull(message = "应用图标不能为空")
35 35 @URL(message = "应用图标的地址不正确")
36 36 private String logo;
37 37  
38   - @ApiModelProperty(value = "应用描述")
  38 + @ApiModelProperty(value = "应用描述", example = "我是一个应用")
39 39 private String description;
40 40  
41   - @ApiModelProperty(value = "状态", required = true)
  41 + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举")
42 42 @NotNull(message = "状态不能为空")
43 43 private Integer status;
44 44  
45   - @ApiModelProperty(value = "访问令牌的有效期", required = true)
  45 + @ApiModelProperty(value = "访问令牌的有效期", required = true, example = "8640")
46 46 @NotNull(message = "访问令牌的有效期不能为空")
47 47 private Integer accessTokenValiditySeconds;
48 48  
49   - @ApiModelProperty(value = "刷新令牌的有效期", required = true)
  49 + @ApiModelProperty(value = "刷新令牌的有效期", required = true, example = "8640000")
50 50 @NotNull(message = "刷新令牌的有效期不能为空")
51 51 private Integer refreshTokenValiditySeconds;
52 52  
53   - @ApiModelProperty(value = "可重定向的 URI 地址", required = true)
  53 + @ApiModelProperty(value = "可重定向的 URI 地址", required = true, example = "https://www.iocoder.cn")
54 54 @NotNull(message = "可重定向的 URI 地址不能为空")
55 55 private List<@NotEmpty(message = "重定向的 URI 不能为空")
56 56 @URL(message = "重定向的 URI 格式不正确") String> redirectUris;
57 57  
58   - @ApiModelProperty(value = "是否自动授权", required = true, example = "true")
59   - @NotNull(message = "是否自动授权不能为空")
60   - private Boolean autoApprove;
61   -
62 58 @ApiModelProperty(value = "授权类型", required = true, example = "password", notes = "参见 OAuth2GrantTypeEnum 枚举")
63 59 @NotNull(message = "授权类型不能为空")
64 60 private List<String> authorizedGrantTypes;
... ... @@ -66,6 +62,9 @@ public class OAuth2ClientBaseVO {
66 62 @ApiModelProperty(value = "授权范围", example = "user_info")
67 63 private List<String> scopes;
68 64  
  65 + @ApiModelProperty(value = "自动通过的授权范围", example = "user_info")
  66 + private List<String> autoApproveScopes;
  67 +
69 68 @ApiModelProperty(value = "权限", example = "system:user:query")
70 69 private List<String> authorities;
71 70  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientCreateReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientCreateReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client;
2 2  
3 3 import lombok.*;
4 4 import io.swagger.annotations.*;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientPageReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client;
2 2  
3 3 import lombok.*;
4 4 import io.swagger.annotations.*;
... ... @@ -10,10 +10,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
10 10 @ToString(callSuper = true)
11 11 public class OAuth2ClientPageReqVO extends PageParam {
12 12  
13   - @ApiModelProperty(value = "应用名")
  13 + @ApiModelProperty(value = "应用名", example = "土豆", notes = "模糊匹配")
14 14 private String name;
15 15  
16   - @ApiModelProperty(value = "状态")
  16 + @ApiModelProperty(value = "状态", example = "1", notes = "参见 CommonStatusEnum 枚举")
17 17 private Integer status;
18 18  
19 19 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientRespVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ... @@ -14,7 +14,7 @@ import java.util.Date;
14 14 @ToString(callSuper = true)
15 15 public class OAuth2ClientRespVO extends OAuth2ClientBaseVO {
16 16  
17   - @ApiModelProperty(value = "编号", required = true)
  17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
18 18 private Long id;
19 19  
20 20 @ApiModelProperty(value = "创建时间", required = true)
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientUpdateReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ... @@ -14,7 +14,7 @@ import javax.validation.constraints.NotNull;
14 14 @ToString(callSuper = true)
15 15 public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO {
16 16  
17   - @ApiModelProperty(value = "编号", required = true)
  17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
18 18 @NotNull(message = "编号不能为空")
19 19 private Long id;
20 20  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.AllArgsConstructor;
  7 +import lombok.Data;
  8 +import lombok.NoArgsConstructor;
  9 +
  10 +@ApiModel("管理后台 - 【开放接口】访问令牌 Response VO")
  11 +@Data
  12 +@NoArgsConstructor
  13 +@AllArgsConstructor
  14 +public class OAuth2OpenAccessTokenRespVO {
  15 +
  16 + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou")
  17 + @JsonProperty("access_token")
  18 + private String accessToken;
  19 +
  20 + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice")
  21 + @JsonProperty("refresh_token")
  22 + private String refreshToken;
  23 +
  24 + @ApiModelProperty(value = "令牌类型", required = true, example = "bearer")
  25 + @JsonProperty("token_type")
  26 + private String tokenType;
  27 +
  28 + @ApiModelProperty(value = "过期时间", required = true, example = "42430", notes = "单位:秒")
  29 + @JsonProperty("expires_in")
  30 + private Long expiresIn;
  31 +
  32 + @ApiModelProperty(value = "授权范围", example = "user_info", notes = "如果多个授权范围,使用空格分隔")
  33 + private String scope;
  34 +
  35 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.AllArgsConstructor;
  7 +import lombok.Data;
  8 +import lombok.NoArgsConstructor;
  9 +
  10 +import java.util.List;
  11 +
  12 +@ApiModel("管理后台 - 授权页的信息 Response VO")
  13 +@Data
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
  16 +public class OAuth2OpenAuthorizeInfoRespVO {
  17 +
  18 + /**
  19 + * 客户端
  20 + */
  21 + private Client client;
  22 +
  23 + @ApiModelProperty(value = "scope 的选中信息", required = true, notes = "使用 List 保证有序性,Key 是 scope,Value 为是否选中")
  24 + private List<KeyValue<String, Boolean>> scopes;
  25 +
  26 + @Data
  27 + @NoArgsConstructor
  28 + @AllArgsConstructor
  29 + public static class Client {
  30 +
  31 + @ApiModelProperty(value = "应用名", required = true, example = "土豆")
  32 + private String name;
  33 +
  34 + @ApiModelProperty(value = "应用图标", required = true, example = "https://www.iocoder.cn/xx.png")
  35 + private String logo;
  36 +
  37 + }
  38 +
  39 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.AllArgsConstructor;
  7 +import lombok.Data;
  8 +import lombok.NoArgsConstructor;
  9 +
  10 +import java.util.Set;
  11 +
  12 +@ApiModel("管理后台 - 【开放接口】校验令牌 Response VO")
  13 +@Data
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
  16 +public class OAuth2OpenCheckTokenRespVO {
  17 +
  18 + @ApiModelProperty(value = "用户编号", required = true, example = "666")
  19 + @JsonProperty("user_id")
  20 + private Long userId;
  21 + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举")
  22 + @JsonProperty("user_type")
  23 + private Integer userType;
  24 + @ApiModelProperty(value = "租户编号", required = true, example = "1024")
  25 + @JsonProperty("tenant_id")
  26 + private Long tenantId;
  27 +
  28 + @ApiModelProperty(value = "客户端编号", required = true, example = "car")
  29 + private String clientId;
  30 + @ApiModelProperty(value = "授权范围", required = true, example = "user_info")
  31 + private Set<String> scopes;
  32 +
  33 + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou")
  34 + @JsonProperty("access_token")
  35 + private String accessToken;
  36 +
  37 + @ApiModelProperty(value = "过期时间", required = true, example = "1593092157", notes = "时间戳 / 1000,即单位:秒")
  38 + @JsonProperty("exp")
  39 + private Long exp;
  40 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserInfoRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Data;
  7 +import lombok.NoArgsConstructor;
  8 +
  9 +import java.util.List;
  10 +
  11 +@ApiModel("管理后台 - 【开放接口】获得用户基本信息 Response VO")
  12 +@Data
  13 +@NoArgsConstructor
  14 +@AllArgsConstructor
  15 +public class OAuth2OpenUserInfoRespVO {
  16 +
  17 + @ApiModelProperty(value = "用户编号", required = true, example = "1")
  18 + private Long id;
  19 +
  20 + @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
  21 + private String username;
  22 +
  23 + @ApiModelProperty(value = "用户昵称", required = true, example = "芋道")
  24 + private String nickname;
  25 +
  26 + @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn")
  27 + private String email;
  28 + @ApiModelProperty(value = "手机号码", example = "15601691300")
  29 + private String mobile;
  30 +
  31 + @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类")
  32 + private Integer sex;
  33 +
  34 + @ApiModelProperty(value = "用户头像", example = "https://www.iocoder.cn/xxx.png")
  35 + private String avatar;
  36 +
  37 + /**
  38 + * 所在部门
  39 + */
  40 + private Dept dept;
  41 +
  42 + /**
  43 + * 所属岗位数组
  44 + */
  45 + private List<Post> posts;
  46 +
  47 + @ApiModel("部门")
  48 + @Data
  49 + public static class Dept {
  50 +
  51 + @ApiModelProperty(value = "部门编号", required = true, example = "1")
  52 + private Long id;
  53 +
  54 + @ApiModelProperty(value = "部门名称", required = true, example = "研发部")
  55 + private String name;
  56 +
  57 + }
  58 +
  59 + @ApiModel("岗位")
  60 + @Data
  61 + public static class Post {
  62 +
  63 + @ApiModelProperty(value = "岗位编号", required = true, example = "1")
  64 + private Long id;
  65 +
  66 + @ApiModelProperty(value = "岗位名称", required = true, example = "开发")
  67 + private String name;
  68 +
  69 + }
  70 +
  71 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserUpdateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Data;
  7 +import lombok.NoArgsConstructor;
  8 +import org.hibernate.validator.constraints.Length;
  9 +
  10 +import javax.validation.constraints.Email;
  11 +import javax.validation.constraints.Size;
  12 +
  13 +@ApiModel("管理后台 - 【开放接口】更新用户基本信息 Request VO")
  14 +@Data
  15 +@NoArgsConstructor
  16 +@AllArgsConstructor
  17 +public class OAuth2OpenUserUpdateReqVO {
  18 +
  19 + @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
  20 + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
  21 + private String nickname;
  22 +
  23 + @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn")
  24 + @Email(message = "邮箱格式不正确")
  25 + @Size(max = 50, message = "邮箱长度不能超过 50 个字符")
  26 + private String email;
  27 +
  28 + @ApiModelProperty(value = "手机号码", example = "15601691300")
  29 + @Length(min = 11, max = 11, message = "手机号长度必须 11 位")
  30 + private String mobile;
  31 +
  32 + @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类")
  33 + private Integer sex;
  34 +
  35 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenPageReqVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.token;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 4 import io.swagger.annotations.ApiModel;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenRespVO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java
1   -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.token;
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token;
2 2  
3 3 import io.swagger.annotations.ApiModel;
4 4 import io.swagger.annotations.ApiModelProperty;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java
... ... @@ -46,7 +46,6 @@ public class UserProfileController {
46 46 private AdminUserService userService;
47 47 @Resource
48 48 private DeptService deptService;
49   -
50 49 @Resource
51 50 private PostService postService;
52 51 @Resource
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java
... ... @@ -13,7 +13,7 @@ import javax.validation.constraints.Size;
13 13 public class UserProfileUpdateReqVO {
14 14  
15 15 @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
16   - @Size(max = 30, message = "用户昵称长度不能超过30个字符")
  16 + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
17 17 private String nickname;
18 18  
19 19 @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn")
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java
... ... @@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
4 4 import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
5 5 import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
6 6 import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
7   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
8   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  7 +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
  8 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
9 9 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
10 10 import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
11 11 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2ClientConvert.java
1 1 package cn.iocoder.yudao.module.system.convert.auth;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO;
5   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientRespVO;
6   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO;
7   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
  4 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO;
  5 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
8 8 import org.mapstruct.Mapper;
9 9 import org.mapstruct.factory.Mappers;
10 10  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2TokenConvert.java
... ... @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.convert.auth;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO;
5 5 import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO;
6   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenRespVO;
7   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
8 8 import org.mapstruct.Mapper;
9 9 import org.mapstruct.factory.Mappers;
10 10  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.convert.oauth2;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  5 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  6 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  7 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
  8 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
  9 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
  10 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
  11 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserUpdateReqVO;
  12 +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
  13 +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
  14 +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
  15 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  16 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
  17 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
  18 +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
  19 +import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
  20 +import org.mapstruct.Mapper;
  21 +import org.mapstruct.factory.Mappers;
  22 +
  23 +import java.util.ArrayList;
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +
  27 +@Mapper
  28 +public interface OAuth2OpenConvert {
  29 +
  30 + OAuth2OpenConvert INSTANCE = Mappers.getMapper(OAuth2OpenConvert.class);
  31 +
  32 + default OAuth2OpenAccessTokenRespVO convert(OAuth2AccessTokenDO bean) {
  33 + OAuth2OpenAccessTokenRespVO respVO = convert0(bean);
  34 + respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase());
  35 + respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime()));
  36 + respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes()));
  37 + return respVO;
  38 + }
  39 + OAuth2OpenAccessTokenRespVO convert0(OAuth2AccessTokenDO bean);
  40 +
  41 + default OAuth2OpenCheckTokenRespVO convert2(OAuth2AccessTokenDO bean) {
  42 + OAuth2OpenCheckTokenRespVO respVO = convert3(bean);
  43 + respVO.setExp(bean.getExpiresTime().getTime() / 1000L);
  44 + respVO.setUserType(UserTypeEnum.ADMIN.getValue());
  45 + return respVO;
  46 + }
  47 + OAuth2OpenCheckTokenRespVO convert3(OAuth2AccessTokenDO bean);
  48 +
  49 + // ============ 用户操作的示例 ============
  50 +
  51 + OAuth2OpenUserInfoRespVO convert(AdminUserDO bean);
  52 + OAuth2OpenUserInfoRespVO.Dept convert(DeptDO dept);
  53 + List<OAuth2OpenUserInfoRespVO.Post> convertList(List<PostDO> list);
  54 +
  55 + UserProfileUpdateReqVO convert(OAuth2OpenUserUpdateReqVO bean);
  56 +
  57 + default OAuth2OpenAuthorizeInfoRespVO convert(OAuth2ClientDO client, List<OAuth2ApproveDO> approves) {
  58 + // 构建 scopes
  59 + List<KeyValue<String, Boolean>> scopes = new ArrayList<>(client.getScopes().size());
  60 + Map<String, OAuth2ApproveDO> approveMap = CollectionUtils.convertMap(approves, OAuth2ApproveDO::getScope);
  61 + client.getScopes().forEach(scope -> {
  62 + OAuth2ApproveDO approve = approveMap.get(scope);
  63 + scopes.add(new KeyValue<>(scope, approve != null ? approve.getApproved() : false));
  64 + });
  65 + // 拼接返回
  66 + return new OAuth2OpenAuthorizeInfoRespVO(
  67 + new OAuth2OpenAuthorizeInfoRespVO.Client(client.getName(), client.getLogo()), scopes);
  68 + }
  69 +
  70 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2AccessTokenDO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java
1   -package cn.iocoder.yudao.module.system.dal.dataobject.auth;
  1 +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
4 4 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
5 5 import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableField;
6 7 import com.baomidou.mybatisplus.annotation.TableId;
7 8 import com.baomidou.mybatisplus.annotation.TableName;
  9 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
8 10 import lombok.Data;
9 11 import lombok.EqualsAndHashCode;
10 12 import lombok.experimental.Accessors;
11 13  
12 14 import java.util.Date;
  15 +import java.util.List;
13 16  
14 17 /**
15 18 * OAuth2 访问令牌 DO
... ... @@ -56,6 +59,11 @@ public class OAuth2AccessTokenDO extends TenantBaseDO {
56 59 */
57 60 private String clientId;
58 61 /**
  62 + * 授权范围
  63 + */
  64 + @TableField(typeHandler = JacksonTypeHandler.class)
  65 + private List<String> scopes;
  66 + /**
59 67 * 过期时间
60 68 */
61 69 private Date expiresTime;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.Data;
  9 +import lombok.EqualsAndHashCode;
  10 +
  11 +import java.util.Date;
  12 +
  13 +/**
  14 + * OAuth2 批准 DO
  15 + *
  16 + * 用户在 sso.vue 界面时,记录接受的 scope 列表
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@TableName(value = "system_oauth2_approve", autoResultMap = true)
  21 +@KeySequence("system_oauth2_approve_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  22 +@Data
  23 +@EqualsAndHashCode(callSuper = true)
  24 +public class OAuth2ApproveDO extends BaseDO {
  25 +
  26 + /**
  27 + * 编号,数据库自增
  28 + */
  29 + @TableId
  30 + private Long id;
  31 + /**
  32 + * 用户编号
  33 + */
  34 + private Long userId;
  35 + /**
  36 + * 用户类型
  37 + *
  38 + * 枚举 {@link UserTypeEnum}
  39 + */
  40 + private Integer userType;
  41 + /**
  42 + * 客户端编号
  43 + *
  44 + * 关联 {@link OAuth2ClientDO#getId()}
  45 + */
  46 + private String clientId;
  47 + /**
  48 + * 授权范围
  49 + */
  50 + private String scope;
  51 + /**
  52 + * 是否接受
  53 + *
  54 + * true - 接受
  55 + * false - 拒绝
  56 + */
  57 + private Boolean approved;
  58 + /**
  59 + * 过期时间
  60 + */
  61 + private Date expiresTime;
  62 +
  63 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2ClientDO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java
1   -package cn.iocoder.yudao.module.system.dal.dataobject.auth;
  1 +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
... ... @@ -71,10 +71,6 @@ public class OAuth2ClientDO extends BaseDO {
71 71 @TableField(typeHandler = JacksonTypeHandler.class)
72 72 private List<String> redirectUris;
73 73 /**
74   - * 是否自动授权
75   - */
76   - private Boolean autoApprove;
77   - /**
78 74 * 授权类型(模式)
79 75 *
80 76 * 枚举 {@link OAuth2GrantTypeEnum}
... ... @@ -87,6 +83,13 @@ public class OAuth2ClientDO extends BaseDO {
87 83 @TableField(typeHandler = JacksonTypeHandler.class)
88 84 private List<String> scopes;
89 85 /**
  86 + * 自动授权的 Scope
  87 + *
  88 + * code 授权时,如果 scope 在这个范围内,则自动通过
  89 + */
  90 + @TableField(typeHandler = JacksonTypeHandler.class)
  91 + private List<String> autoApproveScopes;
  92 + /**
90 93 * 权限
91 94 */
92 95 @TableField(typeHandler = JacksonTypeHandler.class)
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2CodeDO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java
1   -package cn.iocoder.yudao.module.system.dal.dataobject.auth;
  1 +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 5 import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableField;
6 7 import com.baomidou.mybatisplus.annotation.TableName;
  8 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
7 9 import lombok.Data;
8 10 import lombok.EqualsAndHashCode;
9 11  
10 12 import java.util.Date;
  13 +import java.util.List;
11 14  
12 15 /**
13 16 * OAuth2 授权码 DO
14 17 *
15 18 * @author 芋道源码
16 19 */
17   -@TableName("system_oauth2_code")
  20 +@TableName(value = "system_oauth2_code", autoResultMap = true)
18 21 @KeySequence("system_oauth2_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
19 22 @Data
20 23 @EqualsAndHashCode(callSuper = true)
... ... @@ -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 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/dal/dataobject/auth/OAuth2RefreshTokenDO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java
1   -package cn.iocoder.yudao.module.system.dal.dataobject.auth;
  1 +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 5 import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableField;
6 7 import com.baomidou.mybatisplus.annotation.TableName;
  8 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
7 9 import lombok.Data;
8 10 import lombok.EqualsAndHashCode;
9 11 import lombok.experimental.Accessors;
10 12  
11 13 import java.util.Date;
  14 +import java.util.List;
12 15  
13 16 /**
14 17 * OAuth2 刷新令牌
... ... @@ -48,6 +51,11 @@ public class OAuth2RefreshTokenDO extends BaseDO {
48 51 */
49 52 private String clientId;
50 53 /**
  54 + * 授权范围
  55 + */
  56 + @TableField(typeHandler = JacksonTypeHandler.class)
  57 + private List<String> scopes;
  58 + /**
51 59 * 过期时间
52 60 */
53 61 private Date expiresTime;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2AccessTokenMapper.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java
1   -package cn.iocoder.yudao.module.system.dal.mysql.auth;
  1 +package cn.iocoder.yudao.module.system.dal.mysql.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO;
7   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
8 8 import org.apache.ibatis.annotations.Mapper;
9 9  
10 10 import java.util.Date;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.dal.mysql.oauth2;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
  6 +import org.apache.ibatis.annotations.Mapper;
  7 +
  8 +import java.util.List;
  9 +
  10 +@Mapper
  11 +public interface OAuth2ApproveMapper extends BaseMapperX<OAuth2ApproveDO> {
  12 +
  13 + default int update(OAuth2ApproveDO updateObj) {
  14 + return update(updateObj, new LambdaQueryWrapperX<OAuth2ApproveDO>()
  15 + .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId())
  16 + .eq(OAuth2ApproveDO::getUserType, updateObj.getUserType())
  17 + .eq(OAuth2ApproveDO::getClientId, updateObj.getClientId())
  18 + .eq(OAuth2ApproveDO::getScope, updateObj.getScope()));
  19 + }
  20 +
  21 + default List<OAuth2ApproveDO> selectListByUserIdAndUserTypeAndClientId(Long userId, Integer userType, String clientId) {
  22 + return selectList(new LambdaQueryWrapperX<OAuth2ApproveDO>()
  23 + .eq(OAuth2ApproveDO::getUserId, userId)
  24 + .eq(OAuth2ApproveDO::getUserType, userType)
  25 + .eq(OAuth2ApproveDO::getClientId, clientId));
  26 + }
  27 +
  28 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2ClientMapper.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java
1   -package cn.iocoder.yudao.module.system.dal.mysql.auth;
  1 +package cn.iocoder.yudao.module.system.dal.mysql.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO;
7   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
8 8 import org.apache.ibatis.annotations.Mapper;
9 9 import org.apache.ibatis.annotations.Select;
10 10  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.dal.mysql.oauth2;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface OAuth2CodeMapper extends BaseMapperX<OAuth2CodeDO> {
  9 +
  10 + default OAuth2CodeDO selectByCode(String code) {
  11 + return selectOne(OAuth2CodeDO::getCode, code);
  12 + }
  13 +
  14 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2RefreshTokenMapper.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java
1   -package cn.iocoder.yudao.module.system.dal.mysql.auth;
  1 +package cn.iocoder.yudao.module.system.dal.mysql.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO;
  5 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
6 6 import org.apache.ibatis.annotations.Mapper;
7 7  
8 8 @Mapper
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
1 1 package cn.iocoder.yudao.module.system.dal.redis;
2 2  
3 3 import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
4   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  4 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
5 5  
6 6 import java.time.Duration;
7 7  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/OAuth2AccessTokenRedisDAO.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java
1   -package cn.iocoder.yudao.module.system.dal.redis.auth;
  1 +package cn.iocoder.yudao.module.system.dal.redis.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
4 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
5   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  5 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
6 6 import org.springframework.data.redis.core.StringRedisTemplate;
7 7 import org.springframework.stereotype.Repository;
8 8  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java
... ... @@ -35,6 +35,9 @@ public class SecurityConfiguration {
35 35 registry.antMatchers(buildAdminApi("/system/tenant/get-id-by-name")).permitAll();
36 36 // 短信回调 API
37 37 registry.antMatchers(buildAdminApi("/system/sms/callback/**")).permitAll();
  38 + // OAuth2 API
  39 + registry.antMatchers(buildAdminApi("/system/oauth2/token")).permitAll();
  40 + registry.antMatchers(buildAdminApi("/system/oauth2/check-token")).permitAll();
38 41 }
39 42  
40 43 };
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/auth/OAuth2ClientRefreshConsumer.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.mq.consumer.auth;
2 2  
3 3 import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
4 4 import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
5   -import cn.iocoder.yudao.module.system.service.auth.OAuth2ClientService;
  5 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
6 6 import lombok.extern.slf4j.Slf4j;
7 7 import org.springframework.stereotype.Component;
8 8  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java
1 1 package cn.iocoder.yudao.module.system.service.auth;
2 2  
3   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
  3 +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
  4 +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
4 5  
5 6 import javax.validation.Valid;
6 7  
... ... @@ -14,6 +15,15 @@ import javax.validation.Valid;
14 15 public interface AdminAuthService {
15 16  
16 17 /**
  18 + * 验证账号 + 密码。如果通过,则返回用户
  19 + *
  20 + * @param username 账号
  21 + * @param password 密码
  22 + * @return 用户
  23 + */
  24 + AdminUserDO authenticate(String username, String password);
  25 +
  26 + /**
17 27 * 账号登录
18 28 *
19 29 * @param reqVO 登录信息
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
... ... @@ -8,9 +8,9 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
8 8 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
9 9 import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
10 10 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
11   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
  11 +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
12 12 import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
13   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  13 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
14 14 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
15 15 import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientConstants;
16 16 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
... ... @@ -19,6 +19,7 @@ import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
19 19 import cn.iocoder.yudao.module.system.service.common.CaptchaService;
20 20 import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
21 21 import cn.iocoder.yudao.module.system.service.member.MemberService;
  22 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
22 23 import cn.iocoder.yudao.module.system.service.social.SocialUserService;
23 24 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
24 25 import com.google.common.annotations.VisibleForTesting;
... ... @@ -62,12 +63,33 @@ public class AdminAuthServiceImpl implements AdminAuthService {
62 63 private SmsCodeApi smsCodeApi;
63 64  
64 65 @Override
  66 + public AdminUserDO authenticate(String username, String password) {
  67 + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
  68 + // 校验账号是否存在
  69 + AdminUserDO user = userService.getUserByUsername(username);
  70 + if (user == null) {
  71 + createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
  72 + throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
  73 + }
  74 + if (!userService.isPasswordMatch(password, user.getPassword())) {
  75 + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
  76 + throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
  77 + }
  78 + // 校验是否禁用
  79 + if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
  80 + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
  81 + throw exception(AUTH_LOGIN_USER_DISABLED);
  82 + }
  83 + return user;
  84 + }
  85 +
  86 + @Override
65 87 public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
66 88 // 判断验证码是否正确
67 89 verifyCaptcha(reqVO);
68 90  
69 91 // 使用账号密码,进行登录
70   - AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword());
  92 + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
71 93  
72 94 // 创建 Token 令牌,记录登录日志
73 95 return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
... ... @@ -124,27 +146,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
124 146 captchaService.deleteCaptchaCode(reqVO.getUuid());
125 147 }
126 148  
127   - @VisibleForTesting
128   - AdminUserDO login0(String username, String password) {
129   - final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
130   - // 校验账号是否存在
131   - AdminUserDO user = userService.getUserByUsername(username);
132   - if (user == null) {
133   - createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
134   - throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
135   - }
136   - if (!userService.isPasswordMatch(password, user.getPassword())) {
137   - createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
138   - throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
139   - }
140   - // 校验是否禁用
141   - if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
142   - createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
143   - throw exception(AUTH_LOGIN_USER_DISABLED);
144   - }
145   - return user;
146   - }
147   -
148 149 private void createLoginLog(Long userId, String username,
149 150 LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
150 151 // 插入登录日志
... ... @@ -186,7 +187,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
186 187 @Override
187 188 public AuthLoginRespVO socialBindLogin(AuthSocialBindLoginReqVO reqVO) {
188 189 // 使用账号密码,进行登录。
189   - AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword());
  190 + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
190 191  
191 192 // 绑定社交用户
192 193 socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(user.getId(), getUserType().getValue(), reqVO));
... ... @@ -206,7 +207,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
206 207 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
207 208 // 创建访问令牌
208 209 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
209   - OAuth2ClientConstants.CLIENT_ID_DEFAULT);
  210 + OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
210 211 // 构建返回结果
211 212 return AuthConvert.INSTANCE.convert(accessTokenDO);
212 213 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminOAuth2Service.java deleted
1   -package cn.iocoder.yudao.module.system.service.auth;
2   -
3   -/**
4   - * 管理后台的 OAuth2 Service 接口
5   - *
6   - * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。
7   - *
8   - * 问题:为什么自身也作为一个第三方应用,也走这套流程呢?
9   - * 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
10   - *
11   - * @author 芋道源码
12   - */
13   -public interface AdminOAuth2Service {
14   -}
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2CodeService.java deleted
1   -package cn.iocoder.yudao.module.system.service.auth;
2   -
3   -/**
4   - * OAuth2.0 授权码 Service 接口
5   - *
6   - * 从功能上,和 Spring Security OAuth 的 JdbcAuthorizationCodeServices 的功能,提供授权码的操作
7   - *
8   - * @author 芋道源码
9   - */
10   -public class OAuth2CodeService {
11   -}
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
  2 +
  3 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
  4 +
  5 +import java.util.Collection;
  6 +import java.util.List;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * OAuth2 批准 Service 接口
  11 + *
  12 + * 从功能上,和 Spring Security OAuth 的 ApprovalStoreUserApprovalHandler 的功能,记录用户针对指定客户端的授权,减少手动确定。
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public interface OAuth2ApproveService {
  17 +
  18 + /**
  19 + * 获得指定用户,针对指定客户端的指定授权,是否通过
  20 + *
  21 + * 参考 ApprovalStoreUserApprovalHandler 的 checkForPreApproval 方法
  22 + *
  23 + * @param userId 用户编号
  24 + * @param userType 用户类型
  25 + * @param clientId 客户端编号
  26 + * @param requestedScopes 授权范围
  27 + * @return 是否授权通过
  28 + */
  29 + boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection<String> requestedScopes);
  30 +
  31 + /**
  32 + * 在用户发起批准时,基于 scopes 的选项,计算最终是否通过
  33 + *
  34 + * @param userId 用户编号
  35 + * @param userType 用户类型
  36 + * @param clientId 客户端编号
  37 + * @param requestedScopes 授权范围
  38 + * @return 是否授权通过
  39 + */
  40 + boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map<String, Boolean> requestedScopes);
  41 +
  42 + /**
  43 + * 获得用户的批准列表,排除已过期的
  44 + *
  45 + * @param userId 用户编号
  46 + * @param userType 用户类型
  47 + * @param clientId 客户端编号
  48 + * @return 是否授权通过
  49 + */
  50 + List<OAuth2ApproveDO> getApproveList(Long userId, Integer userType, String clientId);
  51 +
  52 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
  6 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
  8 +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ApproveMapper;
  9 +import org.springframework.stereotype.Service;
  10 +import org.springframework.validation.annotation.Validated;
  11 +
  12 +import javax.annotation.Resource;
  13 +import java.util.*;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  16 +
  17 +/**
  18 + * OAuth2 批准 Service 实现类
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +@Service
  23 +@Validated
  24 +public class OAuth2ApproveServiceImpl implements OAuth2ApproveService {
  25 +
  26 + /**
  27 + * 批准的过期时间,默认 30 天
  28 + */
  29 + private static final Integer TIMEOUT = 30 * 24 * 60 * 60; // 单位:秒
  30 +
  31 + @Resource
  32 + private OAuth2ClientService oauth2ClientService;
  33 +
  34 + @Resource
  35 + private OAuth2ApproveMapper oauth2ApproveMapper;
  36 +
  37 + @Override
  38 + public boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection<String> requestedScopes) {
  39 + // 第一步,基于 Client 的自动授权计算,如果 scopes 都在自动授权中,则返回 true 通过
  40 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
  41 + Assert.notNull(clientDO, "客户端不能为空"); // 防御性编程
  42 + if (CollUtil.containsAll(clientDO.getAutoApproveScopes(), requestedScopes)) {
  43 + // gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store.
  44 + Date expireTime = DateUtils.addDate(Calendar.SECOND, TIMEOUT);
  45 + for (String scope : requestedScopes) {
  46 + saveApprove(userId, userType, clientId, scope, true, expireTime);
  47 + }
  48 + return true;
  49 + }
  50 +
  51 + // 第二步,算上用户已经批准的授权。如果 scopes 都包含,则返回 true
  52 + List<OAuth2ApproveDO> approveDOs = oauth2ApproveMapper.selectListByUserIdAndUserTypeAndClientId(
  53 + userId, userType, clientId);
  54 + Set<String> scopes = convertSet(approveDOs, OAuth2ApproveDO::getScope,
  55 + o -> o.getApproved() && !DateUtils.isExpired(o.getExpiresTime())); // 只保留未过期的
  56 + return CollUtil.containsAll(scopes, requestedScopes);
  57 + }
  58 +
  59 + @Override
  60 + public boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map<String, Boolean> requestedScopes) {
  61 + // 如果 requestedScopes 为空,说明没有要求,则返回 true 通过
  62 + if (CollUtil.isEmpty(requestedScopes)) {
  63 + return true;
  64 + }
  65 +
  66 + // 更新批准的信息
  67 + boolean success = false; // 需要至少有一个同意
  68 + Date expireTime = DateUtils.addDate(Calendar.SECOND, TIMEOUT);
  69 + for (Map.Entry<String, Boolean> entry :requestedScopes.entrySet()) {
  70 + if (entry.getValue()) {
  71 + success = true;
  72 + }
  73 + saveApprove(userId, userType, clientId, entry.getKey(), entry.getValue(), expireTime);
  74 + }
  75 + return success;
  76 + }
  77 +
  78 + @Override
  79 + public List<OAuth2ApproveDO> getApproveList(Long userId, Integer userType, String clientId) {
  80 + List<OAuth2ApproveDO> approveDOs = oauth2ApproveMapper.selectListByUserIdAndUserTypeAndClientId(
  81 + userId, userType, clientId);
  82 + approveDOs.removeIf(o -> DateUtils.isExpired(o.getExpiresTime()));
  83 + return approveDOs;
  84 + }
  85 +
  86 + private void saveApprove(Long userId, Integer userType, String clientId,
  87 + String scope, Boolean approved, Date expireTime) {
  88 + // 先更新
  89 + OAuth2ApproveDO approveDO = new OAuth2ApproveDO().setUserId(userId).setUserType(userType)
  90 + .setClientId(clientId).setScope(scope).setApproved(approved).setExpiresTime(expireTime);
  91 + if (oauth2ApproveMapper.update(approveDO) == 1) {
  92 + return;
  93 + }
  94 + // 失败,则说明不存在,进行更新
  95 + oauth2ApproveMapper.insert(approveDO);
  96 + }
  97 +
  98 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientService.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java
1   -package cn.iocoder.yudao.module.system.service.auth;
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO;
5   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO;
6   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO;
7   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
  4 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO;
  5 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
8 8  
9 9 import javax.validation.Valid;
  10 +import java.util.Collection;
10 11  
11 12 /**
12 13 * OAuth2.0 Client Service 接口
... ... @@ -63,9 +64,25 @@ 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, null);
  71 + }
  72 +
  73 + /**
  74 + * 从缓存中,校验客户端是否合法
  75 + *
  76 + * 非空时,进行校验
  77 + *
66 78 * @param clientId 客户端编号
  79 + * @param clientSecret 客户端密钥
  80 + * @param authorizedGrantType 授权方式
  81 + * @param scopes 授权范围
  82 + * @param redirectUri 重定向地址
67 83 * @return 客户端
68 84 */
69   - OAuth2ClientDO validOAuthClientFromCache(String clientId);
  85 + OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
  86 + String authorizedGrantType, Collection<String> scopes, String redirectUri);
70 87  
71 88 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImpl.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java
1   -package cn.iocoder.yudao.module.system.service.auth;
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -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;
7   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO;
  8 +import cn.iocoder.yudao.framework.common.util.string.StrUtils;
  9 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO;
  10 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO;
  11 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO;
8 12 import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert;
9   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
10   -import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper;
  13 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
  14 +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ClientMapper;
11 15 import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
12 16 import com.google.common.annotations.VisibleForTesting;
13 17 import lombok.Getter;
... ... @@ -18,15 +22,12 @@ import org.springframework.validation.annotation.Validated;
18 22  
19 23 import javax.annotation.PostConstruct;
20 24 import javax.annotation.Resource;
21   -import java.util.Date;
22   -import java.util.List;
23   -import java.util.Map;
  25 +import java.util.*;
24 26  
25 27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
26 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
27 29 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;
  30 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
30 31  
31 32 /**
32 33 * OAuth2.0 Client Service 实现类
... ... @@ -175,8 +176,34 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
175 176 }
176 177  
177 178 @Override
178   - public OAuth2ClientDO validOAuthClientFromCache(String clientId) {
179   - return clientCache.get(clientId);
  179 + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
  180 + String authorizedGrantType, Collection<String> scopes, String redirectUri) {
  181 + // 校验客户端存在、且开启
  182 + OAuth2ClientDO client = clientCache.get(clientId);
  183 + if (client == null) {
  184 + throw exception(OAUTH2_CLIENT_NOT_EXISTS);
  185 + }
  186 + if (ObjectUtil.notEqual(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
  187 + throw exception(OAUTH2_CLIENT_DISABLE);
  188 + }
  189 +
  190 + // 校验客户端密钥
  191 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) {
  192 + throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR, clientSecret);
  193 + }
  194 + // 校验授权方式
  195 + if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) {
  196 + throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS);
  197 + }
  198 + // 校验授权范围
  199 + if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScopes(), scopes)) {
  200 + throw exception(OAUTH2_CLIENT_SCOPE_OVER);
  201 + }
  202 + // 校验回调地址
  203 + if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) {
  204 + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri);
  205 + }
  206 + return client;
180 207 }
181 208  
182 209 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
  2 +
  3 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * OAuth2.0 授权码 Service 接口
  9 + *
  10 + * 从功能上,和 Spring Security OAuth 的 JdbcAuthorizationCodeServices 的功能,提供授权码的操作
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public interface OAuth2CodeService {
  15 +
  16 + /**
  17 + * 创建授权码
  18 + *
  19 + * 参考 JdbcAuthorizationCodeServices 的 createAuthorizationCode 方法
  20 + *
  21 + * @param userId 用户编号
  22 + * @param userType 用户类型
  23 + * @param clientId 客户端编号
  24 + * @param scopes 授权范围
  25 + * @param redirectUri 重定向 URI
  26 + * @param state 状态
  27 + * @return 授权码的信息
  28 + */
  29 + OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, List<String> scopes,
  30 + String redirectUri, String state);
  31 +
  32 + /**
  33 + * 使用授权码
  34 + *
  35 + * @param code 授权码
  36 + */
  37 + OAuth2CodeDO consumeAuthorizationCode(String code);
  38 +
  39 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
  5 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO;
  6 +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2CodeMapper;
  7 +import org.springframework.stereotype.Service;
  8 +import org.springframework.validation.annotation.Validated;
  9 +
  10 +import javax.annotation.Resource;
  11 +import java.util.Calendar;
  12 +import java.util.List;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  15 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE;
  16 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS;
  17 +
  18 +/**
  19 + * OAuth2.0 授权码 Service 实现类
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +@Service
  24 +@Validated
  25 +public class OAuth2CodeServiceImpl implements OAuth2CodeService {
  26 +
  27 + /**
  28 + * 授权码的过期时间,默认 5 分钟
  29 + */
  30 + private static final Integer TIMEOUT = 5 * 60;
  31 +
  32 + @Resource
  33 + private OAuth2CodeMapper oauth2CodeMapper;
  34 +
  35 + @Override
  36 + public OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, List<String> scopes,
  37 + String redirectUri, String state) {
  38 + OAuth2CodeDO codeDO = new OAuth2CodeDO().setCode(generateCode())
  39 + .setUserId(userId).setUserType(userType)
  40 + .setClientId(clientId).setScopes(scopes)
  41 + .setExpiresTime(DateUtils.addDate(Calendar.SECOND, TIMEOUT))
  42 + .setRedirectUri(redirectUri).setState(state);
  43 + oauth2CodeMapper.insert(codeDO);
  44 + return codeDO;
  45 + }
  46 +
  47 + @Override
  48 + public OAuth2CodeDO consumeAuthorizationCode(String code) {
  49 + OAuth2CodeDO codeDO = oauth2CodeMapper.selectByCode(code);
  50 + if (codeDO == null) {
  51 + throw exception(OAUTH2_CODE_NOT_EXISTS);
  52 + }
  53 + if (DateUtils.isExpired(codeDO.getExpiresTime())) {
  54 + throw exception(OAUTH2_CODE_EXPIRE);
  55 + }
  56 + oauth2CodeMapper.deleteById(codeDO.getId());
  57 + return codeDO;
  58 + }
  59 +
  60 + private static String generateCode() {
  61 + return IdUtil.fastSimpleUUID();
  62 + }
  63 +
  64 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
  2 +
  3 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * OAuth2 授予 Service 接口
  9 + *
  10 + * 从功能上,和 Spring Security OAuth 的 TokenGranter 的功能,提供访问令牌、刷新令牌的操作
  11 + *
  12 + * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。
  13 + *
  14 + * 问题:为什么自身也作为一个第三方应用,也走这套流程呢?
  15 + * 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +public interface OAuth2GrantService {
  20 +
  21 + /**
  22 + * 简化模式
  23 + *
  24 + * 对应 Spring Security OAuth2 的 ImplicitTokenGranter 功能
  25 + *
  26 + * @param userId 用户编号
  27 + * @param userType 用户类型
  28 + * @param clientId 客户端编号
  29 + * @param scopes 授权范围
  30 + * @return 访问令牌
  31 + */
  32 + OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
  33 + String clientId, List<String> scopes);
  34 +
  35 + /**
  36 + * 授权码模式,第一阶段,获得 code 授权码
  37 + *
  38 + * 对应 Spring Security OAuth2 的 AuthorizationEndpoint 的 generateCode 方法
  39 + *
  40 + * @param userId 用户编号
  41 + * @param userType 用户类型
  42 + * @param clientId 客户端编号
  43 + * @param scopes 授权范围
  44 + * @param redirectUri 重定向 URI
  45 + * @param state 状态
  46 + * @return 授权码
  47 + */
  48 + String grantAuthorizationCodeForCode(Long userId, Integer userType,
  49 + String clientId, List<String> scopes,
  50 + String redirectUri, String state);
  51 +
  52 + /**
  53 + * 授权码模式,第二阶段,获得 accessToken 访问令牌
  54 + *
  55 + * 对应 Spring Security OAuth2 的 AuthorizationCodeTokenGranter 功能
  56 + *
  57 + * @param clientId 客户端编号
  58 + * @param code 授权码
  59 + * @param redirectUri 重定向 URI
  60 + * @param state 状态
  61 + * @return 访问令牌
  62 + */
  63 + OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code,
  64 + String redirectUri, String state);
  65 +
  66 + /**
  67 + * 密码模式
  68 + *
  69 + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能
  70 + *
  71 + * @param username 账号
  72 + * @param password 密码
  73 + * @param clientId 客户端编号
  74 + * @param scopes 授权范围
  75 + * @return 访问令牌
  76 + */
  77 + OAuth2AccessTokenDO grantPassword(String username, String password,
  78 + String clientId, List<String> scopes);
  79 +
  80 + /**
  81 + * 刷新模式
  82 + *
  83 + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能
  84 + *
  85 + * @param refreshToken 刷新令牌
  86 + * @param clientId 客户端编号
  87 + * @return 访问令牌
  88 + */
  89 + OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId);
  90 +
  91 + /**
  92 + * 客户端模式
  93 + *
  94 + * 对应 Spring Security OAuth2 的 ClientCredentialsTokenGranter 功能
  95 + *
  96 + * @param clientId 客户端编号
  97 + * @param scopes 授权范围
  98 + * @return 访问令牌
  99 + */
  100 + OAuth2AccessTokenDO grantClientCredentials(String clientId, List<String> scopes);
  101 +
  102 + /**
  103 + * 移除访问令牌
  104 + *
  105 + * 对应 Spring Security OAuth2 的 ConsumerTokenServices 的 revokeToken 方法
  106 + *
  107 + * @param accessToken 访问令牌
  108 + * @param clientId 客户端编号
  109 + * @return 是否移除到
  110 + */
  111 + boolean revokeToken(String clientId, String accessToken);
  112 +
  113 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ObjectUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  8 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO;
  9 +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
  10 +import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants;
  11 +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService;
  12 +import org.springframework.stereotype.Service;
  13 +
  14 +import javax.annotation.Resource;
  15 +import java.util.List;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  18 +
  19 +/**
  20 + * OAuth2 授予 Service 实现类
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@Service
  25 +public class OAuth2GrantServiceImpl implements OAuth2GrantService {
  26 +
  27 + @Resource
  28 + private OAuth2TokenService oauth2TokenService;
  29 + @Resource
  30 + private OAuth2CodeService oauth2CodeService;
  31 + @Resource
  32 + private AdminAuthService adminAuthService;
  33 +
  34 + @Override
  35 + public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
  36 + String clientId, List<String> scopes) {
  37 + return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes);
  38 + }
  39 +
  40 + @Override
  41 + public String grantAuthorizationCodeForCode(Long userId, Integer userType,
  42 + String clientId, List<String> scopes,
  43 + String redirectUri, String state) {
  44 + return oauth2CodeService.createAuthorizationCode(userId, userType, clientId, scopes,
  45 + redirectUri, state).getCode();
  46 + }
  47 +
  48 + @Override
  49 + public OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code,
  50 + String redirectUri, String state) {
  51 + OAuth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code);
  52 + Assert.notNull(codeDO, "授权码不能为空"); // 防御性编程
  53 + // 校验 clientId 是否匹配
  54 + if (!StrUtil.equals(clientId, codeDO.getClientId())) {
  55 + throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH);
  56 + }
  57 + // 校验 redirectUri 是否匹配
  58 + if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) {
  59 + throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH);
  60 + }
  61 + // 校验 state 是否匹配
  62 + state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串
  63 + if (!StrUtil.equals(state, codeDO.getState())) {
  64 + throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH);
  65 + }
  66 +
  67 + // 创建访问令牌
  68 + return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(),
  69 + codeDO.getClientId(), codeDO.getScopes());
  70 + }
  71 +
  72 + @Override
  73 + public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List<String> scopes) {
  74 + // 使用账号 + 密码进行登录
  75 + AdminUserDO user = adminAuthService.authenticate(username, password);
  76 + Assert.notNull(user, "用户不能为空!"); // 防御性编程
  77 +
  78 + // 创建访问令牌
  79 + return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes);
  80 + }
  81 +
  82 + @Override
  83 + public OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId) {
  84 + return oauth2TokenService.refreshAccessToken(refreshToken, clientId);
  85 + }
  86 +
  87 + @Override
  88 + public OAuth2AccessTokenDO grantClientCredentials(String clientId, List<String> scopes) {
  89 + // TODO 芋艿:项目中使用 OAuth2 解决的是三方应用的授权,内部的 SSO 等问题,所以暂时不考虑 client_credentials 这个场景
  90 + throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式");
  91 + }
  92 +
  93 + @Override
  94 + public boolean revokeToken(String clientId, String accessToken) {
  95 + // 先查询,保证 clientId 时匹配的
  96 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.getAccessToken(accessToken);
  97 + if (accessTokenDO == null || ObjectUtil.notEqual(clientId, accessTokenDO.getClientId())) {
  98 + return false;
  99 + }
  100 + // 再删除
  101 + return oauth2TokenService.removeAccessToken(accessToken) != null;
  102 + }
  103 +
  104 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenService.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java
1   -package cn.iocoder.yudao.module.system.service.auth;
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO;
5   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  4 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
  5 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  6 +
  7 +import java.util.List;
6 8  
7 9 /**
8 10 * OAuth2.0 Token Service 接口
... ... @@ -22,9 +24,10 @@ public interface OAuth2TokenService {
22 24 * @param userId 用户编号
23 25 * @param userType 用户类型
24 26 * @param clientId 客户端编号
  27 + * @param scopes 授权范围
25 28 * @return 访问令牌的信息
26 29 */
27   - OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId);
  30 + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes);
28 31  
29 32 /**
30 33 * 刷新访问令牌
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenServiceImpl.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java
1   -package cn.iocoder.yudao.module.system.service.auth;
  1 +package cn.iocoder.yudao.module.system.service.oauth2;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.IdUtil;
... ... @@ -7,13 +7,13 @@ import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstant
7 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 8 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
9 9 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
10   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO;
11   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
12   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
13   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO;
14   -import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2AccessTokenMapper;
15   -import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2RefreshTokenMapper;
16   -import cn.iocoder.yudao.module.system.dal.redis.auth.OAuth2AccessTokenRedisDAO;
  10 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
  11 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  12 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
  13 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
  14 +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper;
  15 +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper;
  16 +import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
17 17 import org.springframework.stereotype.Service;
18 18 import org.springframework.transaction.annotation.Transactional;
19 19  
... ... @@ -45,10 +45,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
45 45  
46 46 @Override
47 47 @Transactional
48   - public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId) {
  48 + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
49 49 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
50 50 // 创建刷新令牌
51   - OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO);
  51 + OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
52 52 // 创建访问令牌
53 53 return createOAuth2AccessToken(refreshTokenDO, clientDO);
54 54 }
... ... @@ -134,7 +134,8 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
134 134  
135 135 private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
136 136 OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
137   - .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getClientId())
  137 + .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
  138 + .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
138 139 .setRefreshToken(refreshTokenDO.getRefreshToken())
139 140 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
140 141 accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
... ... @@ -144,9 +145,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
144 145 return accessTokenDO;
145 146 }
146 147  
147   - private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) {
  148 + private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List<String> scopes) {
148 149 OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
149   - .setUserId(userId).setUserType(userType).setClientId(clientDO.getClientId())
  150 + .setUserId(userId).setUserType(userType)
  151 + .setClientId(clientDO.getClientId()).setScopes(scopes)
150 152 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds()));
151 153 oauth2RefreshTokenMapper.insert(refreshToken);
152 154 return refreshToken;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.util.oauth2;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  6 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  7 +
  8 +import java.util.*;
  9 +
  10 +/**
  11 + * OAuth2 相关的工具类
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +public class OAuth2Utils {
  16 +
  17 + /**
  18 + * 构建授权码模式下,重定向的 URI
  19 + *
  20 + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 getSuccessfulRedirect 方法
  21 + *
  22 + * @param redirectUri 重定向 URI
  23 + * @param authorizationCode 授权码
  24 + * @param state 状态
  25 + * @return 授权码模式下的重定向 URI
  26 + */
  27 + public static String buildAuthorizationCodeRedirectUri(String redirectUri, String authorizationCode, String state) {
  28 + Map<String, String> query = new LinkedHashMap<>();
  29 + query.put("code", authorizationCode);
  30 + if (state != null) {
  31 + query.put("state", state);
  32 + }
  33 + return HttpUtils.append(redirectUri, query, null, false);
  34 + }
  35 +
  36 + /**
  37 + * 构建简化模式下,重定向的 URI
  38 + *
  39 + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 appendAccessToken 方法
  40 + *
  41 + * @param redirectUri 重定向 URI
  42 + * @param accessToken 访问令牌
  43 + * @param state 状态
  44 + * @param expireTime 过期时间
  45 + * @param scopes 授权范围
  46 + * @param additionalInformation 附加信息
  47 + * @return 简化授权模式下的重定向 URI
  48 + */
  49 + public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, Date expireTime,
  50 + Collection<String> scopes, Map<String, Object> additionalInformation) {
  51 + Map<String, Object> vars = new LinkedHashMap<String, Object>();
  52 + Map<String, String> keys = new HashMap<String, String>();
  53 + vars.put("access_token", accessToken);
  54 + vars.put("token_type", SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase());
  55 + if (state != null) {
  56 + vars.put("state", state);
  57 + }
  58 + if (expireTime != null) {
  59 + vars.put("expires_in", getExpiresIn(expireTime));
  60 + }
  61 + if (CollUtil.isNotEmpty(scopes)) {
  62 + vars.put("scope", buildScopeStr(scopes));
  63 + }
  64 + for (String key : additionalInformation.keySet()) {
  65 + Object value = additionalInformation.get(key);
  66 + if (value != null) {
  67 + keys.put("extra_" + key, key);
  68 + vars.put("extra_" + key, value);
  69 + }
  70 + }
  71 + // Do not include the refresh token (even if there is one)
  72 + return HttpUtils.append(redirectUri, vars, keys, true);
  73 + }
  74 +
  75 + public static String buildUnsuccessfulRedirect(String redirectUri, String responseType, String state,
  76 + String error, String description) {
  77 + Map<String, String> query = new LinkedHashMap<String, String>();
  78 + query.put("error", error);
  79 + query.put("error_description", description);
  80 + if (state != null) {
  81 + query.put("state", state);
  82 + }
  83 + return HttpUtils.append(redirectUri, query, null, !responseType.contains("code"));
  84 + }
  85 +
  86 + public static long getExpiresIn(Date expireTime) {
  87 + return (expireTime.getTime() - System.currentTimeMillis()) / 1000;
  88 + }
  89 +
  90 + public static String buildScopeStr(Collection<String> scopes) {
  91 + return CollUtil.join(scopes, " ");
  92 + }
  93 +
  94 + public static List<String> buildScopes(String scope) {
  95 + return StrUtil.split(scope, ' ');
  96 + }
  97 +
  98 +}
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java renamed to yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java
... ... @@ -5,15 +5,16 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
5 5 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
6 6 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
7 7 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
8   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.AuthLoginReqVO;
9   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.AuthLoginRespVO;
10   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  8 +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginReqVO;
  9 +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginRespVO;
  10 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
11 11 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
12 12 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
13 13 import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
14 14 import cn.iocoder.yudao.module.system.service.common.CaptchaService;
15 15 import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
16 16 import cn.iocoder.yudao.module.system.service.member.MemberService;
  17 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
17 18 import cn.iocoder.yudao.module.system.service.social.SocialUserService;
18 19 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
19 20 import org.junit.jupiter.api.BeforeEach;
... ... @@ -33,7 +34,7 @@ import static org.mockito.ArgumentMatchers.eq;
33 34 import static org.mockito.Mockito.*;
34 35  
35 36 @Import(AdminAuthServiceImpl.class)
36   -public class AuthServiceImplTest extends BaseDbUnitTest {
  37 +public class AdminAuthServiceImplTest extends BaseDbUnitTest {
37 38  
38 39 @Resource
39 40 private AdminAuthServiceImpl authService;
... ... @@ -62,7 +63,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
62 63 }
63 64  
64 65 @Test
65   - public void testLogin0_success() {
  66 + public void testAuthenticate_success() {
66 67 // 准备参数
67 68 String username = randomString();
68 69 String password = randomString();
... ... @@ -74,19 +75,19 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
74 75 when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true);
75 76  
76 77 // 调用
77   - AdminUserDO loginUser = authService.login0(username, password);
  78 + AdminUserDO loginUser = authService.authenticate(username, password);
78 79 // 校验
79 80 assertPojoEquals(user, loginUser);
80 81 }
81 82  
82 83 @Test
83   - public void testLogin0_userNotFound() {
  84 + public void testAuthenticate_userNotFound() {
84 85 // 准备参数
85 86 String username = randomString();
86 87 String password = randomString();
87 88  
88 89 // 调用, 并断言异常
89   - AssertUtils.assertServiceException(() -> authService.login0(username, password),
  90 + AssertUtils.assertServiceException(() -> authService.authenticate(username, password),
90 91 AUTH_LOGIN_BAD_CREDENTIALS);
91 92 verify(loginLogService).createLoginLog(
92 93 argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
... ... @@ -96,7 +97,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
96 97 }
97 98  
98 99 @Test
99   - public void testLogin0_badCredentials() {
  100 + public void testAuthenticate_badCredentials() {
100 101 // 准备参数
101 102 String username = randomString();
102 103 String password = randomString();
... ... @@ -106,7 +107,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
106 107 when(userService.getUserByUsername(eq(username))).thenReturn(user);
107 108  
108 109 // 调用, 并断言异常
109   - AssertUtils.assertServiceException(() -> authService.login0(username, password),
  110 + AssertUtils.assertServiceException(() -> authService.authenticate(username, password),
110 111 AUTH_LOGIN_BAD_CREDENTIALS);
111 112 verify(loginLogService).createLoginLog(
112 113 argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
... ... @@ -116,7 +117,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
116 117 }
117 118  
118 119 @Test
119   - public void testLogin0_userDisabled() {
  120 + public void testAuthenticate_userDisabled() {
120 121 // 准备参数
121 122 String username = randomString();
122 123 String password = randomString();
... ... @@ -128,7 +129,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
128 129 when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true);
129 130  
130 131 // 调用, 并断言异常
131   - AssertUtils.assertServiceException(() -> authService.login0(username, password),
  132 + AssertUtils.assertServiceException(() -> authService.authenticate(username, password),
132 133 AUTH_LOGIN_USER_DISABLED);
133 134 verify(loginLogService).createLoginLog(
134 135 argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
... ... @@ -200,7 +201,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
200 201 // mock 缓存登录用户到 Redis
201 202 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
202 203 .setUserType(UserTypeEnum.ADMIN.getValue()));
203   - when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default")))
  204 + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull()))
204 205 .thenReturn(accessTokenDO);
205 206  
206 207 // 调用, 并断言异常
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImplTest.java
... ... @@ -3,12 +3,13 @@ package cn.iocoder.yudao.module.system.service.auth;
3 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
6   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO;
7   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO;
8   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO;
9   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
10   -import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper;
  6 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO;
  7 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO;
  8 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO;
  9 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
  10 +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ClientMapper;
11 11 import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
  12 +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientServiceImpl;
12 13 import org.junit.jupiter.api.Disabled;
13 14 import org.junit.jupiter.api.Test;
14 15 import org.springframework.boot.test.mock.mockito.MockBean;
... ...
yudao-ui-admin/src/api/login.js
... ... @@ -109,3 +109,40 @@ export function refreshToken() {
109 109 method: 'post'
110 110 })
111 111 }
  112 +
  113 +// ========== OAUTH 2.0 相关 ==========
  114 +
  115 +export function getAuthorize(clientId) {
  116 + return request({
  117 + url: '/system/oauth2/authorize?clientId=' + clientId,
  118 + method: 'get'
  119 + })
  120 +}
  121 +
  122 +export function authorize(responseType, clientId, redirectUri, state,
  123 + autoApprove, checkedScopes, uncheckedScopes) {
  124 + // 构建 scopes
  125 + const scopes = {};
  126 + for (const scope of checkedScopes) {
  127 + scopes[scope] = true;
  128 + }
  129 + for (const scope of uncheckedScopes) {
  130 + scopes[scope] = false;
  131 + }
  132 + // 发起请求
  133 + return service({
  134 + url: '/system/oauth2/authorize',
  135 + headers:{
  136 + 'Content-type': 'application/x-www-form-urlencoded',
  137 + },
  138 + params: {
  139 + response_type: responseType,
  140 + client_id: clientId,
  141 + redirect_uri: redirectUri,
  142 + state: state,
  143 + auto_approve: autoApprove,
  144 + scope: JSON.stringify(scopes)
  145 + },
  146 + method: 'post'
  147 + })
  148 +}
... ...
yudao-ui-admin/src/router/index.js
... ... @@ -43,6 +43,11 @@ export const constantRoutes = [
43 43 hidden: true
44 44 },
45 45 {
  46 + path: '/sso',
  47 + component: (resolve) => require(['@/views/sso'], resolve),
  48 + hidden: true
  49 + },
  50 + {
46 51 path: '/social-login',
47 52 component: (resolve) => require(['@/views/socialLogin'], resolve),
48 53 hidden: true
... ...
yudao-ui-admin/src/views/sso.vue 0 → 100644
  1 +<template>
  2 + <div class="container">
  3 + <div class="logo"></div>
  4 + <!-- 登录区域 -->
  5 + <div class="content">
  6 + <!-- 配图 -->
  7 + <div class="pic"></div>
  8 + <!-- 表单 -->
  9 + <div class="field">
  10 + <!-- [移动端]标题 -->
  11 + <h2 class="mobile-title">
  12 + <h3 class="title">芋道后台管理系统</h3>
  13 + </h2>
  14 +
  15 + <!-- 表单 -->
  16 + <div class="form-cont">
  17 + <el-tabs class="form" style=" float:none;" value="uname">
  18 + <el-tab-pane :label="'三方授权(' + client.name + ')'" name="uname">
  19 + </el-tab-pane>
  20 + </el-tabs>
  21 + <div>
  22 + <el-form ref="loginForm" :model="loginForm" :rules="LoginRules" class="login-form">
  23 + <el-form-item prop="tenantName" v-if="tenantEnable">
  24 + <el-input v-model="loginForm.tenantName" type="text" auto-complete="off" placeholder='租户'>
  25 + <svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon"/>
  26 + </el-input>
  27 + </el-form-item>
  28 + <!-- 授权范围的选择 -->
  29 + 此第三方应用请求获得以下权限:
  30 + <el-form-item prop="scopes">
  31 + <el-checkbox-group v-model="loginForm.scopes">
  32 + <el-checkbox v-for="scope in params.scopes" :label="scope" :key="scope"
  33 + style="display: block; margin-bottom: -10px;">{{formatScope(scope)}}</el-checkbox>
  34 + </el-checkbox-group>
  35 + </el-form-item>
  36 + <!-- 下方的登录按钮 -->
  37 + <el-form-item style="width:100%;">
  38 + <el-button :loading="loading" size="medium" type="primary" style="width:60%;"
  39 + @click.native.prevent="handleAuthorize(true)">
  40 + <span v-if="!loading">统一授权</span>
  41 + <span v-else>授 权 中...</span>
  42 + </el-button>
  43 + <el-button size="medium" style="width:36%"
  44 + @click.native.prevent="handleAuthorize(false)">拒绝</el-button>
  45 + </el-form-item>
  46 + </el-form>
  47 + </div>
  48 + </div>
  49 + </div>
  50 + </div>
  51 + <!-- footer -->
  52 + <div class="footer">
  53 + Copyright © 2020-2022 iocoder.cn All Rights Reserved.
  54 + </div>
  55 + </div>
  56 +</template>
  57 +
  58 +<script>
  59 +import {getTenantIdByName} from "@/api/system/tenant";
  60 +import {getTenantEnable} from "@/utils/ruoyi";
  61 +import {authorize, getAuthorize} from "@/api/login";
  62 +import {getTenantName, setTenantId} from "@/utils/auth";
  63 +
  64 +export default {
  65 + name: "Login",
  66 + data() {
  67 + return {
  68 + tenantEnable: true,
  69 + loginForm: {
  70 + tenantName: "芋道源码",
  71 + scopes: [], // 已选中的 scope 数组
  72 + },
  73 + params: { // URL 上的 client_id、scope 等参数
  74 + responseType: undefined,
  75 + clientId: undefined,
  76 + redirectUri: undefined,
  77 + state: undefined,
  78 + scopes: [], // 优先从 query 参数获取;如果未传递,从后端获取
  79 + },
  80 + client: { // 客户端信息
  81 + name: '',
  82 + logo: '',
  83 + },
  84 + LoginRules: {
  85 + tenantName: [
  86 + {required: true, trigger: "blur", message: "租户不能为空"},
  87 + {
  88 + validator: (rule, value, callback) => {
  89 + // debugger
  90 + getTenantIdByName(value).then(res => {
  91 + const tenantId = res.data;
  92 + if (tenantId && tenantId >= 0) {
  93 + // 设置租户
  94 + setTenantId(tenantId)
  95 + callback();
  96 + } else {
  97 + callback('租户不存在');
  98 + }
  99 + });
  100 + },
  101 + trigger: 'blur'
  102 + }
  103 + ]
  104 + },
  105 + loading: false
  106 + };
  107 + },
  108 + created() {
  109 + // 租户开关
  110 + this.tenantEnable = getTenantEnable();
  111 + this.getCookie();
  112 +
  113 + // 解析参数
  114 + // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
  115 + // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
  116 + this.params.responseType = this.$route.query.response_type
  117 + this.params.clientId = this.$route.query.client_id
  118 + this.params.redirectUri = this.$route.query.redirect_uri
  119 + this.params.state = this.$route.query.state
  120 + if (this.$route.query.scope) {
  121 + this.params.scopes = this.$route.query.scope.split(' ')
  122 + }
  123 +
  124 + // 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
  125 + if (this.params.scopes.length > 0) {
  126 + this.doAuthorize(true, this.params.scopes, []).then(res => {
  127 + const href = res.data
  128 + if (!href) {
  129 + console.log('自动授权未通过!')
  130 + return;
  131 + }
  132 + location.href = href
  133 + })
  134 + }
  135 +
  136 + // 获取授权页的基本信息
  137 + getAuthorize(this.params.clientId).then(res => {
  138 + this.client = res.data.client
  139 + // 解析 scope
  140 + let scopes
  141 + // 1.1 如果 params.scope 非空,则过滤下返回的 scopes
  142 + if (this.params.scopes.length > 0) {
  143 + scopes = []
  144 + for (const scope of res.data.scopes) {
  145 + if (this.params.scopes.indexOf(scope.key) >= 0) {
  146 + scopes.push(scope)
  147 + }
  148 + }
  149 + // 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
  150 + } else {
  151 + scopes = res.data.scopes
  152 + for (const scope of scopes) {
  153 + this.params.scopes.push(scope.key)
  154 + }
  155 + }
  156 + // 生成已选中的 checkedScopes
  157 + for (const scope of scopes) {
  158 + if (scope.value) {
  159 + this.loginForm.scopes.push(scope.key)
  160 + }
  161 + }
  162 + })
  163 + },
  164 + methods: {
  165 + getCookie() {
  166 + const tenantName = getTenantName();
  167 + this.loginForm = {
  168 + ...this.loginForm,
  169 + tenantName: tenantName ? tenantName : this.loginForm.tenantName,
  170 + };
  171 + },
  172 + handleAuthorize(approved) {
  173 + this.$refs.loginForm.validate(valid => {
  174 + if (!valid) {
  175 + return
  176 + }
  177 + this.loading = true
  178 + // 计算 checkedScopes + uncheckedScopes
  179 + let checkedScopes;
  180 + let uncheckedScopes;
  181 + if (approved) { // 同意授权,按照用户的选择
  182 + checkedScopes = this.loginForm.scopes
  183 + uncheckedScopes = this.params.scopes.filter(item => checkedScopes.indexOf(item) === -1)
  184 + } else { // 拒绝,则都是取消
  185 + checkedScopes = []
  186 + uncheckedScopes = this.params.scopes
  187 + }
  188 + // 提交授权的请求
  189 + this.doAuthorize(false, checkedScopes, uncheckedScopes).then(res => {
  190 + const href = res.data
  191 + if (!href) {
  192 + return;
  193 + }
  194 + location.href = href
  195 + }).finally(() => {
  196 + this.loading = false
  197 + })
  198 + })
  199 + },
  200 + doAuthorize(autoApprove, checkedScopes, uncheckedScopes) {
  201 + return authorize(this.params.responseType, this.params.clientId, this.params.redirectUri, this.params.state,
  202 + autoApprove, checkedScopes, uncheckedScopes)
  203 + },
  204 + formatScope(scope) {
  205 + // 格式化 scope 授权范围,方便用户理解。
  206 + // 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
  207 + switch (scope) {
  208 + case 'user.read': return '访问你的个人信息'
  209 + case 'user.write': return '修改你的个人信息'
  210 + default: return scope
  211 + }
  212 + }
  213 + }
  214 +};
  215 +</script>
  216 +<style lang="scss" scoped>
  217 +@import "~@/assets/styles/login.scss";
  218 +.oauth-login {
  219 + display: flex;
  220 + align-items: cen;
  221 + cursor:pointer;
  222 +}
  223 +.oauth-login-item {
  224 + display: flex;
  225 + align-items: center;
  226 + margin-right: 10px;
  227 +}
  228 +.oauth-login-item img {
  229 + height: 25px;
  230 + width: 25px;
  231 +}
  232 +.oauth-login-item span:hover {
  233 + text-decoration: underline red;
  234 + color: red;
  235 +}
  236 +</style>
... ...
yudao-ui-admin/src/views/system/oauth2/client/index.vue
... ... @@ -108,23 +108,16 @@
108 108 <el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
109 109 </el-select>
110 110 </el-form-item>
111   - <el-form-item label="是否自动授权" prop="autoApprove">
112   - <el-radio-group v-model="form.autoApprove">
113   - <el-radio :key="true" :label="true">自动登录</el-radio>
114   - <el-radio :key="false" :label="false">手动登录</el-radio>
115   - </el-radio-group>
116   - </el-form-item>
117   - <el-form-item label="授权类型" prop="authorizedGrantTypes">
118   - <el-select v-model="form.authorizedGrantTypes" multiple filterable placeholder="请输入授权类型" style="width: 500px" >
119   - <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
120   - :key="dict.value" :label="dict.label" :value="dict.value"/>
121   - </el-select>
122   - </el-form-item>
123 111 <el-form-item label="授权范围" prop="scopes">
124 112 <el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" >
125 113 <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
126 114 </el-select>
127 115 </el-form-item>
  116 + <el-form-item label="自动授权" prop="autoApproveScopes">
  117 + <el-select v-model="form.autoApproveScopes" multiple filterable placeholder="请输入授权范围" style="width: 500px" >
  118 + <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
  119 + </el-select>
  120 + </el-form-item>
128 121 <el-form-item label="权限" prop="authorities">
129 122 <el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
130 123 <el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>
... ... @@ -196,7 +189,6 @@ export default {
196 189 accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }],
197 190 refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }],
198 191 redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }],
199   - autoApprove: [{ required: true, message: "是否自动授权不能为空", trigger: "blur" }],
200 192 authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }],
201 193 }
202 194 };
... ... @@ -235,9 +227,9 @@ export default {
235 227 accessTokenValiditySeconds: 30 * 60,
236 228 refreshTokenValiditySeconds: 30 * 24 * 60,
237 229 redirectUris: [],
238   - autoApprove: true,
239 230 authorizedGrantTypes: [],
240 231 scopes: [],
  232 + autoApproveScopes: [],
241 233 authorities: [],
242 234 resourceIds: [],
243 235 additionalInformation: undefined,
... ...