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 | 14 | return update(updateObj, new LambdaQueryWrapperX<OAuth2ApproveDO>() |
| 15 | 15 | .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId()) |
| 16 | 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 | 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 | 26 | /** |
| 27 | 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 | 31 | @Resource |
| 32 | 32 | private OAuth2ClientService oauth2ClientService; | ... | ... |
yudao-ui-admin/src/views/sso.vue
| ... | ... | @@ -15,7 +15,7 @@ |
| 15 | 15 | <!-- 表单 --> |
| 16 | 16 | <div class="form-cont"> |
| 17 | 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 | 19 | </el-tab-pane> |
| 20 | 20 | </el-tabs> |
| 21 | 21 | <div> |
| ... | ... | @@ -25,35 +25,23 @@ |
| 25 | 25 | <svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon"/> |
| 26 | 26 | </el-input> |
| 27 | 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 | 37 | <el-form-item style="width:100%;"> |
| 51 | 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 | 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 | 45 | </el-form-item> |
| 58 | 46 | </el-form> |
| 59 | 47 | </div> |
| ... | ... | @@ -80,6 +68,7 @@ export default { |
| 80 | 68 | tenantEnable: true, |
| 81 | 69 | loginForm: { |
| 82 | 70 | tenantName: "芋道源码", |
| 71 | + scopes: [], // 已选中的 scope 数组 | |
| 83 | 72 | }, |
| 84 | 73 | params: { // URL 上的 client_id、scope 等参数 |
| 85 | 74 | responseType: undefined, |
| ... | ... | @@ -92,7 +81,6 @@ export default { |
| 92 | 81 | name: '', |
| 93 | 82 | logo: '', |
| 94 | 83 | }, |
| 95 | - checkedScopes: [], // 已选中的 scope 数组 | |
| 96 | 84 | LoginRules: { |
| 97 | 85 | tenantName: [ |
| 98 | 86 | {required: true, trigger: "blur", message: "租户不能为空"}, |
| ... | ... | @@ -114,8 +102,7 @@ export default { |
| 114 | 102 | } |
| 115 | 103 | ] |
| 116 | 104 | }, |
| 117 | - loading: false, | |
| 118 | - // | |
| 105 | + loading: false | |
| 119 | 106 | }; |
| 120 | 107 | }, |
| 121 | 108 | created() { |
| ... | ... | @@ -169,7 +156,7 @@ export default { |
| 169 | 156 | // 生成已选中的 checkedScopes |
| 170 | 157 | for (const scope of scopes) { |
| 171 | 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 | 165 | getCookie() { |
| 179 | 166 | const tenantName = getTenantName(); |
| 180 | 167 | this.loginForm = { |
| 168 | + ...this.loginForm, | |
| 181 | 169 | tenantName: tenantName ? tenantName : this.loginForm.tenantName, |
| 182 | 170 | }; |
| 183 | 171 | }, |
| 184 | - handleLogin() { | |
| 172 | + handleAuthorize(approved) { | |
| 185 | 173 | this.$refs.loginForm.validate(valid => { |
| 186 | 174 | if (!valid) { |
| 187 | 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 | 200 | doAuthorize(autoApprove, checkedScopes, uncheckedScopes) { |
| 194 | 201 | return authorize(this.params.responseType, this.params.clientId, this.params.redirectUri, this.params.state, |
| 195 | 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 | }; | ... | ... |