Commit 82cf4e1775beeddbd0a08066e4414289a593f364
Committed by
Gitee
!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
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
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
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
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
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
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
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
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
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
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
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java
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
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, | ... | ... |