Skip to content

Commit c86d9fa

Browse files
YunaiVgitee-org
authored andcommitted
!176 基于 OAuth2.0 实现 SSO 单点登录
Merge pull request !176 from 芋道源码/feature/1.6.2
2 parents 932fdda + 00dcea2 commit c86d9fa

File tree

4 files changed

+284
-14
lines changed

4 files changed

+284
-14
lines changed

src/api/login.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,40 @@ export function refreshToken() {
109109
method: 'post'
110110
})
111111
}
112+
113+
// ========== OAUTH 2.0 相关 ==========
114+
115+
export function getAuthorize(clientId) {
116+
return request({
117+
url: '/system/oauth2/authorize?clientId=' + clientId,
118+
method: 'get'
119+
})
120+
}
121+
122+
export function authorize(responseType, clientId, redirectUri, state,
123+
autoApprove, checkedScopes, uncheckedScopes) {
124+
// 构建 scopes
125+
const scopes = {};
126+
for (const scope of checkedScopes) {
127+
scopes[scope] = true;
128+
}
129+
for (const scope of uncheckedScopes) {
130+
scopes[scope] = false;
131+
}
132+
// 发起请求
133+
return service({
134+
url: '/system/oauth2/authorize',
135+
headers:{
136+
'Content-type': 'application/x-www-form-urlencoded',
137+
},
138+
params: {
139+
response_type: responseType,
140+
client_id: clientId,
141+
redirect_uri: redirectUri,
142+
state: state,
143+
auto_approve: autoApprove,
144+
scope: JSON.stringify(scopes)
145+
},
146+
method: 'post'
147+
})
148+
}

