Commit ce60ec08156f3b84d03c9db62f41abbf97d6f2d6

Authored by YunaiV
1 parent 6c5f5e1a

修改 OAuth2ClientDO 实体,支持自动授权的范围的设置

yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2Controller.java
1 package cn.iocoder.yudao.module.system.controller.admin.auth; 1 package cn.iocoder.yudao.module.system.controller.admin.auth;
2 2
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
3 import io.swagger.annotations.Api; 7 import io.swagger.annotations.Api;
  8 +import io.swagger.annotations.ApiImplicitParam;
  9 +import io.swagger.annotations.ApiImplicitParams;
  10 +import io.swagger.annotations.ApiOperation;
4 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
5 import org.springframework.validation.annotation.Validated; 12 import org.springframework.validation.annotation.Validated;
  13 +import org.springframework.web.bind.annotation.PostMapping;
6 import org.springframework.web.bind.annotation.RequestMapping; 14 import org.springframework.web.bind.annotation.RequestMapping;
  15 +import org.springframework.web.bind.annotation.RequestParam;
7 import org.springframework.web.bind.annotation.RestController; 16 import org.springframework.web.bind.annotation.RestController;
8 17
  18 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
  19 +
9 @Api(tags = "管理后台 - OAuth2.0 授权") 20 @Api(tags = "管理后台 - OAuth2.0 授权")
10 @RestController 21 @RestController
11 @RequestMapping("/system/oauth2") 22 @RequestMapping("/system/oauth2")
@@ -21,4 +32,37 @@ public class OAuth2Controller { @@ -21,4 +32,37 @@ public class OAuth2Controller {
21 32
22 // GET oauth/authorize AuthorizationEndpoint 33 // GET oauth/authorize AuthorizationEndpoint
23 34
  35 + @PostMapping("/authorize")
  36 + @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被调用")
  37 + @ApiImplicitParams({
  38 + @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class),
  39 + @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class),
  40 + @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 多个使用逗号分隔
  41 + @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class),
  42 + @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class)
  43 + })
  44 + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  45 + // 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理
  46 + public CommonResult<String> authorize(@RequestParam("response_type") String responseType,
  47 + @RequestParam("client_id") String clientId,
  48 + @RequestParam(value = "scope", required = false) String scope,
  49 + @RequestParam("redirect_uri") String redirectUri,
  50 + @RequestParam(value = "state", required = false) String state) {
  51 + // 0. 校验用户已经登录。通过 Spring Security 实现
  52 +
  53 + // 1.1 校验 responseType 是否满足 code 或者 token 值
  54 + if (!StrUtil.equalsAny(responseType, "code", "token")) {
  55 + throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token");
  56 + }
  57 + // 1.2 校验 redirectUri 重定向域名是否合法
  58 +
  59 + // 1.3 校验 scope 是否在 Client 授权范围内
  60 +
  61 + // 2.1 如果是 code 授权码模式,则发放 code 授权码,并重定向
  62 +
  63 + // 2.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向
  64 + // TODO 需要确认,是否要 refreshToken 生成
  65 + return CommonResult.success("");
  66 + }
  67 +
