Commit 668551350f3d06b9d4bb28ee5fca1f14b4574fb3
1 parent
d1846386
完成 SSO 登录的功能
Showing
3 changed files
with
52 additions
and
35 deletions
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java
| @@ -14,7 +14,8 @@ public interface OAuth2ApproveMapper extends BaseMapperX<OAuth2ApproveDO> { | @@ -14,7 +14,8 @@ public interface OAuth2ApproveMapper extends BaseMapperX<OAuth2ApproveDO> { | ||
| 14 | return update(updateObj, new LambdaQueryWrapperX<OAuth2ApproveDO>() | 14 | return update(updateObj, new LambdaQueryWrapperX<OAuth2ApproveDO>() |
| 15 | .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId()) | 15 | .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId()) |
| 16 | .eq(OAuth2ApproveDO::getUserType, updateObj.getUserType()) | 16 | .eq(OAuth2ApproveDO::getUserType, updateObj.getUserType()) |
| 17 | - .eq(OAuth2ApproveDO::getClientId, updateObj.getClientId())); | 17 | + .eq(OAuth2ApproveDO::getClientId, updateObj.getClientId()) |
| 18 | + .eq(OAuth2ApproveDO::getScope, updateObj.getScope())); | ||
| 18 | } | 19 | } |
| 19 | 20 | ||
| 20 | default List<OAuth2ApproveDO> selectListByUserIdAndUserTypeAndClientId(Long userId, Integer userType, String clientId) { | 21 | default List<OAuth2ApproveDO> selectListByUserIdAndUserTypeAndClientId(Long userId, Integer userType, String clientId) { |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImpl.java
| @@ -26,7 +26,7 @@ public class OAuth2ApproveServiceImpl implements OAuth2ApproveService { | @@ -26,7 +26,7 @@ public class OAuth2ApproveServiceImpl implements OAuth2ApproveService { | ||
| 26 | /** | 26 | /** |
| 27 | * 批准的过期时间,默认 30 天 | 27 | * 批准的过期时间,默认 30 天 |
| 28 | */ | 28 | */ |
| 29 | - private static final Integer TIMEOUT = 30 * 24 * 60; // 单位:秒 | 29 | + private static final Integer TIMEOUT = 30 * 24 * 60 * 60; // 单位:秒 |
| 30 | 30 | ||
| 31 | @Resource | 31 | @Resource |
| 32 | private OAuth2ClientService oauth2ClientService; | 32 | private OAuth2ClientService oauth2ClientService; |
yudao-ui-admin/src/views/sso.vue
| @@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
| 15 | <!-- 表单 --> | 15 | <!-- 表单 --> |
| 16 | <div class="form-cont"> | 16 | <div class="form-cont"> |
| 17 | <el-tabs class="form" style=" float:none;" value="uname"> | 17 | <el-tabs class="form" style=" float:none;" value="uname"> |
| 18 | - <el-tab-pane label="三方授权" name="uname"> | 18 | + <el-tab-pane :label="'三方授权(' + client.name + ')'" name="uname"> |
| 19 | </el-tab-pane> | 19 | </el-tab-pane> |
| 20 | </el-tabs> | 20 | </el-tabs> |
| 21 | <div> | 21 | <div> |
| @@ -25,35 +25,23 @@ | @@ -25,35 +25,23 @@ | ||
| 25 | <svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon"/> | 25 | <svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon"/> |
| 26 | </el-input> | 26 | </el-input> |
| 27 | </el-form-item> | 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 | - | 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> | ||
| 49 | <!-- 下方的登录按钮 --> | 36 | <!-- 下方的登录按钮 --> |
| 50 | <el-form-item style="width:100%;"> | 37 | <el-form-item style="width:100%;"> |
| 51 | <el-button :loading="loading" size="medium" type="primary" style="width:60%;" | 38 | <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> | 39 | + @click.native.prevent="handleAuthorize(true)"> |
| 40 | + <span v-if="!loading">统一授权</span> | ||
| 41 | + <span v-else>授 权 中...</span> | ||
| 55 | </el-button> | 42 | </el-button> |
| 56 | - <el-button size="medium" style="width:36%">拒绝</el-button> | 43 | + <el-button size="medium" style="width:36%" |
| 44 | + @click.native.prevent="handleAuthorize(false)">拒绝</el-button> | ||
| 57 | </el-form-item> | 45 | </el-form-item> |
| 58 | </el-form> | 46 | </el-form> |
| 59 | </div> | 47 | </div> |
| @@ -80,6 +68,7 @@ export default { | @@ -80,6 +68,7 @@ export default { | ||
| 80 | tenantEnable: true, | 68 | tenantEnable: true, |
| 81 | loginForm: { | 69 | loginForm: { |
| 82 | tenantName: "芋道源码", | 70 | tenantName: "芋道源码", |
| 71 | + scopes: [], // 已选中的 scope 数组 | ||
| 83 | }, | 72 | }, |
| 84 | params: { // URL 上的 client_id、scope 等参数 | 73 | params: { // URL 上的 client_id、scope 等参数 |
| 85 | responseType: undefined, | 74 | responseType: undefined, |
| @@ -92,7 +81,6 @@ export default { | @@ -92,7 +81,6 @@ export default { | ||
| 92 | name: '', | 81 | name: '', |
| 93 | logo: '', | 82 | logo: '', |
| 94 | }, | 83 | }, |
| 95 | - checkedScopes: [], // 已选中的 scope 数组 | ||
| 96 | LoginRules: { | 84 | LoginRules: { |
| 97 | tenantName: [ | 85 | tenantName: [ |
| 98 | {required: true, trigger: "blur", message: "租户不能为空"}, | 86 | {required: true, trigger: "blur", message: "租户不能为空"}, |
| @@ -114,8 +102,7 @@ export default { | @@ -114,8 +102,7 @@ export default { | ||
| 114 | } | 102 | } |
| 115 | ] | 103 | ] |
| 116 | }, | 104 | }, |
| 117 | - loading: false, | ||
| 118 | - // | 105 | + loading: false |
| 119 | }; | 106 | }; |
| 120 | }, | 107 | }, |
| 121 | created() { | 108 | created() { |
| @@ -169,7 +156,7 @@ export default { | @@ -169,7 +156,7 @@ export default { | ||
| 169 | // 生成已选中的 checkedScopes | 156 | // 生成已选中的 checkedScopes |
| 170 | for (const scope of scopes) { | 157 | for (const scope of scopes) { |
| 171 | if (scope.value) { | 158 | if (scope.value) { |
| 172 | - this.checkedScopes.push(scope.key) | 159 | + this.loginForm.scopes.push(scope.key) |
| 173 | } | 160 | } |
| 174 | } | 161 | } |
| 175 | }) | 162 | }) |
| @@ -178,21 +165,50 @@ export default { | @@ -178,21 +165,50 @@ export default { | ||
| 178 | getCookie() { | 165 | getCookie() { |
| 179 | const tenantName = getTenantName(); | 166 | const tenantName = getTenantName(); |
| 180 | this.loginForm = { | 167 | this.loginForm = { |
| 168 | + ...this.loginForm, | ||
| 181 | tenantName: tenantName ? tenantName : this.loginForm.tenantName, | 169 | tenantName: tenantName ? tenantName : this.loginForm.tenantName, |
| 182 | }; | 170 | }; |
| 183 | }, | 171 | }, |
| 184 | - handleLogin() { | 172 | + handleAuthorize(approved) { |
| 185 | this.$refs.loginForm.validate(valid => { | 173 | this.$refs.loginForm.validate(valid => { |
| 186 | if (!valid) { | 174 | if (!valid) { |
| 187 | return | 175 | return |
| 188 | } | 176 | } |
| 189 | - | ||
| 190 | - | 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 | + }) | ||
| 191 | }) | 198 | }) |
| 192 | }, | 199 | }, |
| 193 | doAuthorize(autoApprove, checkedScopes, uncheckedScopes) { | 200 | doAuthorize(autoApprove, checkedScopes, uncheckedScopes) { |
| 194 | return authorize(this.params.responseType, this.params.clientId, this.params.redirectUri, this.params.state, | 201 | return authorize(this.params.responseType, this.params.clientId, this.params.redirectUri, this.params.state, |
| 195 | autoApprove, checkedScopes, uncheckedScopes) | 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 | + } | ||
| 196 | } | 212 | } |
| 197 | } | 213 | } |
| 198 | }; | 214 | }; |