Browse Source

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

YunaiV 2 years ago
parent
commit
ce60ec0815

+ 44 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2Controller.java

@@ -1,11 +1,22 @@
 package cn.iocoder.yudao.module.system.controller.admin.auth;
 
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
+
 @Api(tags = "管理后台 - OAuth2.0 授权")
 @RestController
 @RequestMapping("/system/oauth2")
@@ -21,4 +32,37 @@ public class OAuth2Controller {
 
 //    GET  oauth/authorize AuthorizationEndpoint
 
+    @PostMapping("/authorize")
+    @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被调用")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class),
+            @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class),
+            @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 多个使用逗号分隔
+            @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class),
+            @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class)
+    })
+    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
+    // 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理
+    public CommonResult<String> authorize(@RequestParam("response_type") String responseType,
+                                          @RequestParam("client_id") String clientId,
+                                          @RequestParam(value = "scope", required = false) String scope,
+                                          @RequestParam("redirect_uri") String redirectUri,
+                                          @RequestParam(value = "state", required = false) String state) {
+        // 0. 校验用户已经登录。通过 Spring Security 实现
+
+        // 1.1 校验 responseType 是否满足 code 或者 token 值
+        if (!StrUtil.equalsAny(responseType, "code", "token")) {
+            throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token");
+        }
+        // 1.2 校验 redirectUri 重定向域名是否合法
+
+        // 1.3 校验 scope 是否在 Client 授权范围内
+
+        // 2.1 如果是 code 授权码模式,则发放 code 授权码,并重定向
+
+        // 2.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向
+        // TODO 需要确认,是否要 refreshToken 生成
+        return CommonResult.success("");
+    }
+
 }

+ 3 - 4
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 {
     private List<@NotEmpty(message = "重定向的 URI 不能为空")
         @URL(message = "重定向的 URI 格式不正确") String> redirectUris;
 
-    @ApiModelProperty(value = "是否自动授权", required = true, example = "true")
-    @NotNull(message = "是否自动授权不能为空")
-    private Boolean autoApprove;
-
     @ApiModelProperty(value = "授权类型", required = true, example = "password", notes = "参见 OAuth2GrantTypeEnum 枚举")
     @NotNull(message = "授权类型不能为空")
     private List<String> authorizedGrantTypes;
@@ -66,6 +62,9 @@ public class OAuth2ClientBaseVO {
     @ApiModelProperty(value = "授权范围", example = "user_info")
     private List<String> scopes;
 
+    @ApiModelProperty(value = "自动通过的授权范围", example = "user_info")
+    private List<String> autoApproveScopes;
+
     @ApiModelProperty(value = "权限", example = "system:user:query")
     private List<String> authorities;
 

+ 7 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2ClientDO.java

@@ -70,10 +70,6 @@ public class OAuth2ClientDO extends BaseDO {
      */
     @TableField(typeHandler = JacksonTypeHandler.class)
     private List<String> redirectUris;
-    /**
-     * 是否自动授权
-     */
-    private Boolean autoApprove;
     /**
      * 授权类型(模式)
      *
@@ -86,6 +82,13 @@ public class OAuth2ClientDO extends BaseDO {
      */
     @TableField(typeHandler = JacksonTypeHandler.class)
     private List<String> scopes;
+    /**
+     * 自动授权的 Scope
+     *
+     * code 授权时,如果 scope 在这个范围内,则自动通过
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<String> autoApproveScopes;
     /**
      * 权限
      */

+ 17 - 0
yudao-ui-admin/src/api/login.js

@@ -109,3 +109,20 @@ export function refreshToken() {
     method: 'post'
   })
 }
+
+// ========== OAUTH 2.0 相关 ==========
+export function authorize() {
+  return service({
+    url: '/system/oauth2/authorize',
+    headers:{
+      'Content-type': 'application/x-www-form-urlencoded',
+      "Access-Control-Allow-Origin": "*"
+    },
+    params: {
+      response_type: 'code',
+      client_id: 'test',
+      redirect_uri: 'https://www.iocoder.cn',
+    },
+    method: 'post'
+  })
+}

+ 5 - 0
yudao-ui-admin/src/router/index.js

@@ -42,6 +42,11 @@ export const constantRoutes = [
     component: (resolve) => require(['@/views/login'], resolve),
     hidden: true
   },
+  {
+    path: '/authorize',
+    component: (resolve) => require(['@/views/authorize'], resolve),
+    hidden: true
+  },
   {
     path: '/social-login',
     component: (resolve) => require(['@/views/socialLogin'], resolve),

+ 169 - 0
yudao-ui-admin/src/views/authorize.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="container">
+    <div class="logo"></div>
+    <!-- 登录区域 -->
+    <div class="content">
+      <!-- 配图 -->
+      <div class="pic"></div>
+      <!-- 表单 -->
+      <div class="field">
+        <!-- [移动端]标题 -->
+        <h2 class="mobile-title">
+          <h3 class="title">芋道后台管理系统</h3>
+        </h2>
+
+        <!-- 表单 -->
+        <div class="form-cont">
+          <el-tabs class="form" style=" float:none;">
+            <el-tab-pane label="三方授权" name="uname">
+            </el-tab-pane>
+          </el-tabs>
+          <div>
+            <el-form ref="loginForm" :model="loginForm" :rules="LoginRules" class="login-form">
+              <el-form-item prop="tenantName" v-if="tenantEnable">
+                <el-input v-model="loginForm.tenantName" type="text" auto-complete="off" placeholder='租户'>
+                  <svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon"/>
+                </el-input>
+              </el-form-item>
+              <!-- 账号密码登录 -->
+              <div v-if="loginForm.loginType === 'uname'">
+                <el-form-item prop="username">
+                  <el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
+                    <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon"/>
+                  </el-input>
+                </el-form-item>
+                <el-form-item prop="password">
+                  <el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码"
+                            @keyup.enter.native="handleLogin">
+                    <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
+                  </el-input>
+                </el-form-item>
+                <el-form-item prop="code" v-if="captchaEnable">
+                  <el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
+                            @keyup.enter.native="handleLogin">
+                    <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/>
+                  </el-input>
+                </el-form-item>
+              </div>
+
+              <!-- 下方的登录按钮 -->
+              <el-form-item style="width:100%;">
+                <el-button :loading="loading" size="medium" type="primary" style="width:60%;"
+                           @click.native.prevent="handleLogin">
+                  <span v-if="!loading">同意授权</span>
+                  <span v-else>登 录 中...</span>
+                </el-button>
+                <el-button size="medium" style="width:37%">拒绝</el-button>
+              </el-form-item>
+            </el-form>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- footer -->
+    <div class="footer">
+      Copyright © 2020-2022 iocoder.cn All Rights Reserved.
+    </div>
+  </div>
+</template>
+
+<script>
+import {getTenantIdByName} from "@/api/system/tenant";
+import Cookies from "js-cookie";
+import {SystemUserSocialTypeEnum} from "@/utils/constants";
+import {getTenantEnable} from "@/utils/ruoyi";
+import {authorize} from "@/api/login";
+
+export default {
+  name: "Login",
+  data() {
+    return {
+      tenantEnable: true,
+      loginForm: {
+        tenantName: "芋道源码",
+      },
+      LoginRules: {
+        tenantName: [
+          {required: true, trigger: "blur", message: "租户不能为空"},
+          {
+            validator: (rule, value, callback) => {
+              // debugger
+              getTenantIdByName(value).then(res => {
+                const tenantId = res.data;
+                if (tenantId && tenantId >= 0) {
+                  // 设置租户
+                  Cookies.set("tenantId", tenantId);
+                  callback();
+                } else {
+                  callback('租户不存在');
+                }
+              });
+            },
+            trigger: 'blur'
+          }
+        ]
+      },
+      loading: false,
+      redirect: undefined,
+      // 枚举
+      SysUserSocialTypeEnum: SystemUserSocialTypeEnum,
+    };
+  },
+  created() {
+    // 租户开关
+    this.tenantEnable = getTenantEnable();
+    // 重定向地址
+    this.redirect = this.$route.query.redirect;
+    this.getCookie();
+  },
+  methods: {
+    getCookie() {
+      const tenantName = Cookies.get('tenantName');
+      this.loginForm = {
+        tenantName: tenantName === undefined ? this.loginForm.tenantName : tenantName
+      };
+    },
+    handleLogin() {
+      if (true) {
+        authorize()
+        return;
+      }
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          // 发起登陆
+          console.log("发起登录", this.loginForm);
+          this.$store.dispatch(this.loginForm.loginType === "sms" ? "SmsLogin" : "Login", this.loginForm).then(() => {
+            this.$router.push({path: this.redirect || "/"}).catch(() => {
+            });
+          }).catch(() => {
+            this.loading = false;
+            this.getCode();
+          });
+        }
+      });
+    }
+  }
+};
+</script>
+<style lang="scss" scoped>
+@import "~@/assets/styles/login.scss";
+.oauth-login {
+  display: flex;
+  align-items: cen;
+  cursor:pointer;
+}
+.oauth-login-item {
+  display: flex;
+  align-items: center;
+  margin-right: 10px;
+}
+.oauth-login-item img {
+  height: 25px;
+  width: 25px;
+}
+.oauth-login-item span:hover {
+  text-decoration: underline red;
+  color: red;
+}
+</style>

