2023-07-22 20:51:46 +08:00
|
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"mayfly-go/internal/auth/api/vo"
|
|
|
|
|
|
"mayfly-go/internal/auth/application"
|
2023-08-31 21:49:20 +08:00
|
|
|
|
"mayfly-go/internal/auth/config"
|
2023-07-22 20:51:46 +08:00
|
|
|
|
"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"
|
2023-10-26 17:15:49 +08:00
|
|
|
|
"mayfly-go/pkg/errorx"
|
2023-07-22 20:51:46 +08:00
|
|
|
|
"mayfly-go/pkg/model"
|
|
|
|
|
|
"mayfly-go/pkg/req"
|
2023-10-12 12:14:56 +08:00
|
|
|
|
"mayfly-go/pkg/utils/collx"
|
2023-07-22 20:51:46 +08:00
|
|
|
|
"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.LoginAccount.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)
|
2023-07-24 22:36:07 +08:00
|
|
|
|
biz.ErrIsNilAppendErr(err, "获取OAuth2 accessToken失败: %s")
|
2023-07-22 20:51:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取用户信息
|
|
|
|
|
|
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
|
2023-10-26 17:15:49 +08:00
|
|
|
|
err = a.AccountApp.GetBy(account, "username")
|
2023-07-22 20:51:46 +08:00
|
|
|
|
biz.ErrIsNilAppendErr(err, "该账号不存在")
|
2023-10-12 12:14:56 +08:00
|
|
|
|
rc.ReqParam = collx.Kvs("username", account.Username, "type", "bind")
|
2023-07-22 20:51:46 +08:00
|
|
|
|
|
|
|
|
|
|
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
|
|
|
|
|
|
AccountId: accountId,
|
|
|
|
|
|
}, "account_id", "identity")
|
2023-07-24 22:36:07 +08:00
|
|
|
|
biz.IsTrue(err != nil, "该账号已被其他用户绑定")
|
|
|
|
|
|
|
|
|
|
|
|
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
|
|
|
|
|
|
Identity: userId,
|
|
|
|
|
|
}, "account_id", "identity")
|
|
|
|
|
|
biz.IsTrue(err != nil, "您已绑定其他账号")
|
2023-07-22 20:51:46 +08:00
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
err = a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{
|
|
|
|
|
|
AccountId: accountId,
|
|
|
|
|
|
Identity: userId,
|
|
|
|
|
|
CreateTime: &now,
|
|
|
|
|
|
UpdateTime: &now,
|
|
|
|
|
|
})
|
|
|
|
|
|
biz.ErrIsNilAppendErr(err, "绑定用户失败: %s")
|
2023-10-12 12:14:56 +08:00
|
|
|
|
res := collx.M{
|
2023-07-22 20:51:46 +08:00
|
|
|
|
"action": "oauthBind",
|
|
|
|
|
|
"bind": true,
|
|
|
|
|
|
}
|
2023-07-24 22:36:07 +08:00
|
|
|
|
rc.ResData = res
|
2023-07-22 20:51:46 +08:00
|
|
|
|
} else {
|
2023-10-26 17:15:49 +08:00
|
|
|
|
panic(errorx.NewBiz("state不合法"))
|
2023-07-22 20:51:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 指定登录操作
|
2023-08-31 21:49:20 +08:00
|
|
|
|
func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oauth2Login) {
|
2023-07-22 20:51:46 +08:00
|
|
|
|
// 查询用户是否存在
|
|
|
|
|
|
oauthAccount := &entity.Oauth2Account{Identity: userId}
|
|
|
|
|
|
err := a.Oauth2App.GetOAuthAccount(oauthAccount, "account_id", "identity")
|
|
|
|
|
|
|
|
|
|
|
|
var accountId uint64
|
2023-07-24 22:36:07 +08:00
|
|
|
|
isFirst := false
|
2023-07-22 20:51:46 +08:00
|
|
|
|
// 不存在,进行注册
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
2023-10-26 17:15:49 +08:00
|
|
|
|
biz.ErrIsNil(a.AccountApp.Create(account))
|
2023-07-22 20:51:46 +08:00
|
|
|
|
// 绑定
|
|
|
|
|
|
err := a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{
|
|
|
|
|
|
AccountId: account.Id,
|
|
|
|
|
|
Identity: oauthAccount.Identity,
|
|
|
|
|
|
CreateTime: &now,
|
|
|
|
|
|
UpdateTime: &now,
|
|
|
|
|
|
})
|
|
|
|
|
|
biz.ErrIsNilAppendErr(err, "绑定用户失败: %s")
|
|
|
|
|
|
accountId = account.Id
|
2023-07-24 22:36:07 +08:00
|
|
|
|
isFirst = true
|
2023-07-22 20:51:46 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
accountId = oauthAccount.AccountId
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 进行登录
|
|
|
|
|
|
account := &sysentity.Account{
|
|
|
|
|
|
Model: model.Model{DeletedModel: model.DeletedModel{Id: accountId}},
|
|
|
|
|
|
}
|
2023-10-26 17:15:49 +08:00
|
|
|
|
err = a.AccountApp.GetBy(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
|
2023-07-22 20:51:46 +08:00
|
|
|
|
biz.ErrIsNilAppendErr(err, "获取用户信息失败: %s")
|
|
|
|
|
|
|
2023-07-24 22:36:07 +08:00
|
|
|
|
clientIp := getIpAndRegion(rc)
|
2023-10-12 12:14:56 +08:00
|
|
|
|
rc.ReqParam = collx.Kvs("username", account.Username, "ip", clientIp, "type", "login")
|
2023-07-24 22:36:07 +08:00
|
|
|
|
|
2023-08-31 21:49:20 +08:00
|
|
|
|
res := LastLoginCheck(account, config.GetAccountLoginSecurity(), clientIp)
|
2023-07-22 20:51:46 +08:00
|
|
|
|
res["action"] = "oauthLogin"
|
2023-07-24 22:36:07 +08:00
|
|
|
|
res["isFirstOauth2Login"] = isFirst
|
|
|
|
|
|
rc.ResData = res
|
2023-07-22 20:51:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-31 21:49:20 +08:00
|
|
|
|
func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *config.Oauth2Login) {
|
|
|
|
|
|
oath2LoginConfig := config.GetOauth2Login()
|
2023-07-22 20:51:46 +08:00
|
|
|
|
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,
|
|
|
|
|
|
},
|
2023-07-24 22:36:07 +08:00
|
|
|
|
RedirectURL: oath2LoginConfig.RedirectURL + "/#/oauth2/callback",
|
2023-07-22 20:51:46 +08:00
|
|
|
|
Scopes: strings.Split(oath2LoginConfig.Scopes, ","),
|
|
|
|
|
|
}
|
|
|
|
|
|
return client, oath2LoginConfig
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
|
|
|
|
|
|
res := &vo.Oauth2Status{}
|
2023-08-31 21:49:20 +08:00
|
|
|
|
oauth2LoginConfig := config.GetOauth2Login()
|
2023-07-22 20:51:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2023-07-24 22:36:07 +08:00
|
|
|
|
|
|
|
|
|
|
func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
|
|
|
|
|
|
a.Oauth2App.Unbind(rc.LoginAccount.Id)
|
|
|
|
|
|
}
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取oauth2登录配置信息,因为有些字段是敏感字段,故单独使用接口获取
|
|
|
|
|
|
func (c *Oauth2Login) Oauth2Config(rc *req.Ctx) {
|
|
|
|
|
|
oauth2LoginConfig := config.GetOauth2Login()
|
2023-10-12 12:14:56 +08:00
|
|
|
|
rc.ResData = collx.M{
|
2023-08-31 21:49:20 +08:00
|
|
|
|
"enable": oauth2LoginConfig.Enable,
|
|
|
|
|
|
"name": oauth2LoginConfig.Name,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|