24 } 68 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientBaseVO.java
@@ -55,10 +55,6 @@ public class OAuth2ClientBaseVO { @@ -55,10 +55,6 @@ public class OAuth2ClientBaseVO {
55 private List<@NotEmpty(message = "重定向的 URI 不能为空") 55 private List<@NotEmpty(message = "重定向的 URI 不能为空")
56 @URL(message = "重定向的 URI 格式不正确") String> redirectUris; 56 @URL(message = "重定向的 URI 格式不正确") String> redirectUris;
57 57
58 - @ApiModelProperty(value = "是否自动授权", required = true, example = "true")  
59 - @NotNull(message = "是否自动授权不能为空")  
60 - private Boolean autoApprove;  
61 -  
62 @ApiModelProperty(value = "授权类型", required = true, example = "password", notes = "参见 OAuth2GrantTypeEnum 枚举") 58 @ApiModelProperty(value = "授权类型", required = true, example = "password", notes = "参见 OAuth2GrantTypeEnum 枚举")
63 @NotNull(message = "授权类型不能为空") 59 @NotNull(message = "授权类型不能为空")
64 private List<String> authorizedGrantTypes; 60 private List<String> authorizedGrantTypes;
@@ -66,6 +62,9 @@ public class OAuth2ClientBaseVO { @@ -66,6 +62,9 @@ public class OAuth2ClientBaseVO {
66 @ApiModelProperty(value = "授权范围", example = "user_info") 62 @ApiModelProperty(value = "授权范围", example = "user_info")
67 private List<String> scopes; 63 private List<String> scopes;
68 64
  65 + @ApiModelProperty(value = "自动通过的授权范围", example = "user_info")
  66 + private List<String> autoApproveScopes;
  67 +
69 @ApiModelProperty(value = "权限", example = "system:user:query") 68 @ApiModelProperty(value = "权限", example = "system:user:query")
70 private List<String> authorities; 69 private List<String> authorities;
71 70
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2ClientDO.java
@@ -71,10 +71,6 @@ public class OAuth2ClientDO extends BaseDO { @@ -71,10 +71,6 @@ public class OAuth2ClientDO extends BaseDO {
71 @TableField(typeHandler = JacksonTypeHandler.class) 71 @TableField(typeHandler = JacksonTypeHandler.class)
72 private List<String> redirectUris; 72 private List<String> redirectUris;
73 /** 73 /**
74 - * 是否自动授权  
75 - */  
76 - private Boolean autoApprove;  
77 - /**  
78 * 授权类型(模式) 74 * 授权类型(模式)
79 * 75 *
80 * 枚举 {@link OAuth2GrantTypeEnum} 76 * 枚举 {@link OAuth2GrantTypeEnum}
@@ -87,6 +83,13 @@ public class OAuth2ClientDO extends BaseDO { @@ -87,6 +83,13 @@ public class OAuth2ClientDO extends BaseDO {
87 @TableField(typeHandler = JacksonTypeHandler.class) 83 @TableField(typeHandler = JacksonTypeHandler.class)
88 private List<String> scopes; 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 @TableField(typeHandler = JacksonTypeHandler.class) 95 @TableField(typeHandler = JacksonTypeHandler.class)
yudao-ui-admin/src/api/login.js
@@ -109,3 +109,20 @@ export function refreshToken() { @@ -109,3 +109,20 @@ export function refreshToken() {
109 method: 'post' 109 method: 'post'
110 }) 110 })
111 } 111 }
  112 +
  113 +// ========== OAUTH 2.0 相关 ==========
  114 +export function authorize() {
  115 + return service({
  116 + url: '/system/oauth2/authorize',
  117 + headers:{
  118 + 'Content-type': 'application/x-www-form-urlencoded',
  119 + "Access-Control-Allow-Origin": "*"
  120 + },
  121 + params: {
  122 + response_type: 'code',
  123 + client_id: 'test',
  124 + redirect_uri: 'https://www.iocoder.cn',
  125 + },
  126 + method: 'post'
  127 + })
  128 +}
yudao-ui-admin/src/router/index.js
@@ -43,6 +43,11 @@ export const constantRoutes = [ @@ -43,6 +43,11 @@ export const constantRoutes = [
43 hidden: true 43 hidden: true
44 }, 44 },
45 { 45 {
  46 + path: '/authorize',
  47 + component: (resolve) => require(['@/views/authorize'], resolve),
  48 + hidden: true
  49 + },
  50 + {
46 path: '/social-login', 51 path: '/social-login',
47 component: (resolve) => require(['@/views/socialLogin'], resolve), 52 component: (resolve) => require(['@/views/socialLogin'], resolve),
48 hidden: true 53 hidden: true
yudao-ui-admin/src/views/authorize.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;">
  18 + <el-tab-pane label="三方授权" 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 + <div v-if="loginForm.loginType === 'uname'">
  30 + <el-form-item prop="username">
  31 + <el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
  32 + <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon"/>
  33 + </el-input>
  34 + </el-form-item>
  35 + <el-form-item prop="password">
  36 + <el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码"
  37 + @keyup.enter.native="handleLogin">
  38 + <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
  39 + </el-input>
  40 + </el-form-item>
  41 + <el-form-item prop="code" v-if="captchaEnable">
  42 + <el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
  43 + @keyup.enter.native="handleLogin">
  44 + <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/>
  45 + </el-input>
  46 + </el-form-item>
  47 + </div>
  48 +
  49 + <!-- 下方的登录按钮 -->
  50 + <el-form-item style="width:100%;">
  51 + <el-button :loading="loading" size="medium" type="primary" style="width:60%;"
  52 + @click.native.prevent="handleLogin">
  53 + <span v-if="!loading">同意授权</span>
  54 + <span v-else>登 录 中...</span>
  55 + </el-button>
  56 + <el-button size="medium" style="width:37%">拒绝</el-button>
  57 + </el-form-item>
  58 + </el-form>
  59 + </div>
  60 + </div>
  61 + </div>
  62 + </div>
  63 + <!-- footer -->
  64 + <div class="footer">
  65 + Copyright © 2020-2022 iocoder.cn All Rights Reserved.
  66 + </div>
  67 + </div>
  68 +</template>
  69 +
  70 +<script>
  71 +import {getTenantIdByName} from "@/api/system/tenant";
  72 +import Cookies from "js-cookie";
  73 +import {SystemUserSocialTypeEnum} from "@/utils/constants";
  74 +import {getTenantEnable} from "@/utils/ruoyi";
  75 +import {authorize} from "@/api/login";
  76 +
  77 +export default {
  78 + name: "Login",
  79 + data() {
  80 + return {
  81 + tenantEnable: true,
  82 + loginForm: {
  83 + tenantName: "芋道源码",
  84 + },
  85 + LoginRules: {
  86 + tenantName: [
  87 + {required: true, trigger: "blur", message: "租户不能为空"},
  88 + {
  89 + validator: (rule, value, callback) => {
  90 + // debugger
  91 + getTenantIdByName(value).then(res => {
  92 + const tenantId = res.data;
  93 + if (tenantId && tenantId >= 0) {
  94 + // 设置租户
  95 + Cookies.set("tenantId", tenantId);
  96 + callback();
  97 + } else {
  98 + callback('租户不存在');
  99 + }
  100 + });
  101 + },
  102 + trigger: 'blur'
  103 + }
  104 + ]
  105 + },
  106 + loading: false,
  107 + redirect: undefined,
  108 + // 枚举
  109 + SysUserSocialTypeEnum: SystemUserSocialTypeEnum,
  110 + };
  111 + },
  112 + created() {
  113 + // 租户开关
  114 + this.tenantEnable = getTenantEnable();
  115 + // 重定向地址
  116 + this.redirect = this.$route.query.redirect;
  117 + this.getCookie();
  118 + },
  119 + methods: {
  120 + getCookie() {
  121 + const tenantName = Cookies.get('tenantName');
  122 + this.loginForm = {
  123 + tenantName: tenantName === undefined ? this.loginForm.tenantName : tenantName
  124 + };
  125 + },
  126 + handleLogin() {
  127 + if (true) {
  128 + authorize()
  129 + return;
  130 + }
  131 + this.$refs.loginForm.validate(valid => {
  132 + if (valid) {
  133 + this.loading = true;
  134 + // 发起登陆
  135 + console.log("发起登录", this.loginForm);
  136 + this.$store.dispatch(this.loginForm.loginType === "sms" ? "SmsLogin" : "Login", this.loginForm).then(() => {
  137 + this.$router.push({path: this.redirect || "/"}).catch(() => {
  138 + });
  139 + }).catch(() => {
  140 + this.loading = false;
  141 + this.getCode();
  142 + });
  143 + }
  144 + });
  145 + }
  146 + }
  147 +};
  148 +</script>
  149 +<style lang="scss" scoped>
  150 +@import "~@/assets/styles/login.scss";
  151 +.oauth-login {
  152 + display: flex;
  153 + align-items: cen;
  154 + cursor:pointer;
  155 +}
  156 +.oauth-login-item {
  157 + display: flex;
  158 + align-items: center;
  159 + margin-right: 10px;
  160 +}
  161 +.oauth-login-item img {
  162 + height: 25px;
  163 + width: 25px;
  164 +}
  165 +.oauth-login-item span:hover {
  166 + text-decoration: underline red;
  167 + color: red;
  168 +}
  169 +</style>
yudao-ui-admin/src/views/system/oauth2/client/index.vue
@@ -108,23 +108,16 @@ @@ -108,23 +108,16 @@
108 <el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/> 108 <el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
109 </el-select> 109 </el-select>
110 </el-form-item> 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 <el-form-item label="授权范围" prop="scopes"> 111 <el-form-item label="授权范围" prop="scopes">
124 <el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" > 112 <el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" >
125 <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/> 113 <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
126 </el-select> 114 </el-select>
127 </el-form-item> 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 <el-form-item label="权限" prop="authorities"> 121 <el-form-item label="权限" prop="authorities">
129 <el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" > 122 <el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
130 <el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/> 123 <el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>
@@ -196,7 +189,6 @@ export default { @@ -196,7 +189,6 @@ export default {
196 accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }], 189 accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }],
197 refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }], 190 refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }],
198 redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }], 191 redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }],
199 - autoApprove: [{ required: true, message: "是否自动授权不能为空", trigger: "blur" }],  
200 authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }], 192 authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }],
201 } 193 }
202 }; 194 };
@@ -235,9 +227,9 @@ export default { @@ -235,9 +227,9 @@ export default {
235 accessTokenValiditySeconds: 30 * 60, 227 accessTokenValiditySeconds: 30 * 60,
236 refreshTokenValiditySeconds: 30 * 24 * 60, 228 refreshTokenValiditySeconds: 30 * 24 * 60,
237 redirectUris: [], 229 redirectUris: [],
238 - autoApprove: true,  
239 authorizedGrantTypes: [], 230 authorizedGrantTypes: [],
240 scopes: [], 231 scopes: [],
  232 + autoApproveScopes: [],
241 authorities: [], 233 authorities: [],
242 resourceIds: [], 234 resourceIds: [],
243 additionalInformation: undefined, 235 additionalInformation: undefined,