diff --git a/README.md b/README.md index d04ca346..35c918af 100644 --- a/README.md +++ b/README.md @@ -20,67 +20,84 @@

- ### 介绍 -web版 **linux(终端[终端回放] 文件 脚本 进程)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo统一管理操作平台** +web 版 **linux(终端[终端回放] 文件 脚本 进程)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo 统一管理操作平台** ### 开发语言与主要框架 + - 前端:typescript、vue3、element-plus - 后端:golang、gin、gorm - ### 交流及问题反馈加 QQ 群 + 119699946 - ### 系统相关资料 + - 项目文档: https://www.yuque.com/may-fly/mayfly-go - 系统操作视频: https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854 +### 演示环境 + +http://mayflygo.1yue.net +账号/密码:test/test123. ### 系统核心功能截图 ##### 记录操作记录 + ![记录操作记录](https://objs.gitee.io/mayfly-go-docs/home/log.jpg "屏幕截图.png") #### 机器操作 + ##### 状态查看 + ![状态查看](https://objs.gitee.io/mayfly-go-docs/home/machine-status.jpg "屏幕截图.png") -##### ssh终端 +##### ssh 终端 + ![ssh终端](https://objs.gitee.io/mayfly-go-docs/home/machine-ssh.jpg "屏幕截图.png") ##### 文件操作 + ![文件操作](https://objs.gitee.io/mayfly-go-docs/home/file-dir.jpg "屏幕截图.png") ![文件操作](https://objs.gitee.io/mayfly-go-docs/home/file-content-update.jpg "屏幕截图.png") - #### 数据库操作 -##### sql编辑器 + +##### sql 编辑器 + ![sql编辑器](https://objs.gitee.io/mayfly-go-docs/home/dbms-sql-editor.jpg "屏幕截图.png") ##### 在线增删改查数据 + ![选表查数据](https://objs.gitee.io/mayfly-go-docs/home/dbms-show-table-data.jpg "屏幕截图.png") +#### Redis 操作 -#### Redis操作 ![数据](https://objs.gitee.io/mayfly-go-docs/home/redis-data-list.jpg "屏幕截图.png") +#### Mongo 操作 -#### Mongo操作 ![数据](https://objs.gitee.io/mayfly-go-docs/home/mongo-op.jpg "屏幕截图.png") - ##### 系统管理 + ##### 账号管理 + ![账号管理](https://images.gitee.com/uploads/images/2021/0607/173919_a8d7dc18_1240250.png "屏幕截图.png") ##### 角色管理 + ![角色管理](https://images.gitee.com/uploads/images/2021/0607/174028_3654fb28_1240250.png "屏幕截图.png") ##### 资源管理 + ![资源管理](https://images.gitee.com/uploads/images/2021/0607/174436_e9e1535c_1240250.png "屏幕截图.png") +**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go -**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go \ No newline at end of file +#### 💌 支持作者 + +如果觉得项目不错,或者已经在使用了,希望你可以去 Github 或者 Gitee 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。 diff --git a/mayfly_go_web/src/common/openApi.ts b/mayfly_go_web/src/common/openApi.ts index 062d8ada..c48c3105 100644 --- a/mayfly_go_web/src/common/openApi.ts +++ b/mayfly_go_web/src/common/openApi.ts @@ -1,13 +1,13 @@ import request from './request'; export default { - login: (param: any) => request.post('/sys/accounts/login', param), - otpVerify: (param: any) => request.post('/sys/accounts/otp-verify', param), - changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param), + login: (param: any) => request.post('/auth/accounts/login', param), + otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param), getPublicKey: () => request.get('/common/public-key'), getConfigValue: (params: any) => request.get('/sys/configs/value', params), - oauthConfig: () => request.get('/sys/configs/auth'), + oauth2LoginConfig: () => request.get('/sys/configs/oauth2-login'), + changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param), captcha: () => request.get('/sys/captcha'), - logout: () => request.post('/sys/accounts/logout/{token}'), + logout: () => request.post('/auth/accounts/logout'), getPermissions: () => request.get('/sys/accounts/permissions'), }; diff --git a/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue b/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue index 973fc9c5..20498980 100644 --- a/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue +++ b/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue @@ -25,8 +25,7 @@
- + - - + + - + @@ -240,7 +266,7 @@ const onSignIn = async () => { } state.showLoginFailTips = false; loginResDeal(loginRes); -} +}; const loginResDeal = (loginRes: any) => { // 用户信息 @@ -248,9 +274,8 @@ const loginResDeal = (loginRes: any) => { name: loginRes.name, username: loginRes.username, // 头像 - photo: letterAvatar(state.loginForm.username), + photo: letterAvatar(loginRes.username), time: new Date().getTime(), - // permissions: loginRes.permissions, lastLoginTime: loginRes.lastLoginTime, lastLoginIp: loginRes.lastLoginIp, }; @@ -276,7 +301,7 @@ const loginResDeal = (loginRes: any) => { }; defineExpose({ - loginResDeal + loginResDeal, }); // 登录成功后的跳转 diff --git a/mayfly_go_web/src/views/login/index.vue b/mayfly_go_web/src/views/login/index.vue index 36e83c0c..3d917e8c 100644 --- a/mayfly_go_web/src/views/login/index.vue +++ b/mayfly_go_web/src/views/login/index.vue @@ -18,15 +18,12 @@ --> -
- 第三方登录 - -
-
- - +
+ 第三方登录: + + - + @@ -52,14 +49,14 @@ const { themeConfig } = storeToRefs(useThemeConfig()); const state = reactive({ tabsActiveName: 'account', isTabPaneShow: true, - authConfig: { - oauth2: false, + oauth2LoginConfig: { + enable: false, }, }); const loginForm = ref<{ loginResDeal: (data: any) => void } | null>(null); -const { isTabPaneShow, tabsActiveName, authConfig, } = toRefs(state); +const { isTabPaneShow, tabsActiveName, oauth2LoginConfig: authConfig } = toRefs(state); // 切换密码、手机登录 const onTabsClick = () => { @@ -67,32 +64,28 @@ const onTabsClick = () => { }; onMounted(async () => { - state.authConfig = await openApi.oauthConfig(); - + state.oauth2LoginConfig = await openApi.oauth2LoginConfig(); }); const oauth2Login = () => { // 小窗口打开oauth2鉴权 - let oauthWindoe = window.open(config.baseApiUrl + "/sys/auth/oauth2/login", "oauth2", "width=600,height=600"); + let oauthWindoe = window.open(config.baseApiUrl + '/auth/oauth2/login', 'oauth2', 'width=600,height=600'); if (oauthWindoe) { const handler = (e: any) => { - if (e.data.action === "oauthLogin") { + if (e.data.action === 'oauthLogin') { oauthWindoe!.close(); - window.removeEventListener("message", handler); - // 处理登录token - console.log(e.data); + window.removeEventListener('message', handler); loginForm.value!.loginResDeal(e.data); } - } - window.addEventListener("message", handler); + }; + window.addEventListener('message', handler); setInterval(() => { if (oauthWindoe!.closed) { - window.removeEventListener("message", handler); + window.removeEventListener('message', handler); } }, 1000); } -} - +}; diff --git a/mayfly_go_web/src/views/system/config/ConfigList.vue b/mayfly_go_web/src/views/system/config/ConfigList.vue index 4626dff2..d6a92f60 100755 --- a/mayfly_go_web/src/views/system/config/ConfigList.vue +++ b/mayfly_go_web/src/views/system/config/ConfigList.vue @@ -25,7 +25,7 @@ - + - + autocomplete="off" + :label="item.placeholder" + clearable + /> " + + "" + + "") + } else { + panic(biz.NewBizErr("state不合法")) + } +} + +// 指定登录操作 +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 + // 不存在,进行注册 + if err != nil { + biz.IsTrue(oauth.AutoRegister, "系统未开启自动注册, 请先让管理员添加对应账号") + now := time.Now() + account := &sysentity.Account{ + Model: model.Model{ + CreateTime: &now, + CreatorId: 0, + Creator: "oauth2", + UpdateTime: &now, + }, + Name: userId, + Username: userId, + } + a.AccountApp.Create(account) + // 绑定 + err := a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{ + AccountId: account.Id, + Identity: oauthAccount.Identity, + CreateTime: &now, + UpdateTime: &now, + }) + biz.ErrIsNilAppendErr(err, "绑定用户失败: %s") + accountId = account.Id + } else { + accountId = oauthAccount.AccountId + } + + // 进行登录 + account := &sysentity.Account{ + Model: model.Model{DeletedModel: model.DeletedModel{Id: accountId}}, + } + err = a.AccountApp.GetAccount(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret") + biz.ErrIsNilAppendErr(err, "获取用户信息失败: %s") + + 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("" + + "" + + "") +} + +func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *sysentity.ConfigOauth2Login) { + oath2LoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyOauth2Login).ToOauth2Login() + biz.IsTrue(oath2LoginConfig.Enable, "请先配置oauth2或启用oauth2登录") + biz.IsTrue(oath2LoginConfig.ClientId != "", "oauth2 clientId不能为空") + + client := &oauth2.Config{ + ClientID: oath2LoginConfig.ClientId, + ClientSecret: oath2LoginConfig.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: oath2LoginConfig.AuthorizationURL, + TokenURL: oath2LoginConfig.AccessTokenURL, + }, + RedirectURL: oath2LoginConfig.RedirectURL + "/api/auth/oauth2/callback", + Scopes: strings.Split(oath2LoginConfig.Scopes, ","), + } + return client, oath2LoginConfig +} + +func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) { + res := &vo.Oauth2Status{} + oauth2LoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyOauth2Login).ToOauth2Login() + res.Enable = oauth2LoginConfig.Enable + if res.Enable { + err := a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{ + AccountId: ctx.LoginAccount.Id, + }, "account_id", "identity") + res.Bind = err == nil + } + + ctx.ResData = res +} diff --git a/server/internal/auth/api/vo/vo.go b/server/internal/auth/api/vo/vo.go new file mode 100644 index 00000000..0a94e97a --- /dev/null +++ b/server/internal/auth/api/vo/vo.go @@ -0,0 +1,6 @@ +package vo + +type Oauth2Status struct { + Enable bool `json:"enable"` + Bind bool `json:"bind"` +} diff --git a/server/internal/auth/application/application.go b/server/internal/auth/application/application.go new file mode 100644 index 00000000..ae6c935b --- /dev/null +++ b/server/internal/auth/application/application.go @@ -0,0 +1,11 @@ +package application + +import "mayfly-go/internal/auth/infrastructure/persistence" + +var ( + authApp = newAuthApp(persistence.GetOauthAccountRepo()) +) + +func GetAuthApp() Oauth2 { + return authApp +} diff --git a/server/internal/auth/application/oauth2.go b/server/internal/auth/application/oauth2.go new file mode 100644 index 00000000..1a87a092 --- /dev/null +++ b/server/internal/auth/application/oauth2.go @@ -0,0 +1,38 @@ +package application + +import ( + "mayfly-go/internal/auth/domain/entity" + "mayfly-go/internal/auth/domain/repository" + "mayfly-go/pkg/biz" + + "gorm.io/gorm" +) + +type Oauth2 interface { + GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error + + BindOAuthAccount(e *entity.Oauth2Account) error +} + +func newAuthApp(oauthAccountRepo repository.Oauth2Account) Oauth2 { + return &oauth2AppImpl{ + oauthAccountRepo: oauthAccountRepo, + } +} + +type oauth2AppImpl struct { + oauthAccountRepo repository.Oauth2Account +} + +func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error { + err := a.oauthAccountRepo.GetOAuthAccount(condition, cols...) + if err != nil { + // 排除其他报错,如表不存在等 + biz.IsTrue(err == gorm.ErrRecordNotFound, "查询失败: %s", err.Error()) + } + return err +} + +func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error { + return a.oauthAccountRepo.SaveOAuthAccount(e) +} diff --git a/server/internal/sys/domain/entity/auth.go b/server/internal/auth/domain/entity/oauth2.go similarity index 77% rename from server/internal/sys/domain/entity/auth.go rename to server/internal/auth/domain/entity/oauth2.go index c558ab06..9daabf88 100644 --- a/server/internal/sys/domain/entity/auth.go +++ b/server/internal/auth/domain/entity/oauth2.go @@ -5,7 +5,7 @@ import ( "time" ) -type OAuthAccount struct { +type Oauth2Account struct { model.DeletedModel AccountId uint64 `json:"accountId" gorm:"column:account_id;index:account_id,unique"` @@ -15,6 +15,6 @@ type OAuthAccount struct { UpdateTime *time.Time `json:"updateTime"` } -func (OAuthAccount) TableName() string { - return "t_oauth_account" +func (Oauth2Account) TableName() string { + return "t_oauth2_account" } diff --git a/server/internal/auth/domain/repository/oauth2.go b/server/internal/auth/domain/repository/oauth2.go new file mode 100644 index 00000000..4b749688 --- /dev/null +++ b/server/internal/auth/domain/repository/oauth2.go @@ -0,0 +1,10 @@ +package repository + +import "mayfly-go/internal/auth/domain/entity" + +type Oauth2Account interface { + // GetOAuthAccount 根据identity获取第三方账号信息 + GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error + + SaveOAuthAccount(e *entity.Oauth2Account) error +} diff --git a/server/internal/auth/infrastructure/persistence/oauth2.go b/server/internal/auth/infrastructure/persistence/oauth2.go new file mode 100644 index 00000000..cad3f8d7 --- /dev/null +++ b/server/internal/auth/infrastructure/persistence/oauth2.go @@ -0,0 +1,24 @@ +package persistence + +import ( + "mayfly-go/internal/auth/domain/entity" + "mayfly-go/internal/auth/domain/repository" + "mayfly-go/pkg/gormx" +) + +type oauth2AccountRepoImpl struct{} + +func newAuthAccountRepo() repository.Oauth2Account { + return new(oauth2AccountRepoImpl) +} + +func (a *oauth2AccountRepoImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error { + return gormx.GetBy(condition, cols...) +} + +func (a *oauth2AccountRepoImpl) SaveOAuthAccount(e *entity.Oauth2Account) error { + if e.Id == 0 { + return gormx.Insert(e) + } + return gormx.UpdateById(e) +} diff --git a/server/internal/auth/infrastructure/persistence/persistence.go b/server/internal/auth/infrastructure/persistence/persistence.go new file mode 100644 index 00000000..b3d00e80 --- /dev/null +++ b/server/internal/auth/infrastructure/persistence/persistence.go @@ -0,0 +1,11 @@ +package persistence + +import "mayfly-go/internal/auth/domain/repository" + +var ( + authAccountRepo = newAuthAccountRepo() +) + +func GetOauthAccountRepo() repository.Oauth2Account { + return authAccountRepo +} diff --git a/server/internal/auth/router/router.go b/server/internal/auth/router/router.go new file mode 100644 index 00000000..61804660 --- /dev/null +++ b/server/internal/auth/router/router.go @@ -0,0 +1,54 @@ +package router + +import ( + "mayfly-go/internal/auth/api" + "mayfly-go/internal/auth/application" + msgapp "mayfly-go/internal/msg/application" + sysapp "mayfly-go/internal/sys/application" + "mayfly-go/pkg/req" + + "github.com/gin-gonic/gin" +) + +func Init(router *gin.RouterGroup) { + accountLogin := &api.AccountLogin{ + ConfigApp: sysapp.GetConfigApp(), + AccountApp: sysapp.GetAccountApp(), + MsgApp: msgapp.GetMsgApp(), + } + + oauth2Login := &api.Oauth2Login{ + Oauth2App: application.GetAuthApp(), + ConfigApp: sysapp.GetConfigApp(), + AccountApp: sysapp.GetAccountApp(), + MsgApp: msgapp.GetMsgApp(), + } + + rg := router.Group("/auth") + + reqs := [...]*req.Conf{ + + // 用户账号密码登录 + req.NewPost("/accounts/login", accountLogin.Login).Log(req.NewLogSave("用户登录")).DontNeedToken(), + + // 用户退出登录 + req.NewPost("/accounts/logout", accountLogin.Logout), + + // 用户otp双因素校验 + req.NewPost("/accounts/otp-verify", accountLogin.OtpVerify).DontNeedToken(), + + /*--------oauth2登录相关----------*/ + + // oauth2登录 + req.NewGet("/oauth2/login", oauth2Login.OAuth2Login).DontNeedToken(), + + req.NewGet("/oauth2/bind", oauth2Login.OAuth2Bind), + + // oauth2回调地址 + req.NewGet("/oauth2/callback", oauth2Login.OAuth2Callback).Log(req.NewLogSave("oauth2回调")).NoRes().DontNeedToken(), + + req.NewGet("/oauth2/status", oauth2Login.Oauth2Status), + } + + req.BatchSetGroup(rg, reqs[:]) +} diff --git a/server/internal/common/utils/pwd.go b/server/internal/common/utils/pwd.go index c5b01652..57d49286 100644 --- a/server/internal/common/utils/pwd.go +++ b/server/internal/common/utils/pwd.go @@ -3,8 +3,29 @@ package utils import ( "mayfly-go/pkg/biz" "mayfly-go/pkg/config" + "regexp" ) +// 检查用户密码安全等级 +func CheckAccountPasswordLever(ps string) bool { + if len(ps) < 8 { + return false + } + num := `[0-9]{1}` + a_z := `[a-zA-Z]{1}` + symbol := `[!@#~$%^&*()+|_.,]{1}` + if b, err := regexp.MatchString(num, ps); !b || err != nil { + return false + } + if b, err := regexp.MatchString(a_z, ps); !b || err != nil { + return false + } + if b, err := regexp.MatchString(symbol, ps); !b || err != nil { + return false + } + return true +} + // 使用config.yml的aes.key进行密码加密 func PwdAesEncrypt(password string) string { if password == "" { diff --git a/server/internal/sys/api/account.go b/server/internal/sys/api/account.go index 9cc43709..a0ea88f6 100644 --- a/server/internal/sys/api/account.go +++ b/server/internal/sys/api/account.go @@ -1,28 +1,19 @@ package api import ( - "encoding/json" "fmt" + "mayfly-go/internal/common/utils" msgapp "mayfly-go/internal/msg/application" - msgentity "mayfly-go/internal/msg/domain/entity" "mayfly-go/internal/sys/api/form" "mayfly-go/internal/sys/api/vo" "mayfly-go/internal/sys/application" "mayfly-go/internal/sys/domain/entity" "mayfly-go/pkg/biz" - "mayfly-go/pkg/cache" - "mayfly-go/pkg/captcha" "mayfly-go/pkg/ginx" "mayfly-go/pkg/model" - "mayfly-go/pkg/otp" "mayfly-go/pkg/req" "mayfly-go/pkg/utils/collx" "mayfly-go/pkg/utils/cryptox" - "mayfly-go/pkg/utils/jsonx" - "mayfly-go/pkg/utils/netx" - "mayfly-go/pkg/utils/stringx" - "mayfly-go/pkg/utils/timex" - "regexp" "strconv" "strings" "time" @@ -42,163 +33,6 @@ type Account struct { ConfigApp application.Config } -/** 登录者个人相关操作 **/ - -// @router /accounts/login [post] -func (a *Account) Login(rc *req.Ctx) { - loginForm := ginx.BindJsonAndValid(rc.GinCtx, new(form.LoginForm)) - - accountLoginSecurity := a.ConfigApp.GetConfig(entity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity() - // 判断是否有开启登录验证码校验 - if accountLoginSecurity.UseCaptcha { - // 校验验证码 - biz.IsTrue(captcha.Verify(loginForm.Cid, loginForm.Captcha), "验证码错误") - } - - username := loginForm.Username - - clientIp := getIpAndRegion(rc) - rc.ReqParam = fmt.Sprintf("username: %s | ip: %s", username, clientIp) - - originPwd, err := cryptox.DefaultRsaDecrypt(loginForm.Password, true) - biz.ErrIsNilAppendErr(err, "解密密码错误: %s") - - account := &entity.Account{Username: username} - err = a.AccountApp.GetAccount(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret") - - failCountKey := fmt.Sprintf("account:login:failcount:%s", username) - nowFailCount := cache.GetInt(failCountKey) - loginFailCount := accountLoginSecurity.LoginFailCount - loginFailMin := accountLoginSecurity.LoginFailMin - biz.IsTrue(nowFailCount < loginFailCount, "登录失败超过%d次, 请%d分钟后再试", loginFailCount, loginFailMin) - - if err != nil || !cryptox.CheckPwdHash(originPwd, account.Password) { - nowFailCount++ - cache.SetStr(failCountKey, strconv.Itoa(nowFailCount), time.Minute*time.Duration(loginFailMin)) - panic(biz.NewBizErr(fmt.Sprintf("用户名或密码错误【当前登录失败%d次】", nowFailCount))) - } - biz.IsTrue(account.IsEnable(), "该账号不可用") - - // 校验密码强度(新用户第一次登录密码与账号名一致) - biz.IsTrueBy(CheckPasswordLever(originPwd), biz.NewBizErrCode(401, "您的密码安全等级较低,请修改后重新登录")) - - res := map[string]any{ - "name": account.Name, - "username": username, - "lastLoginTime": account.LastLoginTime, - "lastLoginIp": account.LastLoginIp, - } - - // 默认为不校验otp - otpStatus := OtpStatusNone - // 访问系统使用的token - accessToken := req.CreateToken(account.Id, username) - // 若系统配置中设置开启otp双因素校验,则进行otp校验 - if accountLoginSecurity.UseOtp { - otpInfo, otpurl, otpToken := useOtp(account, accountLoginSecurity.OtpIssuer, accessToken) - otpStatus = otpInfo.OptStatus - if otpurl != "" { - res["otpUrl"] = otpurl - } - accessToken = otpToken - } else { - // 不进行otp二次校验则直接返回accessToken - // 保存登录消息 - go saveLogin(a.AccountApp, a.MsgApp, account, clientIp) - } - - // 赋值otp状态 - res["otp"] = otpStatus - res["token"] = accessToken - rc.ResData = res -} - -func useOtp(account *entity.Account, otpIssuer, accessToken string) (*OtpVerifyInfo, string, string) { - account.OtpSecretDecrypt() - otpSecret := account.OtpSecret - // 修改状态为已注册 - otpStatus := OtpStatusReg - otpUrl := "" - // 该token用于otp双因素校验 - token := stringx.Rand(32) - // 未注册otp secret或重置了秘钥 - if otpSecret == "" || otpSecret == "-" { - otpStatus = OtpStatusNoReg - key, err := otp.NewTOTP(otp.GenerateOpts{ - AccountName: account.Username, - Issuer: otpIssuer, - }) - biz.ErrIsNilAppendErr(err, "otp生成失败: %s") - otpUrl = key.URL() - otpSecret = key.Secret() - } - // 缓存otpInfo, 只有双因素校验通过才可返回真正的accessToken - otpInfo := &OtpVerifyInfo{ - AccountId: account.Id, - Username: account.Username, - OptStatus: otpStatus, - OtpSecret: otpSecret, - AccessToken: accessToken, - } - cache.SetStr(fmt.Sprintf("otp:token:%s", token), jsonx.ToStr(otpInfo), time.Minute*time.Duration(3)) - return otpInfo, otpUrl, token -} - -// 获取ip与归属地信息 -func getIpAndRegion(rc *req.Ctx) string { - clientIp := rc.GinCtx.ClientIP() - return fmt.Sprintf("%s %s", clientIp, netx.Ip2Region(clientIp)) -} - -type OtpVerifyInfo struct { - AccountId uint64 - Username string - OptStatus int - AccessToken string - OtpSecret string -} - -// OTP双因素校验 -func (a *Account) OtpVerify(rc *req.Ctx) { - otpVerify := new(form.OtpVerfiy) - ginx.BindJsonAndValid(rc.GinCtx, otpVerify) - - tokenKey := fmt.Sprintf("otp:token:%s", otpVerify.OtpToken) - otpInfoJson := cache.GetStr(tokenKey) - biz.NotEmpty(otpInfoJson, "otpToken错误或失效, 请重新登陆获取") - otpInfo := new(OtpVerifyInfo) - json.Unmarshal([]byte(otpInfoJson), otpInfo) - - failCountKey := fmt.Sprintf("account:otp:failcount:%d", otpInfo.AccountId) - failCount := cache.GetInt(failCountKey) - biz.IsTrue(failCount < 5, "双因素校验失败超过5次, 请10分钟后再试") - - otpStatus := otpInfo.OptStatus - accessToken := otpInfo.AccessToken - accountId := otpInfo.AccountId - otpSecret := otpInfo.OtpSecret - - if !otp.Validate(otpVerify.Code, otpSecret) { - cache.SetStr(failCountKey, strconv.Itoa(failCount+1), time.Minute*time.Duration(10)) - panic(biz.NewBizErr("双因素认证授权码不正确")) - } - - // 如果是未注册状态,则更新account表的otpSecret信息 - if otpStatus == OtpStatusNoReg { - update := &entity.Account{OtpSecret: otpSecret} - update.Id = accountId - update.OtpSecretEncrypt() - a.AccountApp.Update(update) - } - - la := &entity.Account{Username: otpInfo.Username} - la.Id = accountId - go saveLogin(a.AccountApp, a.MsgApp, la, getIpAndRegion(rc)) - - cache.Del(tokenKey) - rc.ResData = accessToken -} - // 获取当前登录用户的菜单与权限码 func (a *Account) GetPermissions(rc *req.Ctx) { account := rc.LoginAccount @@ -239,7 +73,7 @@ func (a *Account) ChangePassword(rc *req.Ctx) { originNewPwd, err := cryptox.DefaultRsaDecrypt(form.NewPassword, true) biz.ErrIsNilAppendErr(err, "解密新密码错误: %s") - biz.IsTrue(CheckPasswordLever(originNewPwd), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号") + biz.IsTrue(utils.CheckAccountPasswordLever(originNewPwd), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号") updateAccount := new(entity.Account) updateAccount.Id = account.Id @@ -250,46 +84,6 @@ func (a *Account) ChangePassword(rc *req.Ctx) { rc.LoginAccount = &model.LoginAccount{Id: account.Id, Username: account.Username} } -func CheckPasswordLever(ps string) bool { - if len(ps) < 8 { - return false - } - num := `[0-9]{1}` - a_z := `[a-zA-Z]{1}` - symbol := `[!@#~$%^&*()+|_.,]{1}` - if b, err := regexp.MatchString(num, ps); !b || err != nil { - return false - } - if b, err := regexp.MatchString(a_z, ps); !b || err != nil { - return false - } - if b, err := regexp.MatchString(symbol, ps); !b || err != nil { - return false - } - return true -} - -// 保存更新账号登录信息 -func saveLogin(accountApp application.Account, msgApp msgapp.Msg, account *entity.Account, ip string) { - // 更新账号最后登录时间 - now := time.Now() - updateAccount := &entity.Account{LastLoginTime: &now} - updateAccount.Id = account.Id - updateAccount.LastLoginIp = ip - accountApp.Update(updateAccount) - - // 创建登录消息 - loginMsg := &msgentity.Msg{ - RecipientId: int64(account.Id), - Msg: fmt.Sprintf("于[%s]-[%s]登录", ip, timex.DefaultFormat(now)), - Type: 1, - } - loginMsg.CreateTime = &now - loginMsg.Creator = account.Username - loginMsg.CreatorId = account.Id - msgApp.Create(loginMsg) -} - // 获取个人账号信息 func (a *Account) AccountInfo(rc *req.Ctx) { ap := new(vo.AccountPersonVO) @@ -308,7 +102,7 @@ func (a *Account) UpdateAccount(rc *req.Ctx) { updateAccount.Id = rc.LoginAccount.Id if updateAccount.Password != "" { - biz.IsTrue(CheckPasswordLever(updateAccount.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号") + biz.IsTrue(utils.CheckAccountPasswordLever(updateAccount.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号") updateAccount.Password = cryptox.PwdHash(updateAccount.Password) } a.AccountApp.Update(updateAccount) @@ -336,7 +130,7 @@ func (a *Account) SaveAccount(rc *req.Ctx) { a.AccountApp.Create(account) } else { if account.Password != "" { - biz.IsTrue(CheckPasswordLever(account.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号") + biz.IsTrue(utils.CheckAccountPasswordLever(account.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号") account.Password = cryptox.PwdHash(account.Password) } a.AccountApp.Update(account) diff --git a/server/internal/sys/api/auth.go b/server/internal/sys/api/auth.go deleted file mode 100644 index 9440becb..00000000 --- a/server/internal/sys/api/auth.go +++ /dev/null @@ -1,339 +0,0 @@ -package api - -import ( - "encoding/json" - "errors" - "fmt" - "golang.org/x/oauth2" - "gorm.io/gorm" - "io" - msgapp "mayfly-go/internal/msg/application" - form2 "mayfly-go/internal/sys/api/form" - "mayfly-go/internal/sys/api/vo" - "mayfly-go/internal/sys/application" - "mayfly-go/internal/sys/domain/entity" - "mayfly-go/pkg/biz" - "mayfly-go/pkg/cache" - "mayfly-go/pkg/ginx" - "mayfly-go/pkg/global" - "mayfly-go/pkg/model" - "mayfly-go/pkg/req" - "mayfly-go/pkg/utils/stringx" - "net/http" - "strconv" - "strings" - "time" -) - -const ( - AuthOAuth2Name string = "OAuth2.0客户端配置" - AuthOAuth2Key string = "AuthOAuth2" - AuthOAuth2Param string = "[{\"name\":\"Client ID\",\"model\":\"clientID\",\"placeholder\":\"客户端id\"}," + - "{\"name\":\"Client Secret\",\"model\":\"clientSecret\",\"placeholder\":\"客户端密钥\"}," + - "{\"name\":\"Authorization URL\",\"model\":\"authorizationURL\",\"placeholder\":\"https://example.com/oauth/authorize\"}," + - "{\"name\":\"Access Token URL\",\"model\":\"accessTokenURL\",\"placeholder\":\"https://example.com/oauth/token\"}," + - "{\"name\":\"Resource URL\",\"model\":\"resourceURL\",\"placeholder\":\"https://example.com/oauth/token\"}," + - "{\"name\":\"Redirect URL\",\"model\":\"redirectURL\",\"placeholder\":\"http://mayfly地址/\"}," + - "{\"name\":\"User identifier\",\"model\":\"userIdentifier\",\"placeholder\":\"\"}," + - "{\"name\":\"Scopes\",\"model\":\"scopes\",\"placeholder\":\"read_user\"}," + - "{\"name\":\"自动注册\",\"model\":\"autoRegister\",\"placeholder\":\"开启自动注册将会自动注册账号, 否则需要手动创建账号后再进行绑定\",\"type\":\"checkbox\"}]" - AuthOAuth2Remark string = "自定义oauth2.0 server登录" -) - -type Auth struct { - ConfigApp application.Config - AuthApp application.Auth - AccountApp application.Account - MsgApp msgapp.Msg -} - -func (a *Auth) OAuth2Login(rc *req.Ctx) { - client, _, err := a.getOAuthClient() - if err != nil { - biz.ErrIsNil(err, "获取oauth2 client失败: "+err.Error()) - return - } - state := stringx.Rand(32) - cache.SetStr("oauth2:state:"+state, "login", 5*time.Minute) - rc.GinCtx.Redirect(http.StatusFound, client.AuthCodeURL(state)) -} - -func (a *Auth) OAuth2Callback(rc *req.Ctx) { - client, oauth, err := a.getOAuthClient() - if err != nil { - biz.ErrIsNil(err, "获取oauth2 client失败: "+err.Error()) - } - code := rc.GinCtx.Query("code") - if code == "" { - biz.ErrIsNil(errors.New("code不能为空"), "code不能为空") - } - state := rc.GinCtx.Query("state") - if state == "" { - biz.ErrIsNil(errors.New("state不能为空"), "state不能为空") - } - stateAction := cache.GetStr("oauth2:state:" + state) - if stateAction == "" { - biz.ErrIsNil(errors.New("state已过期,请重新登录"), "state已过期,请重新登录") - } - token, err := client.Exchange(rc.GinCtx, code) - if err != nil { - biz.ErrIsNil(err, "获取token失败: "+err.Error()) - } - // 获取用户信息 - httpCli := client.Client(rc.GinCtx.Request.Context(), token) - resp, err := httpCli.Get(oauth.ResourceURL) - if err != nil { - biz.ErrIsNil(err, "获取用户信息失败: "+err.Error()) - } - defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) - if err != nil { - biz.ErrIsNil(err, "获取用户信息失败: "+err.Error()) - } - userInfo := make(map[string]interface{}) - err = json.Unmarshal(b, &userInfo) - if err != nil { - biz.ErrIsNil(err, "解析用户信息失败: "+err.Error()) - } - - // 获取用户唯一标识 - keys := strings.Split(oauth.UserIdentifier, ".") - var identifier interface{} = userInfo - endKey := keys[len(keys)-1] - keys = keys[:len(keys)-1] - for _, key := range keys { - identifier = identifier.(map[string]interface{})[key] - } - identifier = identifier.(map[string]interface{})[endKey] - userId := "" - switch identifier.(type) { - case string: - userId = identifier.(string) - case int, int32, int64: - userId = fmt.Sprintf("%d", identifier) - case float32, float64: - userId = fmt.Sprintf("%.0f", identifier.(float64)) - } - // 查询用户是否存在 - oauthAccount := &entity.OAuthAccount{Identity: userId} - err = a.AuthApp.GetOAuthAccount(oauthAccount, "account_id", "identity") - // 判断是登录还是绑定 - if stateAction == "login" { - var accountId uint64 - if err != nil { - if err != gorm.ErrRecordNotFound { - biz.ErrIsNil(err, "查询用户失败: "+err.Error()) - } - // 不存在,进行注册 - if !oauth.AutoRegister { - biz.ErrIsNil(errors.New("未绑定账号, 请先注册"), "未绑定账号, 请先注册") - } - now := time.Now() - account := &entity.Account{ - Model: model.Model{ - CreateTime: &now, - CreatorId: 0, - Creator: "oauth2", - UpdateTime: &now, - }, - Name: userId, - Username: userId, - } - a.AccountApp.Create(account) - // 绑定 - if err := a.AuthApp.BindOAuthAccount(&entity.OAuthAccount{ - AccountId: account.Id, - Identity: oauthAccount.Identity, - CreateTime: &now, - UpdateTime: &now, - }); err != nil { - biz.ErrIsNil(err, "绑定用户失败: "+err.Error()) - } - accountId = account.Id - } else { - accountId = oauthAccount.AccountId - } - // 进行登录 - account := &entity.Account{ - Model: model.Model{DeletedModel: model.DeletedModel{Id: accountId}}, - } - if err := a.AccountApp.GetAccount(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret"); err != nil { - biz.ErrIsNil(err, "获取用户信息失败: "+err.Error()) - } - biz.IsTrue(account.IsEnable(), "该账号不可用") - // 访问系统使用的token - accessToken := req.CreateToken(accountId, account.Username) - // 默认为不校验otp - otpStatus := OtpStatusNone - clientIp := rc.GinCtx.ClientIP() - rc.ReqParam = fmt.Sprintf("oauth2 login username: %s | ip: %s", account.Username, clientIp) - - res := map[string]any{ - "name": account.Name, - "username": account.Username, - "lastLoginTime": account.LastLoginTime, - "lastLoginIp": account.LastLoginIp, - } - - accountLoginSecurity := a.ConfigApp.GetConfig(entity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity() - // 判断otp - if accountLoginSecurity.UseOtp { - otpInfo, otpurl, otpToken := useOtp(account, accountLoginSecurity.OtpIssuer, accessToken) - otpStatus = otpInfo.OptStatus - if otpurl != "" { - res["otpUrl"] = otpurl - } - accessToken = otpToken - } else { - // 保存登录消息 - go saveLogin(a.AccountApp, a.MsgApp, account, getIpAndRegion(rc)) - } - // 赋值otp状态 - res["action"] = "oauthLogin" - res["otp"] = otpStatus - res["token"] = accessToken - 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("" + - "" + - "") - } else if sAccountId, ok := strings.CutPrefix(stateAction, "bind:"); ok { - // 绑定 - accountId, err := strconv.ParseUint(sAccountId, 10, 64) - if err != nil { - biz.ErrIsNil(err, "绑定用户失败: "+err.Error()) - } - now := time.Now() - if err := a.AuthApp.BindOAuthAccount(&entity.OAuthAccount{ - AccountId: accountId, - Identity: oauthAccount.Identity, - CreateTime: &now, - UpdateTime: &now, - }); err != nil { - biz.ErrIsNil(err, "绑定用户失败: "+err.Error()) - } - res := map[string]any{ - "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("" + - "" + - "") - } else { - biz.ErrIsNil(errors.New("state不合法"), "state不合法") - } -} - -func (a *Auth) getOAuthClient() (*oauth2.Config, *vo.OAuth2VO, error) { - config := a.ConfigApp.GetConfig(AuthOAuth2Key) - oauth2Vo := &vo.OAuth2VO{} - if config.Value != "" { - if err := json.Unmarshal([]byte(config.Value), oauth2Vo); err != nil { - global.Log.Warnf("解析自定义oauth2配置失败,err:%s", err.Error()) - return nil, nil, errors.New("解析自定义oauth2配置失败") - } - } - if oauth2Vo.ClientID == "" { - biz.ErrIsNil(nil, "请先配置oauth2") - return nil, nil, errors.New("请先配置oauth2") - } - client := &oauth2.Config{ - ClientID: oauth2Vo.ClientID, - ClientSecret: oauth2Vo.ClientSecret, - Endpoint: oauth2.Endpoint{ - AuthURL: oauth2Vo.AuthorizationURL, - TokenURL: oauth2Vo.AccessTokenURL, - }, - RedirectURL: oauth2Vo.RedirectURL + "api/sys/auth/oauth2/callback", - Scopes: strings.Split(oauth2Vo.Scopes, ","), - } - return client, oauth2Vo, nil -} - -// GetInfo 获取认证平台信息 -func (a *Auth) GetInfo(rc *req.Ctx) { - config := a.ConfigApp.GetConfig(AuthOAuth2Key) - oauth2 := &vo.OAuth2VO{} - if config.Value != "" { - if err := json.Unmarshal([]byte(config.Value), oauth2); err != nil { - global.Log.Warnf("解析自定义oauth2配置失败,err:%s", err.Error()) - biz.ErrIsNil(err, "解析自定义oauth2配置失败") - } - } - rc.ResData = &vo.AuthVO{ - OAuth2VO: oauth2, - } -} - -func (a *Auth) SaveOAuth2(rc *req.Ctx) { - form := &form2.OAuth2Form{} - form = ginx.BindJsonAndValid(rc.GinCtx, form) - rc.ReqParam = form - // 先获取看看有没有 - config := a.ConfigApp.GetConfig(AuthOAuth2Key) - now := time.Now() - if config.Id == 0 { - config.CreatorId = rc.LoginAccount.Id - config.Creator = rc.LoginAccount.Username - config.CreateTime = &now - } - config.ModifierId = rc.LoginAccount.Id - config.Modifier = rc.LoginAccount.Username - config.UpdateTime = &now - config.Name = AuthOAuth2Name - config.Key = AuthOAuth2Key - config.Params = AuthOAuth2Param - b, err := json.Marshal(form) - if err != nil { - biz.ErrIsNil(err, "json marshal error") - return - } - config.Value = string(b) - config.Remark = AuthOAuth2Remark - a.ConfigApp.Save(config) -} - -func (a *Auth) OAuth2Bind(rc *req.Ctx) { - client, _, err := a.getOAuthClient() - if err != nil { - biz.ErrIsNil(err, "获取oauth2 client失败: "+err.Error()) - return - } - state := stringx.Rand(32) - cache.SetStr("oauth2:state:"+state, "bind:"+strconv.FormatUint(rc.LoginAccount.Id, 10), - 5*time.Minute) - rc.GinCtx.Redirect(http.StatusFound, client.AuthCodeURL(state)) -} - -func (a *Auth) Auth2Status(ctx *req.Ctx) { - res := &vo.AuthStatusVO{} - config := a.ConfigApp.GetConfig(AuthOAuth2Key) - if config.Value != "" { - oauth2 := &vo.OAuth2VO{} - if err := json.Unmarshal([]byte(config.Value), oauth2); err != nil { - global.Log.Warnf("解析自定义oauth2配置失败,err:%s", err.Error()) - biz.ErrIsNil(err, "解析自定义oauth2配置失败") - } else if oauth2.ClientID != "" { - res.Enable.OAuth2 = true - } - } - if res.Enable.OAuth2 { - err := a.AuthApp.GetOAuthAccount(&entity.OAuthAccount{ - AccountId: ctx.LoginAccount.Id, - }, "account_id", "identity") - if err != nil { - if err != gorm.ErrRecordNotFound { - biz.ErrIsNil(err, "查询用户失败: "+err.Error()) - } - } else { - res.Bind.OAuth2 = true - } - } - ctx.ResData = res -} diff --git a/server/internal/sys/api/config.go b/server/internal/sys/api/config.go index 59e3c782..159d9daa 100644 --- a/server/internal/sys/api/config.go +++ b/server/internal/sys/api/config.go @@ -1,14 +1,11 @@ package api import ( - "encoding/json" "mayfly-go/internal/sys/api/form" - "mayfly-go/internal/sys/api/vo" "mayfly-go/internal/sys/application" "mayfly-go/internal/sys/domain/entity" "mayfly-go/pkg/biz" "mayfly-go/pkg/ginx" - "mayfly-go/pkg/global" "mayfly-go/pkg/req" ) @@ -51,18 +48,10 @@ func (c *Config) SaveConfig(rc *req.Ctx) { c.ConfigApp.Save(config) } -// AuthConfig auth相关配置 -func (c *Config) AuthConfig(rc *req.Ctx) { - resp := &vo.Auth2EnableVO{} - config := c.ConfigApp.GetConfig(AuthOAuth2Key) - oauth2 := &vo.OAuth2VO{} - if config.Value != "" { - if err := json.Unmarshal([]byte(config.Value), oauth2); err != nil { - global.Log.Warnf("解析自定义oauth2配置失败,err:%s", err.Error()) - biz.ErrIsNil(err, "解析自定义oauth2配置失败") - } else if oauth2.ClientID != "" { - resp.OAuth2 = true - } +// 获取oauth2登录配置信息,因为有些字段是敏感字段,故单独使用接口获取 +func (c *Config) Oauth2Config(rc *req.Ctx) { + oauth2LoginConfig := c.ConfigApp.GetConfig(entity.ConfigKeyOauth2Login).ToOauth2Login() + rc.ResData = map[string]any{ + "enable": oauth2LoginConfig.Enable, } - rc.ResData = resp } diff --git a/server/internal/sys/api/form/auth.go b/server/internal/sys/api/form/auth.go deleted file mode 100644 index e75b9ced..00000000 --- a/server/internal/sys/api/form/auth.go +++ /dev/null @@ -1,13 +0,0 @@ -package form - -type OAuth2Form struct { - ClientID string `json:"clientID" binding:"required"` - ClientSecret string `json:"clientSecret" binding:"required"` - AuthorizationURL string `json:"authorizationURL" binding:"required,url"` - AccessTokenURL string `json:"accessTokenURL" binding:"required,url"` - ResourceURL string `json:"resourceURL" binding:"required,url"` - RedirectURL string `json:"redirectURL" binding:"required,url"` - UserIdentifier string `json:"userIdentifier" binding:"required"` - Scopes string `json:"scopes"` - AutoRegister bool `json:"autoRegister"` -} diff --git a/server/internal/sys/api/vo/auth.go b/server/internal/sys/api/vo/auth.go deleted file mode 100644 index dbc37e6e..00000000 --- a/server/internal/sys/api/vo/auth.go +++ /dev/null @@ -1,26 +0,0 @@ -package vo - -type OAuth2VO struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - AuthorizationURL string `json:"authorizationURL"` - AccessTokenURL string `json:"accessTokenURL"` - ResourceURL string `json:"resourceURL"` - RedirectURL string `json:"redirectURL"` - UserIdentifier string `json:"userIdentifier"` - Scopes string `json:"scopes"` - AutoRegister bool `json:"autoRegister"` -} - -type AuthVO struct { - *OAuth2VO `json:"oauth2"` -} - -type Auth2EnableVO struct { - OAuth2 bool `json:"oauth2"` -} - -type AuthStatusVO struct { - Enable Auth2EnableVO `json:"enable"` - Bind Auth2EnableVO `json:"bind"` -} diff --git a/server/internal/sys/application/application.go b/server/internal/sys/application/application.go index 059aa04f..08694abe 100644 --- a/server/internal/sys/application/application.go +++ b/server/internal/sys/application/application.go @@ -6,7 +6,6 @@ import ( var ( accountApp = newAccountApp(persistence.GetAccountRepo()) - authApp = newAuthApp(persistence.GetOAuthAccountRepo()) configApp = newConfigApp(persistence.GetConfigRepo()) resourceApp = newResourceApp(persistence.GetResourceRepo()) roleApp = newRoleApp(persistence.GetRoleRepo()) @@ -17,10 +16,6 @@ func GetAccountApp() Account { return accountApp } -func GetAuthApp() Auth { - return authApp -} - func GetConfigApp() Config { return configApp } diff --git a/server/internal/sys/application/auth.go b/server/internal/sys/application/auth.go deleted file mode 100644 index a1772e64..00000000 --- a/server/internal/sys/application/auth.go +++ /dev/null @@ -1,29 +0,0 @@ -package application - -import ( - "mayfly-go/internal/sys/domain/entity" - "mayfly-go/internal/sys/domain/repository" -) - -type Auth interface { - GetOAuthAccount(condition *entity.OAuthAccount, cols ...string) error - BindOAuthAccount(e *entity.OAuthAccount) error -} - -func newAuthApp(oauthAccountRepo repository.OAuthAccount) Auth { - return &authAppImpl{ - oauthAccountRepo: oauthAccountRepo, - } -} - -type authAppImpl struct { - oauthAccountRepo repository.OAuthAccount -} - -func (a *authAppImpl) GetOAuthAccount(condition *entity.OAuthAccount, cols ...string) error { - return a.oauthAccountRepo.GetOAuthAccount(condition, cols...) -} - -func (a *authAppImpl) BindOAuthAccount(e *entity.OAuthAccount) error { - return a.oauthAccountRepo.SaveOAuthAccount(e) -} diff --git a/server/internal/sys/domain/entity/config.go b/server/internal/sys/domain/entity/config.go index 513ab419..36bab420 100644 --- a/server/internal/sys/domain/entity/config.go +++ b/server/internal/sys/domain/entity/config.go @@ -8,8 +8,7 @@ import ( const ( ConfigKeyAccountLoginSecurity string = "AccountLoginSecurity" // 账号登录安全配置 - ConfigKeyUseLoginCaptcha string = "UseLoginCaptcha" // 是否使用登录验证码 - ConfigKeyUseLoginOtp string = "UseLoginOtp" // 是否开启otp双因素校验 + ConfigKeyOauth2Login string = "Oauth2Login" // oauth2认证登录配置 ConfigKeyDbQueryMaxCount string = "DbQueryMaxCount" // 数据库查询的最大数量 ConfigKeyDbSaveQuerySQL string = "DbSaveQuerySQL" // 数据库是否记录查询相关sql ConfigUseWartermark string = "UseWartermark" // 是否使用水印 @@ -19,8 +18,8 @@ type Config struct { model.Model Name string `json:"name"` // 配置名 Key string `json:"key"` // 配置key - Params string `json:"params" gorm:"column:params;type:varchar(1000)"` - Value string `json:"value" gorm:"column:value;type:varchar(1000)"` + Params string `json:"params" gorm:"column:params;type:varchar(1500)"` + Value string `json:"value" gorm:"column:value;type:varchar(1500)"` Remark string `json:"remark"` } @@ -80,6 +79,36 @@ func (c *Config) ToAccountLoginSecurity() *AccountLoginSecurity { return als } +type ConfigOauth2Login struct { + Enable bool // 是否启用 + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + AuthorizationURL string `json:"authorizationURL"` + AccessTokenURL string `json:"accessTokenURL"` + RedirectURL string `json:"redirectURL"` + Scopes string `json:"scopes"` + ResourceURL string `json:"resourceURL"` + UserIdentifier string `json:"userIdentifier"` + AutoRegister bool `json:"autoRegister"` // 是否自动注册 +} + +// 转换为Oauth2Login结构体 +func (c *Config) ToOauth2Login() *ConfigOauth2Login { + jm := c.GetJsonMap() + ol := new(ConfigOauth2Login) + ol.Enable = convertBool(jm["enable"], false) + ol.ClientId = jm["clientId"] + ol.ClientSecret = jm["clientSecret"] + ol.AuthorizationURL = jm["authorizationURL"] + ol.AccessTokenURL = jm["accessTokenURL"] + ol.RedirectURL = jm["redirectURL"] + ol.Scopes = jm["scopes"] + ol.ResourceURL = jm["resourceURL"] + ol.UserIdentifier = jm["userIdentifier"] + ol.AutoRegister = convertBool(jm["autoRegister"], true) + return ol +} + // 转换配置中的值为bool类型(默认"1"或"true"为true,其他为false) func convertBool(value string, defaultValue bool) bool { if value == "" { diff --git a/server/internal/sys/domain/repository/auth.go b/server/internal/sys/domain/repository/auth.go deleted file mode 100644 index e5605c56..00000000 --- a/server/internal/sys/domain/repository/auth.go +++ /dev/null @@ -1,10 +0,0 @@ -package repository - -import "mayfly-go/internal/sys/domain/entity" - -type OAuthAccount interface { - // GetOAuthAccount 根据identity获取第三方账号信息 - GetOAuthAccount(condition *entity.OAuthAccount, cols ...string) error - - SaveOAuthAccount(e *entity.OAuthAccount) error -} diff --git a/server/internal/sys/infrastructure/persistence/auth.go b/server/internal/sys/infrastructure/persistence/auth.go deleted file mode 100644 index ead99ff9..00000000 --- a/server/internal/sys/infrastructure/persistence/auth.go +++ /dev/null @@ -1,24 +0,0 @@ -package persistence - -import ( - "mayfly-go/internal/sys/domain/entity" - "mayfly-go/internal/sys/domain/repository" - "mayfly-go/pkg/gormx" -) - -type authAccountRepoImpl struct{} - -func newAuthAccountRepo() repository.OAuthAccount { - return new(authAccountRepoImpl) -} - -func (a *authAccountRepoImpl) GetOAuthAccount(condition *entity.OAuthAccount, cols ...string) error { - return gormx.GetBy(condition, cols...) -} - -func (a *authAccountRepoImpl) SaveOAuthAccount(e *entity.OAuthAccount) error { - if e.Id == 0 { - return gormx.Insert(e) - } - return gormx.UpdateById(e) -} diff --git a/server/internal/sys/infrastructure/persistence/persistence.go b/server/internal/sys/infrastructure/persistence/persistence.go index ad07b943..ca95c5d2 100644 --- a/server/internal/sys/infrastructure/persistence/persistence.go +++ b/server/internal/sys/infrastructure/persistence/persistence.go @@ -3,22 +3,17 @@ package persistence import "mayfly-go/internal/sys/domain/repository" var ( - accountRepo = newAccountRepo() - authAccountRepo = newAuthAccountRepo() - configRepo = newConfigRepo() - resourceRepo = newResourceRepo() - roleRepo = newRoleRepo() - syslogRepo = newSyslogRepo() + accountRepo = newAccountRepo() + configRepo = newConfigRepo() + resourceRepo = newResourceRepo() + roleRepo = newRoleRepo() + syslogRepo = newSyslogRepo() ) func GetAccountRepo() repository.Account { return accountRepo } -func GetOAuthAccountRepo() repository.OAuthAccount { - return authAccountRepo -} - func GetConfigRepo() repository.Config { return configRepo } diff --git a/server/internal/sys/router/account.go b/server/internal/sys/router/account.go index eb7b4ce4..ef2f7631 100644 --- a/server/internal/sys/router/account.go +++ b/server/internal/sys/router/account.go @@ -22,11 +22,6 @@ func InitAccountRouter(router *gin.RouterGroup) { addAccountPermission := req.NewPermission("account:add") reqs := [...]*req.Conf{ - // 用户登录 - req.NewPost("/login", a.Login).Log(req.NewLogSave("用户登录")).DontNeedToken(), - - // otp双因素校验 - req.NewPost("/otp-verify", a.OtpVerify).DontNeedToken(), // 获取个人账号的权限资源信息 req.NewGet("/permissions", a.GetPermissions), diff --git a/server/internal/sys/router/auth.go b/server/internal/sys/router/auth.go deleted file mode 100644 index 0f0670a6..00000000 --- a/server/internal/sys/router/auth.go +++ /dev/null @@ -1,35 +0,0 @@ -package router - -import ( - "github.com/gin-gonic/gin" - msgapp "mayfly-go/internal/msg/application" - "mayfly-go/internal/sys/api" - "mayfly-go/internal/sys/application" - "mayfly-go/pkg/req" -) - -func InitSysAuthRouter(router *gin.RouterGroup) { - r := &api.Auth{ - ConfigApp: application.GetConfigApp(), - AuthApp: application.GetAuthApp(), - AccountApp: application.GetAccountApp(), - MsgApp: msgapp.GetMsgApp(), - } - rg := router.Group("sys/auth") - - baseP := req.NewPermission("system:auth:base") - - reqs := [...]*req.Conf{ - req.NewGet("", r.GetInfo).RequiredPermission(baseP), - - req.NewPut("/oauth2", r.SaveOAuth2).RequiredPermission(baseP), - - req.NewGet("/status", r.Auth2Status), - req.NewGet("/oauth2/bind", r.OAuth2Bind), - - req.NewGet("/oauth2/login", r.OAuth2Login).DontNeedToken(), - req.NewGet("/oauth2/callback", r.OAuth2Callback).NoRes().DontNeedToken(), - } - - req.BatchSetGroup(rg, reqs[:]) -} diff --git a/server/internal/sys/router/config.go b/server/internal/sys/router/config.go index e7511df2..332f18d0 100644 --- a/server/internal/sys/router/config.go +++ b/server/internal/sys/router/config.go @@ -26,7 +26,7 @@ func InitSysConfigRouter(router *gin.RouterGroup) { entity.ConfigUseWartermark, })).DontNeedToken(), - req.NewGet("/auth", r.AuthConfig).DontNeedToken(), + req.NewGet("/oauth2-login", r.Oauth2Config).DontNeedToken(), req.NewPost("", r.SaveConfig).Log(req.NewLogSave("保存系统配置信息")). RequiredPermissionCode("config:save"), diff --git a/server/internal/sys/router/router.go b/server/internal/sys/router/router.go index 104a277e..b94e305a 100644 --- a/server/internal/sys/router/router.go +++ b/server/internal/sys/router/router.go @@ -10,5 +10,4 @@ func Init(router *gin.RouterGroup) { InitSystemRouter(router) InitSyslogRouter(router) InitSysConfigRouter(router) - InitSysAuthRouter(router) } diff --git a/server/mayfly-go.sql b/server/mayfly-go.sql index ec131146..37001fab 100644 --- a/server/mayfly-go.sql +++ b/server/mayfly-go.sql @@ -346,6 +346,18 @@ CREATE TABLE `t_redis` ( BEGIN; COMMIT; +DROP TABLE IF EXISTS `t_oauth2_account`; +CREATE TABLE `t_oauth2_account` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `account_id` bigint NOT NULL, + `identity` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL, + `create_time` datetime NOT NULL, + `update_time` datetime NOT NULL, + `is_deleted` tinyint DEFAULT 0, + `delete_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='oauth2关联账号' + -- ---------------------------- -- Table structure for t_sys_account -- ---------------------------- @@ -408,8 +420,8 @@ CREATE TABLE `t_sys_config` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '配置名', `key` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '配置key', - `params` varchar(500) COLLATE utf8mb4_bin DEFAULT NULL, - `value` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '配置value', + `params` varchar(1500) COLLATE utf8mb4_bin DEFAULT NULL, + `value` varchar(1500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '配置value', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注', `create_time` datetime NOT NULL, `creator_id` bigint(20) NOT NULL, @@ -427,6 +439,7 @@ CREATE TABLE `t_sys_config` ( -- ---------------------------- BEGIN; INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier) VALUES('账号登录安全设置', 'AccountLoginSecurity', '[{"name":"登录验证码","model":"useCaptcha","placeholder":"是否启用登录验证码","options":"true,false"},{"name":"双因素校验(OTP)","model":"useOtp","placeholder":"是否启用双因素(OTP)校验","options":"true,false"},{"name":"OTP签发人","model":"otpIssuer","placeholder":"otp签发人"},{"name":"允许失败次数","model":"loginFailCount","placeholder":"登录失败n次后禁止登录"},{"name":"禁止登录时间","model":"loginFailMin","placeholder":"登录失败指定次数后禁止m分钟内再次登录"}]', '{"useCaptcha":"true","useOtp":"false","loginFailCount":"5","loginFailMin":"10","otpIssuer":"mayfly-go"}', '系统账号登录相关安全设置', '2023-06-17 11:02:11', 1, 'admin', '2023-06-17 14:18:07', 1, 'admin'); +INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('oauth2登录配置', 'Oauth2Login', '[{"name":"是否启用","model":"enable","placeholder":"是否启用oauth2登录","options":"true,false"},{"name":"Client ID","model":"clientId","placeholder":"Client ID"},{"name":"Client Secret","model":"clientSecret","placeholder":"Client Secret"},{"name":"Authorization URL","model":"authorizationURL","placeholder":"Authorization URL"},{"name":"AccessToken URL","model":"accessTokenURL","placeholder":"AccessToken URL"},{"name":"Redirect URL","model":"redirectURL","placeholder":"本系统地址"},{"name":"Scopes","model":"scopes","placeholder":"Scopes"},{"name":"Resource URL","model":"resourceURL","placeholder":"获取用户信息资源地址"},{"name":"UserIdentifier","model":"userIdentifier","placeholder":"用户唯一标识字段;格式为type:fieldPath(string:username)"},{"name":"是否自动注册","model":"autoRegister","placeholder":"","options":"true,false"}]', '', 'oauth2登录相关配置信息', '2023-07-22 13:58:51', 1, 'admin', '2023-07-22 19:34:37', 1, 'admin', 0, NULL); INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('是否启用水印', 'UseWartermark', NULL, '1', '1: 启用、0: 不启用', '2022-08-25 23:36:35', 1, 'admin', '2022-08-26 10:02:52', 1, 'admin'); INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库查询最大结果集', 'DbQueryMaxCount', '[]', '200', '允许sql查询的最大结果集数。注: 0=不限制', '2023-02-11 14:29:03', 1, 'admin', '2023-02-11 14:40:56', 1, 'admin'); INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库是否记录查询SQL', 'DbSaveQuerySQL', '[]', '0', '1: 记录、0:不记录', '2023-02-11 16:07:14', 1, 'admin', '2023-02-11 16:44:17', 1, 'admin'); diff --git a/server/migrations/2022.go b/server/migrations/2022.go index a47596ac..c5b2c319 100644 --- a/server/migrations/2022.go +++ b/server/migrations/2022.go @@ -1,8 +1,6 @@ package migrations import ( - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" entity2 "mayfly-go/internal/db/domain/entity" "mayfly-go/internal/machine/domain/entity" entity3 "mayfly-go/internal/mongo/domain/entity" @@ -10,6 +8,9 @@ import ( entity4 "mayfly-go/internal/redis/domain/entity" entity5 "mayfly-go/internal/sys/domain/entity" entity7 "mayfly-go/internal/tag/domain/entity" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" ) // T2022 TODO 在此之前的数据库表结构初始化, 目前先使用mayfly-go.sql文件初始化数据库结构 diff --git a/server/migrations/20230720.go b/server/migrations/20230720.go index fb80b674..70f6fc8a 100644 --- a/server/migrations/20230720.go +++ b/server/migrations/20230720.go @@ -1,77 +1,18 @@ package migrations import ( + authentity "mayfly-go/internal/auth/domain/entity" + "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" - "mayfly-go/internal/sys/api" - "mayfly-go/internal/sys/domain/entity" - "mayfly-go/pkg/model" - "time" ) // T20230720 三方登录表 func T20230720() *gormigrate.Migration { return &gormigrate.Migration{ - ID: "20230319", + ID: "20230720", Migrate: func(tx *gorm.DB) error { - // 添加路由权限 - res := &entity.Resource{ - Model: model.Model{ - DeletedModel: model.DeletedModel{Id: 133}, - }, - Pid: 4, - UiPath: "sys/auth", - Type: 1, - Status: 1, - Code: "system:auth", - Name: "登录认证", - Weight: 10000001, - Meta: "{\"component\":\"system/auth/AuthInfo\"," + - "\"icon\":\"User\",\"isKeepAlive\":true," + - "\"routeName\":\"AuthInfo\"}", - } - if err := insertResource(tx, res); err != nil { - return err - } - res = &entity.Resource{ - Model: model.Model{ - DeletedModel: model.DeletedModel{Id: 134}, - }, - Pid: 133, - UiPath: "sys/auth/base", - Type: 2, - Status: 1, - Code: "system:auth:base", - Name: "基本权限", - Weight: 10000000, - Meta: "null", - } - if err := insertResource(tx, res); err != nil { - return err - } - // 加大params字段长度 - now := time.Now() - if err := tx.AutoMigrate(&entity.Config{}); err != nil { - return err - } - if err := tx.Save(&entity.Config{ - Model: model.Model{ - CreateTime: &now, - CreatorId: 1, - Creator: "admin", - UpdateTime: &now, - ModifierId: 1, - Modifier: "admin", - }, - Name: api.AuthOAuth2Name, - Key: api.AuthOAuth2Key, - Params: api.AuthOAuth2Param, - Value: "{}", - Remark: api.AuthOAuth2Remark, - }).Error; err != nil { - return err - } - return tx.AutoMigrate(&entity.OAuthAccount{}) + return tx.AutoMigrate(&authentity.Oauth2Account{}) }, Rollback: func(tx *gorm.DB) error { return nil diff --git a/server/migrations/init.go b/server/migrations/init.go index 02e5361e..7ba2f56d 100644 --- a/server/migrations/init.go +++ b/server/migrations/init.go @@ -1,29 +1,33 @@ package migrations import ( - "context" - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" "mayfly-go/internal/sys/domain/entity" + "mayfly-go/pkg/config" "mayfly-go/pkg/model" "mayfly-go/pkg/rediscli" "time" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" ) // RunMigrations 数据库迁移操作 func RunMigrations(db *gorm.DB) error { // 添加分布式锁, 防止多个服务同时执行迁移 - if rediscli.GetCli() != nil { - if ok, err := rediscli.GetCli(). - SetNX(context.Background(), "migrations", "lock", time.Minute).Result(); err != nil { - return err - } else if !ok { + lock := rediscli.NewLock("mayfly:db:migrations", 1*time.Minute) + if lock != nil { + if !lock.Lock() { return nil } - defer rediscli.Del("migrations") + defer lock.UnLock() } + + if !config.Conf.Mysql.AutoMigration { + return nil + } + return run(db, - T2022, + // T2022, T20230720, ) } diff --git a/server/pkg/config/mysql.go b/server/pkg/config/mysql.go index ca45e569..b761b167 100644 --- a/server/pkg/config/mysql.go +++ b/server/pkg/config/mysql.go @@ -1,15 +1,16 @@ package config type Mysql struct { - Host string `mapstructure:"path" json:"host" yaml:"host"` - Config string `mapstructure:"config" json:"config" yaml:"config"` - Dbname string `mapstructure:"db-name" json:"dbname" yaml:"db-name"` - Username string `mapstructure:"username" json:"username" yaml:"username"` - Password string `mapstructure:"password" json:"password" yaml:"password"` - MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"` - MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"` - LogMode bool `mapstructure:"log-mode" json:"logMode" yaml:"log-mode"` - LogZap string `mapstructure:"log-zap" json:"logZap" yaml:"log-zap"` + AutoMigration bool `mapstructure:"auto-migration" json:"autoMigration" yaml:"auto-migration"` + Host string `mapstructure:"path" json:"host" yaml:"host"` + Config string `mapstructure:"config" json:"config" yaml:"config"` + Dbname string `mapstructure:"db-name" json:"dbname" yaml:"db-name"` + Username string `mapstructure:"username" json:"username" yaml:"username"` + Password string `mapstructure:"password" json:"password" yaml:"password"` + MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"` + MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"` + LogMode bool `mapstructure:"log-mode" json:"logMode" yaml:"log-mode"` + LogZap string `mapstructure:"log-zap" json:"logZap" yaml:"log-zap"` } func (m *Mysql) Dsn() string { diff --git a/server/pkg/gormx/gormx.go b/server/pkg/gormx/gormx.go index 92eff8cb..ec57fb3b 100644 --- a/server/pkg/gormx/gormx.go +++ b/server/pkg/gormx/gormx.go @@ -35,7 +35,7 @@ func GetByIdIn(model any, list any, ids []uint64, orderBy ...string) { // 若 error不为nil,则为不存在该记录 // @param model 数据库映射实体模型 func GetBy(model any, cols ...string) error { - return global.Db.Debug().Select(cols).Where(model).Scopes(UndeleteScope).First(model).Error + return global.Db.Select(cols).Where(model).Scopes(UndeleteScope).First(model).Error } // 根据model指定条件统计数量 diff --git a/server/pkg/req/permission_handler.go b/server/pkg/req/permission_handler.go index 42dba034..3272fb9d 100644 --- a/server/pkg/req/permission_handler.go +++ b/server/pkg/req/permission_handler.go @@ -79,6 +79,10 @@ func SetPermissionCodeRegistery(pcr PermissionCodeRegistry) { permissionCodeRegistry = pcr } +func GetPermissionCodeRegistery() PermissionCodeRegistry { + return permissionCodeRegistry +} + type PermissionCodeRegistry interface { // 保存用户权限code SaveCodes(userId uint64, codes []string) @@ -117,7 +121,9 @@ func (r *DefaultPermissionCodeRegistry) HasCode(userId uint64, code string) bool } func (r *DefaultPermissionCodeRegistry) Remove(userId uint64) { - r.cache.Delete(fmt.Sprintf("%v", userId)) + if r.cache != nil { + r.cache.Delete(fmt.Sprintf("%v", userId)) + } } type RedisPermissionCodeRegistry struct { diff --git a/server/pkg/starter/run.go b/server/pkg/starter/run.go index 9011769a..14f2d875 100644 --- a/server/pkg/starter/run.go +++ b/server/pkg/starter/run.go @@ -1,8 +1,8 @@ package starter import ( - "mayfly-go/migrations" "mayfly-go/initialize" + "mayfly-go/migrations" "mayfly-go/pkg/config" "mayfly-go/pkg/global" "mayfly-go/pkg/logger" @@ -27,9 +27,10 @@ func RunWebServer() { // 有配置redis信息,则初始化redis。多台机器部署需要使用redis存储验证码、权限、公私钥等 initRedis() + // 数据库升级操作 if err := migrations.RunMigrations(global.Db); err != nil { - logger.Log.Fatalf("数据库升级失败: %v", err) + global.Log.Fatalf("数据库升级失败: %v", err) } // 初始化其他需要启动时运行的方法 diff --git a/server/pkg/utils/jsonx/jsonx.go b/server/pkg/utils/jsonx/jsonx.go index cc7b08ac..6a8bccd5 100644 --- a/server/pkg/utils/jsonx/jsonx.go +++ b/server/pkg/utils/jsonx/jsonx.go @@ -3,15 +3,23 @@ package jsonx import ( "encoding/json" "mayfly-go/pkg/global" + "strings" + + "github.com/buger/jsonparser" ) // json字符串转map func ToMap(jsonStr string) map[string]any { + return ToMapByBytes([]byte(jsonStr)) +} + +// json字节数组转map +func ToMapByBytes(bytes []byte) map[string]any { var res map[string]any - if jsonStr == "" { - return res + err := json.Unmarshal(bytes, &res) + if err != nil { + global.Log.Errorf("json字符串转map失败: %s", err.Error()) } - _ = json.Unmarshal([]byte(jsonStr), &res) return res } @@ -24,3 +32,45 @@ func ToStr(val any) string { return string(strBytes) } } + +// 根据json字节数组获取对应字段路径的string类型值 +// +// @param fieldPath字段路径。如user.username等 +func GetStringByBytes(bytes []byte, fieldPath string) (string, error) { + return jsonparser.GetString(bytes, strings.Split(fieldPath, ".")...) +} + +// 根据json字符串获取对应字段路径的string类型值 +// +// @param fieldPath字段路径。如user.username等 +func GetString(jsonStr string, fieldPath string) (string, error) { + return GetStringByBytes([]byte(jsonStr), fieldPath) +} + +// 根据json字节数组获取对应字段路径的int类型值 +// +// @param fieldPath字段路径。如user.age等 +func GetIntByBytes(bytes []byte, fieldPath string) (int64, error) { + return jsonparser.GetInt(bytes, strings.Split(fieldPath, ".")...) +} + +// 根据json字符串获取对应字段路径的int类型值 +// +// @param fieldPath字段路径。如user.age等 +func GetInt(jsonStr string, fieldPath string) (int64, error) { + return GetIntByBytes([]byte(jsonStr), fieldPath) +} + +// 根据json字节数组获取对应字段路径的bool类型值 +// +// @param fieldPath字段路径。如user.isDeleted等 +func GetBoolByBytes(bytes []byte, fieldPath string) (bool, error) { + return jsonparser.GetBoolean(bytes, strings.Split(fieldPath, ".")...) +} + +// 根据json字符串获取对应字段路径的bool类型值 +// +// @param fieldPath字段路径。如user.isDeleted等 +func GetBool(jsonStr string, fieldPath string) (bool, error) { + return GetBoolByBytes([]byte(jsonStr), fieldPath) +} diff --git a/server/pkg/utils/jsonx/jsonx_test.go b/server/pkg/utils/jsonx/jsonx_test.go new file mode 100644 index 00000000..343adb50 --- /dev/null +++ b/server/pkg/utils/jsonx/jsonx_test.go @@ -0,0 +1,109 @@ +package jsonx + +import ( + "fmt" + "testing" + + "github.com/buger/jsonparser" +) + +const jsonStr = `{ + "username": "test", + "age": 12, + "person": { + "name": { + "first": "Leonid", + "last": "Bugaev", + "fullName": "Leonid Bugaev" + }, + "github": { + "handle": "buger", + "followers": 109 + }, + "avatars": [ + { "url": "https://avatars1.githubusercontent.com/u/14009?v=3&s=460", "type": "thumbnail" } + ] + }, + "company": { + "name": "Acme" + } + }` + +func TestGetString(t *testing.T) { + // val, err := GetString(jsonStr, "username") + + // 错误路径 + // val, err := GetString(jsonStr, "username1") + + // 含有数组的 + val, err := GetString(jsonStr, "person.avatars.[0].url") + + if err != nil { + fmt.Println("error: ", err.Error()) + } else { + fmt.Println(val) + } +} + +func TestGetInt(t *testing.T) { + val, _ := GetInt(jsonStr, "age") + val2, _ := GetInt(jsonStr, "person.github.followers") + fmt.Println(val, ",", val2) +} + +// 官方demo +func TestJsonParser(t *testing.T) { + data := []byte(jsonStr) + // You can specify key path by providing arguments to Get function + jsonparser.Get(data, "person", "name", "fullName") + + // There is `GetInt` and `GetBoolean` helpers if you exactly know key data type + jsonparser.GetInt(data, "person", "github", "followers") + + // When you try to get object, it will return you []byte slice pointer to data containing it + // In `company` it will be `{"name": "Acme"}` + jsonparser.Get(data, "company") + + // If the key doesn't exist it will throw an error + var size int64 + if value, err := jsonparser.GetInt(data, "company", "size"); err == nil { + size = value + fmt.Println(size) + } + + // You can use `ArrayEach` helper to iterate items [item1, item2 .... itemN] + jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { + fmt.Println(jsonparser.Get(value, "url")) + }, "person", "avatars") + + // Or use can access fields by index! + jsonparser.GetString(data, "person", "avatars", "[0]", "url") + + // You can use `ObjectEach` helper to iterate objects { "key1":object1, "key2":object2, .... "keyN":objectN } + jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error { + fmt.Printf("Key: '%s'\n Value: '%s'\n Type: %s\n", string(key), string(value), dataType) + return nil + }, "person", "name") + + // The most efficient way to extract multiple keys is `EachKey` + + paths := [][]string{ + []string{"person", "name", "fullName"}, + []string{"person", "avatars", "[0]", "url"}, + []string{"company", "url"}, + } + + jsonparser.EachKey(data, func(idx int, value []byte, vt jsonparser.ValueType, err error) { + switch idx { + case 0: // []string{"person", "name", "fullName"} + { + } + case 1: // []string{"person", "avatars", "[0]", "url"} + { + } + case 2: // []string{"company", "url"}, + { + } + } + }, paths...) +}