mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
feat: OAuth2 登录
This commit is contained in:
@@ -3,5 +3,6 @@ import Api from '@/common/Api';
|
|||||||
export const personApi = {
|
export const personApi = {
|
||||||
accountInfo: Api.newGet('/sys/accounts/self'),
|
accountInfo: Api.newGet('/sys/accounts/self'),
|
||||||
updateAccount: Api.newPut('/sys/accounts/self'),
|
updateAccount: Api.newPut('/sys/accounts/self'),
|
||||||
|
authStatus: Api.newGet('/sys/auth/status'),
|
||||||
getMsgs: Api.newGet('/msgs/self'),
|
getMsgs: Api.newGet('/msgs/self'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -155,14 +155,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="personal-edit-safe-box">
|
<div v-show="authStatus.enable.oauth2" class="personal-edit-safe-box">
|
||||||
<div class="personal-edit-safe-item">
|
<div class="personal-edit-safe-item">
|
||||||
<div class="personal-edit-safe-item-left">
|
<div class="personal-edit-safe-item-left">
|
||||||
<div class="personal-edit-safe-item-left-label">绑定OAuth2</div>
|
<div class="personal-edit-safe-item-left-label">绑定OAuth2</div>
|
||||||
<div class="personal-edit-safe-item-left-value">已绑定OAuth2</div>
|
<div class="personal-edit-safe-item-left-value">
|
||||||
|
{{ authStatus.bind.oauth2 ? '已绑定' : '未绑定' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="personal-edit-safe-item-right">
|
<div class="personal-edit-safe-item-right">
|
||||||
<el-button type="text">立即绑定</el-button>
|
<el-button type="text" @click="bindOAuth2" :disabled="authStatus.bind">立即绑定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,6 +182,8 @@ import { personApi } from './api';
|
|||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useUserInfo } from '@/store/userInfo';
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
|
import config from '@/common/config';
|
||||||
|
import { getSession } from '@/common/utils/storage';
|
||||||
|
|
||||||
const { userInfo } = storeToRefs(useUserInfo());
|
const { userInfo } = storeToRefs(useUserInfo());
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -202,9 +206,13 @@ const state = reactive({
|
|||||||
accountForm: {
|
accountForm: {
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
|
authStatus: {
|
||||||
|
enable: { oauth2: false },
|
||||||
|
bind: { oauth2: false }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { msgDialog, accountForm } = toRefs(state);
|
const { msgDialog, accountForm, authStatus } = toRefs(state);
|
||||||
|
|
||||||
// 当前时间提示语
|
// 当前时间提示语
|
||||||
const currentTime = computed(() => {
|
const currentTime = computed(() => {
|
||||||
@@ -222,9 +230,10 @@ const roleInfo = computed(() => {
|
|||||||
return state.accountInfo.roles.map((val: any) => val.name).join('、');
|
return state.accountInfo.roles.map((val: any) => val.name).join('、');
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
getAccountInfo();
|
getAccountInfo();
|
||||||
getMsgs();
|
getMsgs();
|
||||||
|
state.authStatus = await personApi.authStatus.request()
|
||||||
});
|
});
|
||||||
|
|
||||||
const getAccountInfo = async () => {
|
const getAccountInfo = async () => {
|
||||||
@@ -236,6 +245,31 @@ const updateAccount = async () => {
|
|||||||
ElMessage.success('更新成功');
|
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 getMsgs = async () => {
|
||||||
const res = await personApi.getMsgs.request(state.msgDialog.query);
|
const res = await personApi.getMsgs.request(state.msgDialog.query);
|
||||||
state.msgDialog.msgs = res;
|
state.msgDialog.msgs = res;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
"mayfly-go/pkg/utils"
|
"mayfly-go/pkg/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -70,7 +71,8 @@ func (a *Auth) OAuth2Callback(rc *req.Ctx) {
|
|||||||
if state == "" {
|
if state == "" {
|
||||||
biz.ErrIsNil(errors.New("state不能为空"), "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已过期,请重新登录")
|
biz.ErrIsNil(errors.New("state已过期,请重新登录"), "state已过期,请重新登录")
|
||||||
}
|
}
|
||||||
token, err := client.Exchange(rc.GinCtx, code)
|
token, err := client.Exchange(rc.GinCtx, code)
|
||||||
@@ -115,86 +117,117 @@ func (a *Auth) OAuth2Callback(rc *req.Ctx) {
|
|||||||
// 查询用户是否存在
|
// 查询用户是否存在
|
||||||
oauthAccount := &entity.OAuthAccount{Identity: userId}
|
oauthAccount := &entity.OAuthAccount{Identity: userId}
|
||||||
err = a.AuthApp.GetOAuthAccount(oauthAccount, "account_id", "identity")
|
err = a.AuthApp.GetOAuthAccount(oauthAccount, "account_id", "identity")
|
||||||
var accountId uint64
|
// 判断是登录还是绑定
|
||||||
if err != nil {
|
if stateAction == "login" {
|
||||||
if err != gorm.ErrRecordNotFound {
|
var accountId uint64
|
||||||
biz.ErrIsNil(err, "查询用户失败: "+err.Error())
|
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 {
|
account := &entity.Account{
|
||||||
biz.ErrIsNil(errors.New("用户不存在,请先注册"), "用户不存在,请先注册")
|
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("<html>" +
|
||||||
|
"<script>top.opener.postMessage(" + string(b) + ")</script>" +
|
||||||
|
"</html>")
|
||||||
|
} 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()
|
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{
|
if err := a.AuthApp.BindOAuthAccount(&entity.OAuthAccount{
|
||||||
AccountId: account.Id,
|
AccountId: accountId,
|
||||||
Identity: oauthAccount.Identity,
|
Identity: oauthAccount.Identity,
|
||||||
CreateTime: &now,
|
CreateTime: &now,
|
||||||
UpdateTime: &now,
|
UpdateTime: &now,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
biz.ErrIsNil(err, "绑定用户失败: "+err.Error())
|
biz.ErrIsNil(err, "绑定用户失败: "+err.Error())
|
||||||
}
|
}
|
||||||
accountId = account.Id
|
res := map[string]any{
|
||||||
} else {
|
"action": "oauthBind",
|
||||||
accountId = oauthAccount.AccountId
|
"bind": true,
|
||||||
}
|
|
||||||
// 进行登录
|
|
||||||
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
|
b, err = json.Marshal(res)
|
||||||
|
biz.ErrIsNil(err, "数据序列化失败")
|
||||||
|
rc.GinCtx.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
rc.GinCtx.Writer.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = rc.GinCtx.Writer.WriteString("<html>" +
|
||||||
|
"<script>top.opener.postMessage(" + string(b) + ")</script>" +
|
||||||
|
"</html>")
|
||||||
} else {
|
} else {
|
||||||
// 保存登录消息
|
biz.ErrIsNil(errors.New("state不合法"), "state不合法")
|
||||||
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("<html>" +
|
|
||||||
"<script>top.opener.postMessage(" + string(b) + ")</script>" +
|
|
||||||
"</html>")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) getOAuthClient() (*oauth2.Config, *vo.OAuth2VO, error) {
|
func (a *Auth) getOAuthClient() (*oauth2.Config, *vo.OAuth2VO, error) {
|
||||||
@@ -265,3 +298,42 @@ func (a *Auth) SaveOAuth2(rc *req.Ctx) {
|
|||||||
config.Remark = AuthOAuth2Remark
|
config.Remark = AuthOAuth2Remark
|
||||||
a.ConfigApp.Save(config)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (c *Config) SaveConfig(rc *req.Ctx) {
|
|||||||
|
|
||||||
// AuthConfig auth相关配置
|
// AuthConfig auth相关配置
|
||||||
func (c *Config) AuthConfig(rc *req.Ctx) {
|
func (c *Config) AuthConfig(rc *req.Ctx) {
|
||||||
resp := &vo.OAuth2EnableVO{}
|
resp := &vo.Auth2EnableVO{}
|
||||||
config := c.ConfigApp.GetConfig(AuthOAuth2Key)
|
config := c.ConfigApp.GetConfig(AuthOAuth2Key)
|
||||||
oauth2 := &vo.OAuth2VO{}
|
oauth2 := &vo.OAuth2VO{}
|
||||||
if config.Value != "" {
|
if config.Value != "" {
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type AuthVO struct {
|
|||||||
*OAuth2VO `json:"oauth2"`
|
*OAuth2VO `json:"oauth2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuth2EnableVO struct {
|
type Auth2EnableVO struct {
|
||||||
OAuth2 bool `json:"oauth2"`
|
OAuth2 bool `json:"oauth2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthStatusVO struct {
|
||||||
|
Enable Auth2EnableVO `json:"enable"`
|
||||||
|
Bind Auth2EnableVO `json:"bind"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ func InitSysAuthRouter(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
req.NewPut("/oauth2", r.SaveOAuth2).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/login", r.OAuth2Login).DontNeedToken(),
|
||||||
req.NewGet("/oauth2/callback", r.OAuth2Callback).NoRes().DontNeedToken(),
|
req.NewGet("/oauth2/callback", r.OAuth2Callback).NoRes().DontNeedToken(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user