diff --git a/mayfly_go_web/src/views/personal/api.ts b/mayfly_go_web/src/views/personal/api.ts
index f3f7b9af..34957489 100644
--- a/mayfly_go_web/src/views/personal/api.ts
+++ b/mayfly_go_web/src/views/personal/api.ts
@@ -3,5 +3,6 @@ import Api from '@/common/Api';
export const personApi = {
accountInfo: Api.newGet('/sys/accounts/self'),
updateAccount: Api.newPut('/sys/accounts/self'),
+ authStatus: Api.newGet('/sys/auth/status'),
getMsgs: Api.newGet('/msgs/self'),
};
diff --git a/mayfly_go_web/src/views/personal/index.vue b/mayfly_go_web/src/views/personal/index.vue
index e0212b28..3fecfb93 100644
--- a/mayfly_go_web/src/views/personal/index.vue
+++ b/mayfly_go_web/src/views/personal/index.vue
@@ -155,14 +155,16 @@
-->
-
+
绑定OAuth2
-
已绑定OAuth2
+
+ {{ authStatus.bind.oauth2 ? '已绑定' : '未绑定' }}
+
- 立即绑定
+ 立即绑定
@@ -180,6 +182,8 @@ import { personApi } from './api';
import { dateFormat } from '@/common/utils/date';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
+import config from '@/common/config';
+import { getSession } from '@/common/utils/storage';
const { userInfo } = storeToRefs(useUserInfo());
const state = reactive({
@@ -202,9 +206,13 @@ const state = reactive({
accountForm: {
password: '',
},
+ authStatus: {
+ enable: { oauth2: false },
+ bind: { oauth2: false }
+ }
});
-const { msgDialog, accountForm } = toRefs(state);
+const { msgDialog, accountForm, authStatus } = toRefs(state);
// 当前时间提示语
const currentTime = computed(() => {
@@ -222,9 +230,10 @@ const roleInfo = computed(() => {
return state.accountInfo.roles.map((val: any) => val.name).join('、');
});
-onMounted(() => {
+onMounted(async () => {
getAccountInfo();
getMsgs();
+ state.authStatus = await personApi.authStatus.request()
});
const getAccountInfo = async () => {
@@ -236,6 +245,31 @@ const updateAccount = async () => {
ElMessage.success('更新成功');
};
+const bindOAuth2 = () => {
+ // 小窗口打开oauth2鉴权
+ let oauthWindoe = window.open(config.baseApiUrl + "/sys/auth/oauth2/bind?token=" + getSession('token'), "oauth2", "width=600,height=600");
+ if (oauthWindoe) {
+ const handler = (e: any) => {
+ if (e.data.action === "oauthBind") {
+ oauthWindoe!.close();
+ window.removeEventListener("message", handler);
+ // 处理登录token
+ console.log(e.data);
+ ElMessage.success('绑定成功');
+ setTimeout(() => {
+ location.reload();
+ }, 1000);
+ }
+ }
+ window.addEventListener("message", handler);
+ setInterval(() => {
+ if (oauthWindoe!.closed) {
+ window.removeEventListener("message", handler);
+ }
+ }, 1000);
+ }
+}
+
const getMsgs = async () => {
const res = await personApi.getMsgs.request(state.msgDialog.query);
state.msgDialog.msgs = res;
diff --git a/server/internal/sys/api/auth.go b/server/internal/sys/api/auth.go
index 006f4c95..6a0e2f8e 100644
--- a/server/internal/sys/api/auth.go
+++ b/server/internal/sys/api/auth.go
@@ -20,6 +20,7 @@ import (
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
"net/http"
+ "strconv"
"strings"
"time"
)
@@ -70,7 +71,8 @@ func (a *Auth) OAuth2Callback(rc *req.Ctx) {
if state == "" {
biz.ErrIsNil(errors.New("state不能为空"), "state不能为空")
}
- if cache.GetStr("oauth2:state:"+state) == "" {
+ stateAction := cache.GetStr("oauth2:state:" + state)
+ if stateAction == "" {
biz.ErrIsNil(errors.New("state已过期,请重新登录"), "state已过期,请重新登录")
}
token, err := client.Exchange(rc.GinCtx, code)
@@ -115,86 +117,117 @@ func (a *Auth) OAuth2Callback(rc *req.Ctx) {
// 查询用户是否存在
oauthAccount := &entity.OAuthAccount{Identity: userId}
err = a.AuthApp.GetOAuthAccount(oauthAccount, "account_id", "identity")
- var accountId uint64
- if err != nil {
- if err != gorm.ErrRecordNotFound {
- biz.ErrIsNil(err, "查询用户失败: "+err.Error())
+ // 判断是登录还是绑定
+ 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
}
- // 不存在,进行注册
- if !oauth.AutoRegister {
- biz.ErrIsNil(errors.New("用户不存在,请先注册"), "用户不存在,请先注册")
+ // 进行登录
+ account := &entity.Account{
+ Model: model.Model{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, rc.GinCtx.ClientIP())
+ }
+ // 赋值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()
- 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,
+ AccountId: accountId,
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{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
+ res := map[string]any{
+ "action": "oauthBind",
+ "bind": true,
}
- accessToken = otpToken
+ 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 {
- // 保存登录消息
- go saveLogin(a.AccountApp, a.MsgApp, account, rc.GinCtx.ClientIP())
+ biz.ErrIsNil(errors.New("state不合法"), "state不合法")
}
- // 赋值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("" +
- "" +
- "")
}
func (a *Auth) getOAuthClient() (*oauth2.Config, *vo.OAuth2VO, error) {
@@ -265,3 +298,42 @@ func (a *Auth) SaveOAuth2(rc *req.Ctx) {
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 := utils.RandString(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 85cfdd47..59e3c782 100644
--- a/server/internal/sys/api/config.go
+++ b/server/internal/sys/api/config.go
@@ -53,7 +53,7 @@ func (c *Config) SaveConfig(rc *req.Ctx) {
// AuthConfig auth相关配置
func (c *Config) AuthConfig(rc *req.Ctx) {
- resp := &vo.OAuth2EnableVO{}
+ resp := &vo.Auth2EnableVO{}
config := c.ConfigApp.GetConfig(AuthOAuth2Key)
oauth2 := &vo.OAuth2VO{}
if config.Value != "" {
diff --git a/server/internal/sys/api/vo/auth.go b/server/internal/sys/api/vo/auth.go
index 2356bda6..dbc37e6e 100644
--- a/server/internal/sys/api/vo/auth.go
+++ b/server/internal/sys/api/vo/auth.go
@@ -16,6 +16,11 @@ type AuthVO struct {
*OAuth2VO `json:"oauth2"`
}
-type OAuth2EnableVO struct {
+type Auth2EnableVO struct {
OAuth2 bool `json:"oauth2"`
}
+
+type AuthStatusVO struct {
+ Enable Auth2EnableVO `json:"enable"`
+ Bind Auth2EnableVO `json:"bind"`
+}
diff --git a/server/internal/sys/router/auth.go b/server/internal/sys/router/auth.go
index 0935472b..0f0670a6 100644
--- a/server/internal/sys/router/auth.go
+++ b/server/internal/sys/router/auth.go
@@ -24,6 +24,9 @@ func InitSysAuthRouter(router *gin.RouterGroup) {
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(),
}