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 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 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 11 import lombok.extern.slf4j.Slf4j;
5 12 import org.springframework.validation.annotation.Validated;
  13 +import org.springframework.web.bind.annotation.PostMapping;
6 14 import org.springframework.web.bind.annotation.RequestMapping;
  15 +import org.springframework.web.bind.annotation.RequestParam;
7 16 import org.springframework.web.bind.annotation.RestController;
8 17  
  18 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
  19 +
9 20 @Api(tags = "管理后台 - OAuth2.0 授权")
10 21 @RestController
11 22 @RequestMapping("/system/oauth2")
... ... @@ -21,4 +32,37 @@ public class OAuth2Controller {
21 32  
22 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 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/dal/dataobject/auth/OAuth2ClientDO.java
... ... @@ -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-ui-admin/src/api/login.js
... ... @@ -109,3 +109,20 @@ export function refreshToken() {
109 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 43 hidden: true
44 44 },
45 45 {
  46 + path: '/authorize',
  47 + component: (resolve) => require(['@/views/authorize'], 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/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 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,
... ...