+ 6 - 14
yudao-ui-admin/src/views/system/oauth2/client/index.vue

@@ -108,23 +108,16 @@
             <el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
           </el-select>
         </el-form-item>
-        <el-form-item label="是否自动授权" prop="autoApprove">
-          <el-radio-group v-model="form.autoApprove">
-            <el-radio :key="true" :label="true">自动登录</el-radio>
-            <el-radio :key="false" :label="false">手动登录</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="授权类型" prop="authorizedGrantTypes">
-          <el-select v-model="form.authorizedGrantTypes" multiple filterable placeholder="请输入授权类型" style="width: 500px" >
-            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
-                       :key="dict.value" :label="dict.label" :value="dict.value"/>
-          </el-select>
-        </el-form-item>
         <el-form-item label="授权范围" prop="scopes">
           <el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" >
             <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
           </el-select>
         </el-form-item>
+        <el-form-item label="自动授权" prop="autoApproveScopes">
+          <el-select v-model="form.autoApproveScopes" multiple filterable placeholder="请输入授权范围" style="width: 500px" >
+            <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
+          </el-select>
+        </el-form-item>
         <el-form-item label="权限" prop="authorities">
           <el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
             <el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>
@@ -196,7 +189,6 @@ export default {
         accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }],
         refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }],
         redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }],
-        autoApprove: [{ required: true, message: "是否自动授权不能为空", trigger: "blur" }],
         authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }],
       }
     };
@@ -235,9 +227,9 @@ export default {
         accessTokenValiditySeconds: 30 * 60,
         refreshTokenValiditySeconds: 30 * 24 * 60,
         redirectUris: [],
-        autoApprove: true,
         authorizedGrantTypes: [],
         scopes: [],
+        autoApproveScopes: [],
         authorities: [],
         resourceIds: [],
         additionalInformation: undefined,