From 5083b2bdfedd9e1ac737f5148689bcdbd259c0a5 Mon Sep 17 00:00:00 2001 From: "meilin.huang" <954537473@qq.com> Date: Mon, 24 Jul 2023 22:36:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20oauth2=E7=99=BB=E5=BD=95=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mayfly_go_web/public/config.js | 39 +++++------ mayfly_go_web/src/common/openApi.ts | 1 + mayfly_go_web/src/router/index.ts | 2 +- mayfly_go_web/src/router/route.ts | 8 +++ .../views/login/component/AccountLogin.vue | 68 +++++++++++++++++-- mayfly_go_web/src/views/login/index.vue | 11 +-- .../src/views/oauth/Oauth2Callback.vue | 39 +++++++++++ mayfly_go_web/src/views/personal/api.ts | 1 + mayfly_go_web/src/views/personal/index.vue | 24 +++++-- server/internal/auth/api/oauth2_login.go | 41 ++++++----- server/internal/auth/application/oauth2.go | 6 ++ .../internal/auth/domain/repository/oauth2.go | 2 + .../auth/infrastructure/persistence/oauth2.go | 4 ++ server/internal/auth/router/router.go | 4 +- server/internal/sys/api/account.go | 9 +++ server/internal/sys/api/form/account.go | 4 +- server/internal/sys/application/account.go | 14 +++- .../internal/sys/domain/repository/account.go | 2 + .../sys/infrastructure/persistence/account.go | 8 +++ 19 files changed, 227 insertions(+), 60 deletions(-) create mode 100644 mayfly_go_web/src/views/oauth/Oauth2Callback.vue diff --git a/mayfly_go_web/public/config.js b/mayfly_go_web/public/config.js index b94f39ff..29c203f5 100644 --- a/mayfly_go_web/public/config.js +++ b/mayfly_go_web/public/config.js @@ -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; +// } +// } +// })(); diff --git a/mayfly_go_web/src/common/openApi.ts b/mayfly_go_web/src/common/openApi.ts index c48c3105..7fc2a3b3 100644 --- a/mayfly_go_web/src/common/openApi.ts +++ b/mayfly_go_web/src/common/openApi.ts @@ -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), }; diff --git a/mayfly_go_web/src/router/index.ts b/mayfly_go_web/src/router/index.ts index 9df9b04e..f5650805 100644 --- a/mayfly_go_web/src/router/index.ts +++ b/mayfly_go_web/src/router/index.ts @@ -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; diff --git a/mayfly_go_web/src/router/route.ts b/mayfly_go_web/src/router/route.ts index fd65d728..b33245fa 100644 --- a/mayfly_go_web/src/router/route.ts +++ b/mayfly_go_web/src/router/route.ts @@ -143,6 +143,14 @@ export const staticRoutes: Array = [ title: '没有权限', }, }, + { + path: '/oauth2/callback', + name: 'oauth2Callback', + component: () => import('@/views/oauth/Oauth2Callback.vue'), + meta: { + title: 'oauth2回调', + }, + }, { path: '/machine/terminal', name: 'machineTerminal', diff --git a/mayfly_go_web/src/views/login/component/AccountLogin.vue b/mayfly_go_web/src/views/login/component/AccountLogin.vue index 26807e17..5390991b 100644 --- a/mayfly_go_web/src/views/login/component/AccountLogin.vue +++ b/mayfly_go_web/src/views/login/component/AccountLogin.vue @@ -104,6 +104,24 @@ + + + + + + + + + + + + + @@ -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, +}); diff --git a/mayfly_go_web/src/views/personal/api.ts b/mayfly_go_web/src/views/personal/api.ts index c85efe82..3a4c97be 100644 --- a/mayfly_go_web/src/views/personal/api.ts +++ b/mayfly_go_web/src/views/personal/api.ts @@ -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'), }; diff --git a/mayfly_go_web/src/views/personal/index.vue b/mayfly_go_web/src/views/personal/index.vue index 856d4acd..b4940184 100644 --- a/mayfly_go_web/src/views/personal/index.vue +++ b/mayfly_go_web/src/views/personal/index.vue @@ -136,7 +136,8 @@
当前状态:{{ authStatus.bind ? '已绑定' : '未绑定' }}
- 立即绑定 + 立即绑定 + 解绑
@@ -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; diff --git a/server/internal/auth/api/oauth2_login.go b/server/internal/auth/api/oauth2_login.go index ce478420..08ff9717 100644 --- a/server/internal/auth/api/oauth2_login.go +++ b/server/internal/auth/api/oauth2_login.go @@ -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("" + - "" + - "") + 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("" + - "" + - "") + 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) +} diff --git a/server/internal/auth/application/oauth2.go b/server/internal/auth/application/oauth2.go index 1a87a092..7dee2d9e 100644 --- a/server/internal/auth/application/oauth2.go +++ b/server/internal/auth/application/oauth2.go @@ -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}) +} diff --git a/server/internal/auth/domain/repository/oauth2.go b/server/internal/auth/domain/repository/oauth2.go index 4b749688..f923a25a 100644 --- a/server/internal/auth/domain/repository/oauth2.go +++ b/server/internal/auth/domain/repository/oauth2.go @@ -7,4 +7,6 @@ type Oauth2Account interface { GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error SaveOAuthAccount(e *entity.Oauth2Account) error + + DeleteBy(e *entity.Oauth2Account) } diff --git a/server/internal/auth/infrastructure/persistence/oauth2.go b/server/internal/auth/infrastructure/persistence/oauth2.go index cad3f8d7..e3d67d52 100644 --- a/server/internal/auth/infrastructure/persistence/oauth2.go +++ b/server/internal/auth/infrastructure/persistence/oauth2.go @@ -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) +} diff --git a/server/internal/auth/router/router.go b/server/internal/auth/router/router.go index 61804660..eb7d0895 100644 --- a/server/internal/auth/router/router.go +++ b/server/internal/auth/router/router.go @@ -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[:]) diff --git a/server/internal/sys/api/account.go b/server/internal/sys/api/account.go index a0ea88f6..fda0ad43 100644 --- a/server/internal/sys/api/account.go +++ b/server/internal/sys/api/account.go @@ -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) } } diff --git a/server/internal/sys/api/form/account.go b/server/internal/sys/api/form/account.go index c540d87f..42fb0f06 100644 --- a/server/internal/sys/api/form/account.go +++ b/server/internal/sys/api/form/account.go @@ -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 { diff --git a/server/internal/sys/application/account.go b/server/internal/sys/application/account.go index fefc432b..e799e33b 100644 --- a/server/internal/sys/application/account.go +++ b/server/internal/sys/application/account.go @@ -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) } diff --git a/server/internal/sys/domain/repository/account.go b/server/internal/sys/domain/repository/account.go index eb45c83c..7b2654a7 100644 --- a/server/internal/sys/domain/repository/account.go +++ b/server/internal/sys/domain/repository/account.go @@ -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) diff --git a/server/internal/sys/infrastructure/persistence/account.go b/server/internal/sys/infrastructure/persistence/account.go index f96c995c..038a2457 100644 --- a/server/internal/sys/infrastructure/persistence/account.go +++ b/server/internal/sys/infrastructure/persistence/account.go @@ -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).