Files
mayfly-go/server/internal/auth/api/oauth2_login.go

228 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"context"
"fmt"
"io"
"mayfly-go/internal/auth/api/vo"
"mayfly-go/internal/auth/application"
"mayfly-go/internal/auth/config"
"mayfly-go/internal/auth/domain/entity"
msgapp "mayfly-go/internal/msg/application"
sysapp "mayfly-go/internal/sys/application"
sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/jsonx"
"mayfly-go/pkg/utils/stringx"
"net/http"
"strconv"
"strings"
"time"
"golang.org/x/oauth2"
)
type Oauth2Login struct {
Oauth2App application.Oauth2
AccountApp sysapp.Account
MsgApp msgapp.Msg
}
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {
client, _ := a.getOAuthClient()
state := stringx.Rand(32)
cache.SetStr("oauth2:state:"+state, "login", 5*time.Minute)
rc.GinCtx.Redirect(http.StatusFound, client.AuthCodeURL(state))
}
func (a *Oauth2Login) OAuth2Bind(rc *req.Ctx) {
client, _ := a.getOAuthClient()
state := stringx.Rand(32)
cache.SetStr("oauth2:state:"+state, "bind:"+strconv.FormatUint(rc.GetLoginAccount().Id, 10),
5*time.Minute)
rc.GinCtx.Redirect(http.StatusFound, client.AuthCodeURL(state))
}
func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
client, oauth := a.getOAuthClient()
code := rc.GinCtx.Query("code")
biz.NotEmpty(code, "code不能为空")
state := rc.GinCtx.Query("state")
biz.NotEmpty(state, "state不能为空")
stateAction := cache.GetStr("oauth2:state:" + state)
biz.NotEmpty(stateAction, "state已过期, 请重新登录")
token, err := client.Exchange(rc.GinCtx, code)
biz.ErrIsNilAppendErr(err, "获取OAuth2 accessToken失败: %s")
// 获取用户信息
httpCli := client.Client(rc.GinCtx.Request.Context(), token)
resp, err := httpCli.Get(oauth.ResourceURL)
biz.ErrIsNilAppendErr(err, "获取用户信息失败: %s")
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
biz.ErrIsNilAppendErr(err, "读取响应的用户信息失败: %s")
// UserIdentifier格式为 type:fieldPath。如string:user.username 或 number:user.id
userIdTypeAndFieldPath := strings.Split(oauth.UserIdentifier, ":")
biz.IsTrue(len(userIdTypeAndFieldPath) == 2, "oauth2配置属性'UserIdentifier'不符合规则")
// 解析用户唯一标识
userIdFieldPath := userIdTypeAndFieldPath[1]
userId := ""
if userIdTypeAndFieldPath[0] == "string" {
userId, err = jsonx.GetStringByBytes(b, userIdFieldPath)
biz.ErrIsNilAppendErr(err, "解析用户唯一标识失败: %s")
} else {
intUserId, err := jsonx.GetIntByBytes(b, userIdFieldPath)
biz.ErrIsNilAppendErr(err, "解析用户唯一标识失败: %s")
userId = fmt.Sprintf("%d", intUserId)
}
biz.NotBlank(userId, "用户唯一标识字段值不能为空")
// 判断是登录还是绑定
if stateAction == "login" {
a.doLoginAction(rc, userId, oauth)
} else if sAccountId, ok := strings.CutPrefix(stateAction, "bind:"); ok {
// 绑定
accountId, err := strconv.ParseUint(sAccountId, 10, 64)
biz.ErrIsNilAppendErr(err, "绑定用户失败: %s")
account := new(sysentity.Account)
account.Id = accountId
err = a.AccountApp.GetBy(account, "username")
biz.ErrIsNilAppendErr(err, "该账号不存在")
rc.ReqParam = collx.Kvs("username", account.Username, "type", "bind")
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
AccountId: accountId,
}, "account_id", "identity")
biz.IsTrue(err != nil, "该账号已被其他用户绑定")
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
Identity: userId,
}, "account_id", "identity")
biz.IsTrue(err != nil, "您已绑定其他账号")
now := time.Now()
err = a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{
AccountId: accountId,
Identity: userId,
CreateTime: &now,
UpdateTime: &now,
})
biz.ErrIsNilAppendErr(err, "绑定用户失败: %s")
res := collx.M{
"action": "oauthBind",
"bind": true,
}
rc.ResData = res
} else {
panic(errorx.NewBiz("state不合法"))
}
}
// 指定登录操作
func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oauth2Login) {
// 查询用户是否存在
oauthAccount := &entity.Oauth2Account{Identity: userId}
err := a.Oauth2App.GetOAuthAccount(oauthAccount, "account_id", "identity")
var accountId uint64
isFirst := false
// 不存在,进行注册
if err != nil {
biz.IsTrue(oauth.AutoRegister, "系统未开启自动注册, 请先让管理员添加对应账号")
now := time.Now()
account := &sysentity.Account{
Model: model.Model{
CreateTime: &now,
CreatorId: 0,
Creator: "oauth2",
UpdateTime: &now,
},
Name: userId,
Username: userId,
}
biz.ErrIsNil(a.AccountApp.Create(context.TODO(), account))
// 绑定
err := a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{
AccountId: account.Id,
Identity: oauthAccount.Identity,
CreateTime: &now,
UpdateTime: &now,
})
biz.ErrIsNilAppendErr(err, "绑定用户失败: %s")
accountId = account.Id
isFirst = true
} else {
accountId = oauthAccount.AccountId
}
// 进行登录
account, err := a.AccountApp.GetById(new(sysentity.Account), accountId, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
biz.ErrIsNilAppendErr(err, "获取用户信息失败: %s")
clientIp := getIpAndRegion(rc)
rc.ReqParam = collx.Kvs("username", account.Username, "ip", clientIp, "type", "login")
res := LastLoginCheck(account, config.GetAccountLoginSecurity(), clientIp)
res["action"] = "oauthLogin"
res["isFirstOauth2Login"] = isFirst
rc.ResData = res
}
func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *config.Oauth2Login) {
oath2LoginConfig := config.GetOauth2Login()
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 + "/#/oauth2/callback",
Scopes: strings.Split(oath2LoginConfig.Scopes, ","),
}
return client, oath2LoginConfig
}
func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
res := &vo.Oauth2Status{}
oauth2LoginConfig := config.GetOauth2Login()
res.Enable = oauth2LoginConfig.Enable
if res.Enable {
err := a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
AccountId: ctx.GetLoginAccount().Id,
}, "account_id", "identity")
res.Bind = err == nil
}
ctx.ResData = res
}
func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
a.Oauth2App.Unbind(rc.GetLoginAccount().Id)
}
// 获取oauth2登录配置信息因为有些字段是敏感字段故单独使用接口获取
func (c *Oauth2Login) Oauth2Config(rc *req.Ctx) {
oauth2LoginConfig := config.GetOauth2Login()
rc.ResData = collx.M{
"enable": oauth2LoginConfig.Enable,
"name": oauth2LoginConfig.Name,
}
}