重构认证授权模块,统一到 auth 包下
This commit is contained in:
@@ -2,7 +2,7 @@ name: Docker
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ["v*"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
@@ -10,14 +10,12 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
156
web/auth/account.go
Normal file
156
web/auth/account.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"platform/pkg/u"
|
||||||
|
"platform/web/core"
|
||||||
|
m "platform/web/models"
|
||||||
|
q "platform/web/queries"
|
||||||
|
s "platform/web/services"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authClient(clientId string, clientSecrets ...string) (*m.Client, error) {
|
||||||
|
|
||||||
|
// 获取客户端信息
|
||||||
|
client, err := q.Client.
|
||||||
|
Where(
|
||||||
|
q.Client.ClientID.Eq(clientId),
|
||||||
|
q.Client.Status.Eq(1)).
|
||||||
|
Take()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查客户端密钥
|
||||||
|
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
|
||||||
|
if len(clientSecrets) == 0 {
|
||||||
|
return nil, errors.New("客户端密钥错误")
|
||||||
|
}
|
||||||
|
clientSecret := clientSecrets[0]
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
|
||||||
|
return nil, errors.New("客户端密钥错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo 查询客户端关联权限
|
||||||
|
|
||||||
|
// 组织授权信息(一次性请求)
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUser(loginType PwdLoginType, username, password string) (user *m.User, err error) {
|
||||||
|
switch loginType {
|
||||||
|
case PwdLoginByPhone:
|
||||||
|
user, err = authUserBySms(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
user = &m.User{
|
||||||
|
Phone: username,
|
||||||
|
Username: u.P(username),
|
||||||
|
Status: m.UserStatusEnabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PwdLoginByEmail:
|
||||||
|
user, err = authUserByEmail(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case PwdLoginByPassword:
|
||||||
|
user, err = authUserByPassword(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, ErrAuthorizeInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账户状态
|
||||||
|
if user.Status == m.UserStatusDisabled {
|
||||||
|
return nil, core.NewBizErr("账号已禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
|
||||||
|
// 验证验证码
|
||||||
|
err := s.Verifier.VerifySms(context.Background(), username, code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewBizErr("短信认证失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
|
||||||
|
return nil, core.NewServErr("邮箱登录不可用")
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
|
||||||
|
user, err := tx.User.
|
||||||
|
Where(tx.User.Phone.Eq(username)).
|
||||||
|
Or(tx.User.Email.Eq(username)).
|
||||||
|
Or(tx.User.Username.Eq(username)).
|
||||||
|
Take()
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("查找用户失败", "error", err)
|
||||||
|
return nil, core.NewBizErr("用户不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if user.Password == nil || *user.Password == "" {
|
||||||
|
slog.Debug("用户未设置密码", "username", username)
|
||||||
|
return nil, core.NewBizErr("用户不存在或密码错误")
|
||||||
|
}
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
|
||||||
|
slog.Debug("密码验证失败", "username", username)
|
||||||
|
return nil, core.NewBizErr("用户不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authAdmin(loginType PwdLoginType, username, password string) (admin *m.Admin, err error) {
|
||||||
|
switch loginType {
|
||||||
|
case PwdLoginByPhone, PwdLoginByEmail:
|
||||||
|
return nil, core.NewServErr("不支持的登录方式:" + string(loginType))
|
||||||
|
case PwdLoginByPassword:
|
||||||
|
admin, err = authAdminByPassword(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, ErrAuthorizeInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账户状态
|
||||||
|
if admin.Status == m.AdminStatusDisabled {
|
||||||
|
return nil, core.NewBizErr("账号已禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authAdminByPassword(tx *q.Query, username, password string) (*m.Admin, error) {
|
||||||
|
admin, err := tx.Admin.Where(tx.Admin.Username.Eq(username)).Take()
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewBizErr("账号不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if admin.Password == "" {
|
||||||
|
return nil, core.NewBizErr("账号不存在或密码错误")
|
||||||
|
}
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password)) != nil {
|
||||||
|
return nil, core.NewBizErr("账号不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin, nil
|
||||||
|
}
|
||||||
@@ -6,10 +6,8 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
|
||||||
"platform/pkg/env"
|
"platform/pkg/env"
|
||||||
"platform/pkg/u"
|
"platform/pkg/u"
|
||||||
"platform/web/core"
|
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
"platform/web/globals/orm"
|
"platform/web/globals/orm"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
@@ -22,67 +20,52 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GrantType string
|
// AuthorizeGet 授权端点
|
||||||
|
func AuthorizeGet(ctx *fiber.Ctx) error {
|
||||||
|
|
||||||
const (
|
// 检查请求
|
||||||
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
|
req := new(AuthorizeGetReq)
|
||||||
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
|
if err := g.Validator.ParseQuery(ctx, req); err != nil {
|
||||||
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
|
return err
|
||||||
GrantPassword = GrantType("password") // 密码模式(私有扩展)
|
|
||||||
)
|
|
||||||
|
|
||||||
type PasswordGrantType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
GrantPasswordSecret = PasswordGrantType("password") // 账号密码
|
|
||||||
GrantPasswordPhone = PasswordGrantType("phone_code") // 手机验证码
|
|
||||||
GrantPasswordEmail = PasswordGrantType("email_code") // 邮箱验证码
|
|
||||||
)
|
|
||||||
|
|
||||||
type TokenReq struct {
|
|
||||||
GrantType GrantType `json:"grant_type" form:"grant_type"`
|
|
||||||
ClientID string `json:"client_id" form:"client_id"`
|
|
||||||
ClientSecret string `json:"client_secret" form:"client_secret"`
|
|
||||||
Scope string `json:"scope" form:"scope"`
|
|
||||||
GrantCodeData
|
|
||||||
GrantClientData
|
|
||||||
GrantRefreshData
|
|
||||||
GrantPasswordData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantCodeData struct {
|
// 检查客户端
|
||||||
Code string `json:"code" form:"code"`
|
client, err := authClient(req.ClientID)
|
||||||
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
|
if err != nil {
|
||||||
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantClientData struct {
|
if client.RedirectURI == nil || *client.RedirectURI != req.RedirectURI {
|
||||||
|
return errors.New("客户端重定向URI错误")
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantRefreshData struct {
|
// todo 检查 scope
|
||||||
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
|
||||||
|
// 授权确认页面
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantPasswordData struct {
|
type AuthorizeGetReq struct {
|
||||||
LoginType PasswordGrantType `json:"login_type" form:"login_type"`
|
ResponseType string `json:"response_type" validate:"eq=code"`
|
||||||
Username string `json:"username" form:"username"`
|
ClientID string `json:"client_id" validate:"required"`
|
||||||
Password string `json:"password" form:"password"`
|
RedirectURI string `json:"redirect_uri" validate:"required"`
|
||||||
Remember bool `json:"remember" form:"remember"`
|
Scope string `json:"scope"`
|
||||||
|
State string `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenResp struct {
|
func AuthorizePost(ctx *fiber.Ctx) error {
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token,omitempty"`
|
// todo 解析用户授权的范围
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
TokenType string `json:"token_type"`
|
return nil
|
||||||
Scope string `json:"scope,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenErrResp struct {
|
type AuthorizePostReq struct {
|
||||||
Error string `json:"error"`
|
Accept bool `json:"accept"`
|
||||||
Description string `json:"error_description,omitempty"`
|
Scope string `json:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Token 令牌端点
|
||||||
func Token(c *fiber.Ctx) error {
|
func Token(c *fiber.Ctx) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -165,6 +148,75 @@ func Token(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenReq struct {
|
||||||
|
GrantType GrantType `json:"grant_type" form:"grant_type"`
|
||||||
|
ClientID string `json:"client_id" form:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret" form:"client_secret"`
|
||||||
|
Scope string `json:"scope" form:"scope"`
|
||||||
|
GrantCodeData
|
||||||
|
GrantClientData
|
||||||
|
GrantRefreshData
|
||||||
|
GrantPasswordData
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantCodeData struct {
|
||||||
|
Code string `json:"code" form:"code"`
|
||||||
|
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
|
||||||
|
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantClientData struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantRefreshData struct {
|
||||||
|
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantPasswordData struct {
|
||||||
|
LoginType PwdLoginType `json:"login_type" form:"login_type"`
|
||||||
|
LoginPool PwdLoginPool `json:"login_pool" form:"login_pool"`
|
||||||
|
Username string `json:"username" form:"username"`
|
||||||
|
Password string `json:"password" form:"password"`
|
||||||
|
Remember bool `json:"remember" form:"remember"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
|
||||||
|
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
|
||||||
|
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
|
||||||
|
GrantPassword = GrantType("password") // 密码模式(私有扩展)
|
||||||
|
)
|
||||||
|
|
||||||
|
type PwdLoginType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PwdLoginByPassword = PwdLoginType("password") // 账号密码
|
||||||
|
PwdLoginByPhone = PwdLoginType("phone_code") // 手机验证码
|
||||||
|
PwdLoginByEmail = PwdLoginType("email_code") // 邮箱验证码
|
||||||
|
)
|
||||||
|
|
||||||
|
type PwdLoginPool string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PwdLoginAsUser = PwdLoginPool("user") // 用户池
|
||||||
|
PwdLoginAsAdmin = PwdLoginPool("admin") // 管理员池
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenResp struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenErrResp struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Description string `json:"error_description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
|
func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
|
||||||
|
|
||||||
// 检查 code 获取用户授权信息
|
// 检查 code 获取用户授权信息
|
||||||
@@ -226,7 +278,7 @@ func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.
|
|||||||
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SaveSession(session)
|
err = SaveSession(q.Q, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -249,7 +301,7 @@ func authClientCredential(c *fiber.Ctx, auth *AuthCtx, _ *TokenReq, now time.Tim
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保存会话
|
// 保存会话
|
||||||
err := SaveSession(session)
|
err := SaveSession(q.Q, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -261,71 +313,85 @@ func authPassword(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m
|
|||||||
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
|
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
|
||||||
ua := u.X(c.Get(fiber.HeaderUserAgent))
|
ua := u.X(c.Get(fiber.HeaderUserAgent))
|
||||||
|
|
||||||
|
// 分池认证
|
||||||
|
var err error
|
||||||
var user *m.User
|
var user *m.User
|
||||||
err := q.Q.Transaction(func(tx *q.Query) (err error) {
|
var admin *m.Admin
|
||||||
switch req.LoginType {
|
|
||||||
case GrantPasswordPhone:
|
pool := req.LoginPool
|
||||||
user, err = authUserBySms(tx, req.Username, req.Password)
|
if pool == "" {
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
pool = PwdLoginAsUser
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if user == nil {
|
switch pool {
|
||||||
|
case PwdLoginAsUser:
|
||||||
|
user, err = authUser(req.LoginType, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
if req.LoginType != PwdLoginByPhone || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号首次登录的自动创建用户
|
||||||
user = &m.User{
|
user = &m.User{
|
||||||
Phone: req.Username,
|
Phone: req.Username,
|
||||||
Username: u.P(req.Username),
|
Username: u.P(req.Username),
|
||||||
Status: m.UserStatusEnabled,
|
Status: m.UserStatusEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case GrantPasswordEmail:
|
|
||||||
user, err = authUserByEmail(tx, req.Username, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case GrantPasswordSecret:
|
|
||||||
user, err = authUserByPassword(tx, req.Username, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return ErrAuthorizeInvalidRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
// 账户状态
|
|
||||||
if user.Status == m.UserStatusDisabled {
|
|
||||||
slog.Debug("账户状态异常", "username", req.Username, "status", user.Status)
|
|
||||||
return core.NewBizErr("账号无法登录")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户的登录时间
|
// 更新用户的登录时间
|
||||||
user.LastLogin = u.P(time.Now())
|
user.LastLogin = u.P(time.Now())
|
||||||
user.LastLoginIP = ip
|
user.LastLoginIP = ip
|
||||||
user.LastLoginUA = ua
|
user.LastLoginUA = ua
|
||||||
if err := tx.User.Save(user); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
case PwdLoginAsAdmin:
|
||||||
})
|
admin, err = authAdmin(req.LoginType, req.Username, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新管理员登录时间
|
||||||
|
admin.LastLogin = u.P(time.Now())
|
||||||
|
admin.LastLoginIP = ip
|
||||||
|
admin.LastLoginUA = ua
|
||||||
|
}
|
||||||
|
|
||||||
// 生成会话
|
// 生成会话
|
||||||
session := &m.Session{
|
session := &m.Session{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
UA: ua,
|
UA: ua,
|
||||||
UserID: &user.ID,
|
|
||||||
ClientID: &auth.Client.ID,
|
ClientID: &auth.Client.ID,
|
||||||
Scopes: u.X(req.Scope),
|
Scopes: u.X(req.Scope),
|
||||||
AccessToken: uuid.NewString(),
|
AccessToken: uuid.NewString(),
|
||||||
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
|
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
|
||||||
}
|
}
|
||||||
|
if user != nil {
|
||||||
|
session.UserID = &user.ID
|
||||||
|
}
|
||||||
|
if admin != nil {
|
||||||
|
session.AdminID = &admin.ID
|
||||||
|
}
|
||||||
if req.Remember {
|
if req.Remember {
|
||||||
session.RefreshToken = u.P(uuid.NewString())
|
session.RefreshToken = u.P(uuid.NewString())
|
||||||
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SaveSession(session)
|
// 保存用户更新和会话
|
||||||
|
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||||
|
if err := SaveSession(tx, session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if user != nil {
|
||||||
|
if err := tx.User.Save(user); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if admin != nil {
|
||||||
|
if err := tx.Admin.Save(admin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -353,7 +419,7 @@ func authRefreshToken(_ *fiber.Ctx, _ *AuthCtx, req *TokenReq, now time.Time) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保存令牌
|
// 保存令牌
|
||||||
err = SaveSession(session)
|
err = SaveSession(q.Q, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -394,14 +460,87 @@ func sendError(c *fiber.Ctx, err error, description ...string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Revoke() error {
|
// Revoke 令牌撤销端点
|
||||||
|
func Revoke(ctx *fiber.Ctx) error {
|
||||||
|
_, err := GetAuthCtx(ctx).PermitUser()
|
||||||
|
if err != nil {
|
||||||
|
// 用户未登录
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Introspect() error {
|
// 解析请求参数
|
||||||
|
req := new(RevokeReq)
|
||||||
|
if err := ctx.BodyParser(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会话
|
||||||
|
err = RemoveSession(ctx.Context(), req.AccessToken, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RevokeReq struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Introspect 令牌检查端点
|
||||||
|
func Introspect(ctx *fiber.Ctx) error {
|
||||||
|
// 验证权限
|
||||||
|
authCtx, err := GetAuthCtx(ctx).PermitUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
profile, err := q.User.
|
||||||
|
Where(q.User.ID.Eq(authCtx.User.ID)).
|
||||||
|
Omit(q.User.DeletedAt).
|
||||||
|
Take()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否设置了密码
|
||||||
|
hasPassword := false
|
||||||
|
if profile.Password != nil && *profile.Password != "" {
|
||||||
|
hasPassword = true
|
||||||
|
profile.Password = nil // 不返回密码
|
||||||
|
}
|
||||||
|
|
||||||
|
// 掩码敏感信息
|
||||||
|
if profile.Phone != "" {
|
||||||
|
profile.Phone = maskPhone(profile.Phone)
|
||||||
|
}
|
||||||
|
if profile.IDNo != nil && *profile.IDNo != "" {
|
||||||
|
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
|
||||||
|
}
|
||||||
|
return ctx.JSON(IntrospectResp{*profile, hasPassword})
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntrospectResp struct {
|
||||||
|
m.User
|
||||||
|
HasPassword bool `json:"has_password"` // 是否设置了密码
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskPhone(phone string) string {
|
||||||
|
if len(phone) < 11 {
|
||||||
|
return phone
|
||||||
|
}
|
||||||
|
return phone[:3] + "****" + phone[7:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskIdNo(idNo string) string {
|
||||||
|
if len(idNo) < 18 {
|
||||||
|
return idNo
|
||||||
|
}
|
||||||
|
return idNo[:3] + "*********" + idNo[14:]
|
||||||
|
}
|
||||||
|
|
||||||
type CodeContext struct {
|
type CodeContext struct {
|
||||||
UserID int32 `json:"user_id"`
|
UserID int32 `json:"user_id"`
|
||||||
ClientID int32 `json:"client_id"`
|
ClientID int32 `json:"client_id"`
|
||||||
@@ -6,15 +6,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"platform/web/core"
|
|
||||||
m "platform/web/models"
|
|
||||||
q "platform/web/queries"
|
|
||||||
s "platform/web/services"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Authenticate() fiber.Handler {
|
func Authenticate() fiber.Handler {
|
||||||
@@ -123,67 +118,3 @@ func authBasic(_ context.Context, token string) (*AuthCtx, error) {
|
|||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authClient(clientId, clientSecret string) (*m.Client, error) {
|
|
||||||
|
|
||||||
// 获取客户端信息
|
|
||||||
client, err := q.Client.
|
|
||||||
Where(
|
|
||||||
q.Client.ClientID.Eq(clientId),
|
|
||||||
q.Client.Status.Eq(1)).
|
|
||||||
Take()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查客户端密钥
|
|
||||||
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
|
|
||||||
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
|
|
||||||
return nil, errors.New("客户端密钥错误")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo 查询客户端关联权限
|
|
||||||
|
|
||||||
// 组织授权信息(一次性请求)
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
|
|
||||||
// 验证验证码
|
|
||||||
err := s.Verifier.VerifySms(context.Background(), username, code)
|
|
||||||
if err != nil {
|
|
||||||
return nil, core.NewBizErr("短信认证失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找用户
|
|
||||||
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
|
|
||||||
}
|
|
||||||
|
|
||||||
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
|
|
||||||
return nil, core.NewServErr("邮箱登录不可用")
|
|
||||||
}
|
|
||||||
|
|
||||||
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
|
|
||||||
user, err := tx.User.
|
|
||||||
Where(tx.User.Phone.Eq(username)).
|
|
||||||
Or(tx.User.Email.Eq(username)).
|
|
||||||
Or(tx.User.Username.Eq(username)).
|
|
||||||
Take()
|
|
||||||
if err != nil {
|
|
||||||
slog.Debug("查找用户失败", "error", err)
|
|
||||||
return nil, core.NewBizErr("用户不存在或密码错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证密码
|
|
||||||
if user.Password == nil || *user.Password == "" {
|
|
||||||
slog.Debug("用户未设置密码", "username", username)
|
|
||||||
return nil, core.NewBizErr("用户不存在或密码错误")
|
|
||||||
}
|
|
||||||
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
|
|
||||||
slog.Debug("密码验证失败", "username", username)
|
|
||||||
return nil, core.NewBizErr("用户不存在或密码错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
@@ -29,8 +29,8 @@ func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error
|
|||||||
).First()
|
).First()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveSession(session *m.Session) error {
|
func SaveSession(tx *q.Query, session *m.Session) error {
|
||||||
return q.Session.Save(session)
|
return tx.Session.Save(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {
|
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"platform/pkg/u"
|
|
||||||
auth2 "platform/web/auth"
|
|
||||||
m "platform/web/models"
|
|
||||||
q "platform/web/queries"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// region /revoke
|
|
||||||
|
|
||||||
type RevokeReq struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Revoke(c *fiber.Ctx) error {
|
|
||||||
_, err := auth2.GetAuthCtx(c).PermitUser()
|
|
||||||
if err != nil {
|
|
||||||
// 用户未登录
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析请求参数
|
|
||||||
req := new(RevokeReq)
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除会话
|
|
||||||
err = auth2.RemoveSession(c.Context(), req.AccessToken, req.RefreshToken)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region /profile
|
|
||||||
|
|
||||||
type IntrospectResp struct {
|
|
||||||
m.User
|
|
||||||
HasPassword bool `json:"has_password"` // 是否设置了密码
|
|
||||||
}
|
|
||||||
|
|
||||||
func Introspect(c *fiber.Ctx) error {
|
|
||||||
// 验证权限
|
|
||||||
authCtx, err := auth2.GetAuthCtx(c).PermitUser()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
profile, err := q.User.
|
|
||||||
Where(q.User.ID.Eq(authCtx.User.ID)).
|
|
||||||
Omit(q.User.DeletedAt).
|
|
||||||
Take()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户是否设置了密码
|
|
||||||
hasPassword := false
|
|
||||||
if profile.Password != nil && *profile.Password != "" {
|
|
||||||
hasPassword = true
|
|
||||||
profile.Password = nil // 不返回密码
|
|
||||||
}
|
|
||||||
|
|
||||||
// 掩码敏感信息
|
|
||||||
if profile.Phone != "" {
|
|
||||||
profile.Phone = maskPhone(profile.Phone)
|
|
||||||
}
|
|
||||||
if profile.IDNo != nil && *profile.IDNo != "" {
|
|
||||||
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
|
|
||||||
}
|
|
||||||
return c.JSON(IntrospectResp{*profile, hasPassword})
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskPhone(phone string) string {
|
|
||||||
if len(phone) < 11 {
|
|
||||||
return phone
|
|
||||||
}
|
|
||||||
return phone[:3] + "****" + phone[7:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskIdNo(idNo string) string {
|
|
||||||
if len(idNo) < 18 {
|
|
||||||
return idNo
|
|
||||||
}
|
|
||||||
return idNo[:3] + "*********" + idNo[14:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/contrib/otelfiber/v2"
|
"github.com/gofiber/contrib/otelfiber/v2"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||||
@@ -19,6 +20,14 @@ func ApplyMiddlewares(app *fiber.App) {
|
|||||||
EnableStackTrace: true,
|
EnableStackTrace: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// cors
|
||||||
|
app.Use(cors.New(cors.Config{
|
||||||
|
AllowCredentials: true,
|
||||||
|
AllowOriginsFunc: func(origin string) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
// logger
|
// logger
|
||||||
app.Use(logger.New(logger.Config{
|
app.Use(logger.New(logger.Config{
|
||||||
Next: func(c *fiber.Ctx) bool {
|
Next: func(c *fiber.Ctx) bool {
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ func ApplyRouters(app *fiber.App) {
|
|||||||
|
|
||||||
// 认证
|
// 认证
|
||||||
auth := api.Group("/auth")
|
auth := api.Group("/auth")
|
||||||
|
auth.Get("/authorize", auth2.AuthorizeGet)
|
||||||
|
auth.Post("/authorize", auth2.AuthorizePost)
|
||||||
auth.Post("/token", auth2.Token)
|
auth.Post("/token", auth2.Token)
|
||||||
auth.Post("/revoke", handlers.Revoke)
|
auth.Post("/revoke", auth2.Revoke)
|
||||||
auth.Post("/introspect", handlers.Introspect)
|
auth.Post("/introspect", auth2.Introspect)
|
||||||
auth.Post("/verify/sms", handlers.SmsCode)
|
auth.Post("/verify/sms", handlers.SmsCode)
|
||||||
|
|
||||||
// 用户
|
// 用户
|
||||||
|
|||||||
Reference in New Issue
Block a user