src/router/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export const constantRoutes = [
4242
component: (resolve) => require(['@/views/login'], resolve),
4343
hidden: true
4444
},
45+
{
46+
path: '/sso',
47+
component: (resolve) => require(['@/views/sso'], resolve),
48+
hidden: true
49+
},
4550
{
4651
path: '/social-login',
4752
component: (resolve) => require(['@/views/socialLogin'], resolve),

src/views/sso.vue

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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;" value="uname">
18+
<el-tab-pane :label="'三方授权(' + client.name + ')'" 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+
此第三方应用请求获得以下权限:
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>
36+
<!-- 下方的登录按钮 -->
37+
<el-form-item style="width:100%;">
38+
<el-button :loading="loading" size="medium" type="primary" style="width:60%;"
39+
@click.native.prevent="handleAuthorize(true)">
40+
<span v-if="!loading">统一授权</span>
41+
<span v-else>授 权 中...</span>
42+
</el-button>
43+
<el-button size="medium" style="width:36%"
44+
@click.native.prevent="handleAuthorize(false)">拒绝</el-button>
45+
</el-form-item>
46+
</el-form>
47+
</div>
48+
</div>
49+
</div>
50+
</div>
51+
<!-- footer -->
52+
<div class="footer">
53+
Copyright © 2020-2022 iocoder.cn All Rights Reserved.
54+
</div>
55+
</div>
56+
</template>
57+
58+
<script>
59+
import {getTenantIdByName} from "@/api/system/tenant";
60+
import {getTenantEnable} from "@/utils/ruoyi";
61+
import {authorize, getAuthorize} from "@/api/login";
62+
import {getTenantName, setTenantId} from "@/utils/auth";
63+
64+
export default {
65+
name: "Login",
66+
data() {
67+
return {
68+
tenantEnable: true,
69+
loginForm: {
70+
tenantName: "芋道源码",
71+
scopes: [], // 已选中的 scope 数组
72+
},
73+
params: { // URL 上的 client_id、scope 等参数
74+
responseType: undefined,
75+
clientId: undefined,
76+
redirectUri: undefined,
77+
state: undefined,
78+
scopes: [], // 优先从 query 参数获取;如果未传递,从后端获取
79+
},
80+
client: { // 客户端信息
81+
name: '',
82+
logo: '',
83+
},
84+
LoginRules: {
85+
tenantName: [
86+
{required: true, trigger: "blur", message: "租户不能为空"},
87+
{
88+
validator: (rule, value, callback) => {
89+
// debugger
90+
getTenantIdByName(value).then(res => {
91+
const tenantId = res.data;
92+
if (tenantId && tenantId >= 0) {
93+
// 设置租户
94+
setTenantId(tenantId)
95+
callback();
96+
} else {
97+
callback('租户不存在');
98+
}
99+
});
100+
},
101+
trigger: 'blur'
102+
}
103+
]
104+
},
105+
loading: false
106+
};
107+
},
108+
created() {
109+
// 租户开关
110+
this.tenantEnable = getTenantEnable();
111+
this.getCookie();
112+
113+
// 解析参数
114+
// 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
115+
// 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
116+
this.params.responseType = this.$route.query.response_type
117+
this.params.clientId = this.$route.query.client_id
118+
this.params.redirectUri = this.$route.query.redirect_uri
119+
this.params.state = this.$route.query.state
120+
if (this.$route.query.scope) {
121+
this.params.scopes = this.$route.query.scope.split(' ')
122+
}
123+
124+
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
125+
if (this.params.scopes.length > 0) {
126+
this.doAuthorize(true, this.params.scopes, []).then(res => {
127+
const href = res.data
128+
if (!href) {
129+
console.log('自动授权未通过!')
130+
return;
131+
}
132+
location.href = href
133+
})
134+
}
135+
136+
// 获取授权页的基本信息
137+
getAuthorize(this.params.clientId).then(res => {
138+
this.client = res.data.client
139+
// 解析 scope
140+
let scopes
141+
// 1.1 如果 params.scope 非空,则过滤下返回的 scopes
142+
if (this.params.scopes.length > 0) {
143+
scopes = []
144+
for (const scope of res.data.scopes) {
145+
if (this.params.scopes.indexOf(scope.key) >= 0) {
146+
scopes.push(scope)
147+
}
148+
}
149+
// 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
150+
} else {
151+
scopes = res.data.scopes
152+
for (const scope of scopes) {
153+
this.params.scopes.push(scope.key)
154+
}
155+
}
156+
// 生成已选中的 checkedScopes
157+
for (const scope of scopes) {
158+
if (scope.value) {
159+
this.loginForm.scopes.push(scope.key)
160+
}
161+
}
162+
})
163+
},
164+
methods: {
165+
getCookie() {
166+
const tenantName = getTenantName();
167+
this.loginForm = {
168+
...this.loginForm,
169+
tenantName: tenantName ? tenantName : this.loginForm.tenantName,
170+
};
171+
},
172+
handleAuthorize(approved) {
173+
this.$refs.loginForm.validate(valid => {
174+
if (!valid) {
175+
return
176+
}
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+
})
198+
})
199+
},
200+
doAuthorize(autoApprove, checkedScopes, uncheckedScopes) {
201+
return authorize(this.params.responseType, this.params.clientId, this.params.redirectUri, this.params.state,
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+
}
212+
}
213+
}
214+
};
215+
</script>
216+
<style lang="scss" scoped>
217+
@import "~@/assets/styles/login.scss";
218+
.oauth-login {
219+
display: flex;
220+
align-items: cen;
221+
cursor:pointer;
222+
}
223+
.oauth-login-item {
224+
display: flex;
225+
align-items: center;
226+
margin-right: 10px;
227+
}
228+
.oauth-login-item img {
229+
height: 25px;
230+
width: 25px;
231+
}
232+
.oauth-login-item span:hover {
233+
text-decoration: underline red;
234+
color: red;
235+
}
236+
</style>

src/views/system/oauth2/client/index.vue

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,23 +108,16 @@
108108
<el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
109109
</el-select>
110110
</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>
123111
<el-form-item label="授权范围" prop="scopes">
124112
<el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" >
125113
<el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
126114
</el-select>
127115
</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>
128121
<el-form-item label="权限" prop="authorities">
129122
<el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
130123
<el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>
@@ -196,7 +189,6 @@ export default {
196189
accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }],
197190
refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }],
198191
redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }],
199-
autoApprove: [{ required: true, message: "是否自动授权不能为空", trigger: "blur" }],
200192
authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }],
201193
}
202194
};
@@ -235,9 +227,9 @@ export default {
235227
accessTokenValiditySeconds: 30 * 60,
236228
refreshTokenValiditySeconds: 30 * 24 * 60,
237229
redirectUris: [],
238-
autoApprove: true,
239230
authorizedGrantTypes: [],
240231
scopes: [],
232+
autoApproveScopes: [],
241233
authorities: [],
242234
resourceIds: [],
243235
additionalInformation: undefined,

0 commit comments

Comments
 (0)