refactor: oauth2登录调整

This commit is contained in:
meilin.huang
2023-07-24 22:36:07 +08:00
parent 155ae65b4a
commit 5083b2bdfe
19 changed files with 227 additions and 60 deletions

View File

@@ -1,24 +1,25 @@
window.globalConfig = {
// 默认为空以访问根目录为api请求地址。若前后端分离部署可单独配置该后端api请求地址
"BaseApiUrl": "",
"BaseWsUrl": ""
}
BaseApiUrl: '',
BaseWsUrl: '',
};
// index.html添加百秒级时间戳防止被浏览器缓存
!function () {
let t = "t=" + new Date().getTime().toString().substring(0, 8)
let search = location.search;
let m = search && search.match(/t=\d*/g)
// !(function () {
// let t = 't=' + new Date().getTime().toString().substring(0, 8);
// let search = location.search;
// let m = search && search.match(/t=\d*/g);
if (m[0]) {
if (m[0] !== t) {
location.search = search.replace(m[0], t)
}
} else {
if (search.indexOf('?') > -1) {
location.search = search + '&' + t
} else {
location.search = t
}
}
}()
// console.log(location);
// if (m[0]) {
// if (m[0] !== t) {
// location.search = search.replace(m[0], t);
// }
// } else {
// if (search.indexOf('?') > -1) {
// location.search = search + '&' + t;
// } else {
// location.search = t;
// }
// }
// })();

View File

@@ -10,4 +10,5 @@ export default {
captcha: () => request.get('/sys/captcha'),
logout: () => request.post('/auth/accounts/logout'),
getPermissions: () => request.get('/sys/accounts/permissions'),
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
};

View File

@@ -257,7 +257,7 @@ router.beforeEach(async (to, from, next) => {
}
const token = getSession('token');
if (to.path === '/login' && !token) {
if ((to.path === '/login' || to.path == '/oauth2/callback') && !token) {
next();
NProgress.done();
return;

View File

@@ -143,6 +143,14 @@ export const staticRoutes: Array<RouteRecordRaw> = [
title: '没有权限',
},
},
{
path: '/oauth2/callback',
name: 'oauth2Callback',
component: () => import('@/views/oauth/Oauth2Callback.vue'),
meta: {
title: 'oauth2回调',
},
},
{
path: '/machine/terminal',
name: 'machineTerminal',

View File

@@ -104,6 +104,24 @@
</div>
</template>
</el-dialog>
<el-dialog title="修改基本信息" v-model="baseInfoDialog.visible" :close-on-click-modal="false" width="450px" :destroy-on-close="true">
<el-form :model="baseInfoDialog.form" :rules="baseInfoDialog.rules" ref="changePwdFormRef" label-width="auto">
<el-form-item prop="username" label="用户名" required>
<el-input v-model.trim="baseInfoDialog.form.username"></el-input>
</el-form-item>
<el-form-item prop="name" label="姓名" required>
<el-input v-model.trim="baseInfoDialog.form.name"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<!-- <el-button @click="cancelChangePwd"> </el-button> -->
<el-button @click="updateUserInfo()" type="primary" :loading="loading.updateUserConfirm"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -112,7 +130,7 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { initRouter } from '@/router/index';
import { setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage';
import { getSession, setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage';
import { formatAxis } from '@/common/utils/format';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
@@ -120,6 +138,7 @@ import { getAccountLoginSecurity, useWartermark } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo';
import QrcodeVue from 'qrcode.vue';
import { personApi } from '@/views/personal/api';
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -149,6 +168,7 @@ const state = reactive({
captcha: '',
cid: '',
},
loginRes: {} as any,
changePwdDialog: {
visible: false,
form: {
@@ -178,14 +198,26 @@ const state = reactive({
code: [{ required: true, message: '请输入OTP授权码', trigger: 'blur' }],
},
},
baseInfoDialog: {
visible: false,
form: {
username: '',
name: '',
},
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
},
},
loading: {
signIn: false,
changePwd: false,
otpConfirm: false,
updateUserConfirm: false,
},
});
const { accountLoginSecurity, showLoginFailTips, captchaImage, loginForm, changePwdDialog, otpDialog, loading } = toRefs(state);
const { accountLoginSecurity, showLoginFailTips, captchaImage, loginForm, changePwdDialog, otpDialog, baseInfoDialog, loading } = toRefs(state);
onMounted(async () => {
nextTick(async () => {
@@ -268,7 +300,17 @@ const onSignIn = async () => {
loginResDeal(loginRes);
};
const updateUserInfo = async () => {
const form = state.baseInfoDialog.form;
await personApi.updateAccount.request(state.baseInfoDialog.form);
state.baseInfoDialog.visible = false;
useUserInfo().userInfo.username = form.username;
useUserInfo().userInfo.name = form.name;
await toIndex();
};
const loginResDeal = (loginRes: any) => {
state.loginRes = loginRes;
// 用户信息
const userInfos = {
name: loginRes.name,
@@ -300,16 +342,26 @@ const loginResDeal = (loginRes: any) => {
}, 400);
};
defineExpose({
loginResDeal,
});
// 登录成功后的跳转
const signInSuccess = async (accessToken: string = '') => {
if (!accessToken) {
accessToken = getSession('token');
}
// 存储 token 到浏览器缓存
setSession('token', accessToken);
// 初始化路由
await initRouter();
// 判断是否为第一次oauth2登录是的话需要用户填写姓名和用户名
if (state.loginRes.isFirstOauth2Login) {
state.baseInfoDialog.form.username = state.loginRes.username;
state.baseInfoDialog.visible = true;
} else {
await toIndex();
}
};
const toIndex = async () => {
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
@@ -356,6 +408,10 @@ const cancelChangePwd = () => {
state.changePwdDialog.form.username = '';
getCaptcha();
};
defineExpose({
loginResDeal,
});
</script>
<style scoped lang="scss">

View File

@@ -68,19 +68,22 @@ onMounted(async () => {
});
const oauth2Login = () => {
const width = 700;
const height = 500;
var iTop = (window.screen.height - 30 - height) / 2; //获得窗口的垂直位置;
var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置;
// 小窗口打开oauth2鉴权
let oauthWindoe = window.open(config.baseApiUrl + '/auth/oauth2/login', 'oauth2', 'width=600,height=600');
if (oauthWindoe) {
let oauthWindow = window.open(config.baseApiUrl + '/auth/oauth2/login', 'oauth2', `height=${height},width=${width},top=${iTop},left=${iLeft},location=no`);
if (oauthWindow) {
const handler = (e: any) => {
if (e.data.action === 'oauthLogin') {
oauthWindoe!.close();
window.removeEventListener('message', handler);
loginForm.value!.loginResDeal(e.data);
}
};
window.addEventListener('message', handler);
setInterval(() => {
if (oauthWindoe!.closed) {
if (oauthWindow!.closed) {
window.removeEventListener('message', handler);
}
}, 1000);

View File

@@ -0,0 +1,39 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import openApi from '@/common/openApi';
const route = useRoute();
onMounted(async () => {
try {
const queryParam = route.query;
// 使用hash路由回调code可能会被设置到search
// 如 localhost:8888/?code=xxxx/oauth2/callback导致route.query获取不到值
if (location.search) {
const searchParams = location.search.split('?')[1];
if (searchParams) {
for (let searchParam of searchParams.split('&')) {
const searchParamSplit = searchParam.split('=');
queryParam[searchParamSplit[0]] = searchParamSplit[1];
}
}
}
const res: any = await openApi.oauth2Callback(queryParam);
ElMessage.success('授权认证成功');
top?.opener.postMessage(res);
window.close();
} catch (e: any) {
setTimeout(() => {
window.close();
}, 1500);
}
});
</script>
<style lang="scss"></style>

View File

@@ -5,4 +5,5 @@ export const personApi = {
updateAccount: Api.newPut('/sys/accounts/self'),
authStatus: Api.newGet('/auth/oauth2/status'),
getMsgs: Api.newGet('/msgs/self'),
unbindOauth2: Api.newGet('/auth/oauth2/unbind'),
};

View File

@@ -136,7 +136,8 @@
<div class="personal-edit-safe-item-left-value">当前状态{{ authStatus.bind ? '已绑定' : '未绑定' }}</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button link @click="bindOAuth2" :disabled="authStatus.bind" type="primary">立即绑定</el-button>
<el-button v-if="!authStatus.bind" link @click="bindOAuth2" type="primary">立即绑定</el-button>
<el-button v-else link @click="unbindOAuth2()" type="warning">解绑</el-button>
</div>
</div>
</div>
@@ -202,8 +203,8 @@ const state = reactive({
password: '',
},
authStatus: {
enable: { oauth2: false },
bind: { oauth2: false },
enable: false,
bind: false,
},
});
@@ -241,12 +242,19 @@ const updateAccount = async () => {
};
const bindOAuth2 = () => {
const width = 700;
const height = 500;
var iTop = (window.screen.height - 30 - height) / 2; //获得窗口的垂直位置;
var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置;
// 小窗口打开oauth2鉴权
let oauthWindow = window.open(config.baseApiUrl + '/auth/oauth2/bind?token=' + getSession('token'), 'oauth2', 'width=600,height=600');
let oauthWindow = window.open(
config.baseApiUrl + '/auth/oauth2/bind?token=' + getSession('token'),
'oauth2',
`height=${height},width=${width},top=${iTop},left=${iLeft},location=no`
);
if (oauthWindow) {
const handler = (e: any) => {
if (e.data.action === 'oauthBind') {
oauthWindow!.close();
window.removeEventListener('message', handler);
// 处理登录token
ElMessage.success('绑定成功');
@@ -264,6 +272,12 @@ const bindOAuth2 = () => {
}
};
const unbindOAuth2 = async () => {
await personApi.unbindOauth2.request();
ElMessage.success('解绑成功');
state.authStatus = await personApi.authStatus.request();
};
const getMsgs = async () => {
const res = await personApi.getMsgs.request(state.msgDialog.query);
state.msgDialog.msgs = res;

View File

@@ -1,7 +1,6 @@
package api
import (
"encoding/json"
"fmt"
"io"
"mayfly-go/internal/auth/api/vo"
@@ -59,7 +58,7 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
biz.NotEmpty(stateAction, "state已过期, 请重新登录")
token, err := client.Exchange(rc.GinCtx, code)
biz.ErrIsNilAppendErr(err, "获取token失败: %s")
biz.ErrIsNilAppendErr(err, "获取OAuth2 accessToken失败: %s")
// 获取用户信息
httpCli := client.Client(rc.GinCtx.Request.Context(), token)
@@ -104,7 +103,12 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
AccountId: accountId,
}, "account_id", "identity")
biz.IsTrue(err != nil, "该账号已被绑定")
biz.IsTrue(err != nil, "该账号已被其他用户绑定")
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
Identity: userId,
}, "account_id", "identity")
biz.IsTrue(err != nil, "您已绑定其他账号")
now := time.Now()
err = a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{
@@ -118,13 +122,7 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
"action": "oauthBind",
"bind": true,
}
b, err = json.Marshal(res)
biz.ErrIsNil(err, "数据序列化失败")
rc.GinCtx.Header("Content-Type", "text/html; charset=utf-8")
rc.GinCtx.Writer.WriteHeader(http.StatusOK)
_, _ = rc.GinCtx.Writer.WriteString("<html>" +
"<script>top.opener.postMessage(" + string(b) + ")</script>" +
"</html>")
rc.ResData = res
} else {
panic(biz.NewBizErr("state不合法"))
}
@@ -132,14 +130,12 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
// 指定登录操作
func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *sysentity.ConfigOauth2Login) {
clientIp := getIpAndRegion(rc)
rc.ReqParam = fmt.Sprintf("oauth2 login username: %s | ip: %s", userId, clientIp)
// 查询用户是否存在
oauthAccount := &entity.Oauth2Account{Identity: userId}
err := a.Oauth2App.GetOAuthAccount(oauthAccount, "account_id", "identity")
var accountId uint64
isFirst := false
// 不存在,进行注册
if err != nil {
biz.IsTrue(oauth.AutoRegister, "系统未开启自动注册, 请先让管理员添加对应账号")
@@ -164,6 +160,7 @@ func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *sysentity
})
biz.ErrIsNilAppendErr(err, "绑定用户失败: %s")
accountId = account.Id
isFirst = true
} else {
accountId = oauthAccount.AccountId
}
@@ -175,15 +172,13 @@ func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *sysentity
err = a.AccountApp.GetAccount(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
biz.ErrIsNilAppendErr(err, "获取用户信息失败: %s")
clientIp := getIpAndRegion(rc)
rc.ReqParam = fmt.Sprintf("oauth2 login username: %s | ip: %s", account.Username, clientIp)
res := LastLoginCheck(account, a.ConfigApp.GetConfig(sysentity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity(), clientIp)
res["action"] = "oauthLogin"
b, err := json.Marshal(res)
biz.ErrIsNil(err, "数据序列化失败")
rc.GinCtx.Header("Content-Type", "text/html; charset=utf-8")
rc.GinCtx.Writer.WriteHeader(http.StatusOK)
_, _ = rc.GinCtx.Writer.WriteString("<html>" +
"<script>top.opener.postMessage(" + string(b) + ")</script>" +
"</html>")
res["isFirstOauth2Login"] = isFirst
rc.ResData = res
}
func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *sysentity.ConfigOauth2Login) {
@@ -198,7 +193,7 @@ func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *sysentity.ConfigOauth2L
AuthURL: oath2LoginConfig.AuthorizationURL,
TokenURL: oath2LoginConfig.AccessTokenURL,
},
RedirectURL: oath2LoginConfig.RedirectURL + "/api/auth/oauth2/callback",
RedirectURL: oath2LoginConfig.RedirectURL + "/#/oauth2/callback",
Scopes: strings.Split(oath2LoginConfig.Scopes, ","),
}
return client, oath2LoginConfig
@@ -217,3 +212,7 @@ func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
ctx.ResData = res
}
func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
a.Oauth2App.Unbind(rc.LoginAccount.Id)
}

View File

@@ -12,6 +12,8 @@ type Oauth2 interface {
GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error
BindOAuthAccount(e *entity.Oauth2Account) error
Unbind(accountId uint64)
}
func newAuthApp(oauthAccountRepo repository.Oauth2Account) Oauth2 {
@@ -36,3 +38,7 @@ func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ..
func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error {
return a.oauthAccountRepo.SaveOAuthAccount(e)
}
func (a *oauth2AppImpl) Unbind(accountId uint64) {
a.oauthAccountRepo.DeleteBy(&entity.Oauth2Account{AccountId: accountId})
}

View File

@@ -7,4 +7,6 @@ type Oauth2Account interface {
GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error
SaveOAuthAccount(e *entity.Oauth2Account) error
DeleteBy(e *entity.Oauth2Account)
}

View File

@@ -22,3 +22,7 @@ func (a *oauth2AccountRepoImpl) SaveOAuthAccount(e *entity.Oauth2Account) error
}
return gormx.UpdateById(e)
}
func (a *oauth2AccountRepoImpl) DeleteBy(e *entity.Oauth2Account) {
gormx.DeleteByCondition(e)
}

View File

@@ -45,9 +45,11 @@ func Init(router *gin.RouterGroup) {
req.NewGet("/oauth2/bind", oauth2Login.OAuth2Bind),
// oauth2回调地址
req.NewGet("/oauth2/callback", oauth2Login.OAuth2Callback).Log(req.NewLogSave("oauth2回调")).NoRes().DontNeedToken(),
req.NewGet("/oauth2/callback", oauth2Login.OAuth2Callback).Log(req.NewLogSave("oauth2回调")).DontNeedToken(),
req.NewGet("/oauth2/status", oauth2Login.Oauth2Status),
req.NewGet("/oauth2/unbind", oauth2Login.Oauth2Unbind).Log(req.NewLogSave("oauth2解绑")),
}
req.BatchSetGroup(rg, reqs[:])

View File

@@ -105,6 +105,13 @@ func (a *Account) UpdateAccount(rc *req.Ctx) {
biz.IsTrue(utils.CheckAccountPasswordLever(updateAccount.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号")
updateAccount.Password = cryptox.PwdHash(updateAccount.Password)
}
oldAcc := a.AccountApp.GetById(updateAccount.Id)
// 账号创建十分钟内允许修改用户名兼容oauth2首次登录修改用户名否则不允许修改
if oldAcc.CreateTime.Add(10 * time.Minute).Before(time.Now()) {
// 禁止更新用户名,防止误传被更新
updateAccount.Username = ""
}
a.AccountApp.Update(updateAccount)
}
@@ -133,6 +140,8 @@ func (a *Account) SaveAccount(rc *req.Ctx) {
biz.IsTrue(utils.CheckAccountPasswordLever(account.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号")
account.Password = cryptox.PwdHash(account.Password)
}
// 更新操作不允许修改用户名、防止误传更新
account.Username = ""
a.AccountApp.Update(account)
}
}

View File

@@ -8,7 +8,9 @@ type AccountCreateForm struct {
}
type AccountUpdateForm struct {
Password *string `json:"password" binding:"min=6,max=16"`
Name string `json:"name" binding:"max=16"` // 姓名
Username string `json:"username" binding:"max=20"`
Password *string `json:"password"`
}
type AccountChangePasswordForm struct {

View File

@@ -14,6 +14,8 @@ import (
type Account interface {
GetAccount(condition *entity.Account, cols ...string) error
GetById(id uint64) *entity.Account
GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any]
Create(account *entity.Account)
@@ -38,6 +40,10 @@ func (a *accountAppImpl) GetAccount(condition *entity.Account, cols ...string) e
return a.accountRepo.GetAccount(condition, cols...)
}
func (a *accountAppImpl) GetById(id uint64) *entity.Account {
return a.accountRepo.GetById(id)
}
func (a *accountAppImpl) GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any] {
return a.accountRepo.GetPageList(condition, pageParam, toEntity)
}
@@ -51,8 +57,12 @@ func (a *accountAppImpl) Create(account *entity.Account) {
}
func (a *accountAppImpl) Update(account *entity.Account) {
// 禁止更新用户名,防止误传被更新
account.Username = ""
if account.Username != "" {
unAcc := &entity.Account{Username: account.Username}
err := a.GetAccount(unAcc)
biz.IsTrue(err != nil || unAcc.Id == account.Id, "该用户名已存在")
}
a.accountRepo.Update(account)
}

View File

@@ -9,6 +9,8 @@ type Account interface {
// 根据条件获取账号信息
GetAccount(condition *entity.Account, cols ...string) error
GetById(id uint64) *entity.Account
GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any]
Insert(account *entity.Account)

View File

@@ -18,6 +18,14 @@ func (a *accountRepoImpl) GetAccount(condition *entity.Account, cols ...string)
return gormx.GetBy(condition, cols...)
}
func (a *accountRepoImpl) GetById(id uint64) *entity.Account {
ac := new(entity.Account)
if err := gormx.GetById(ac, id); err != nil {
return nil
}
return ac
}
func (m *accountRepoImpl) GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity any, orderBy ...string) *model.PageResult[any] {
qd := gormx.NewQuery(new(entity.Account)).
Like("name", condition.Name).