认证授权主要流程实现

This commit is contained in:
2025-03-18 17:57:07 +08:00
parent 19530d9d40
commit 6ddf1118a5
37 changed files with 2209 additions and 180 deletions

52
web/handlers/client.go Normal file
View File

@@ -0,0 +1,52 @@
package handlers
import (
"platform/web/models"
q "platform/web/queries"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
)
type CreateClientReq struct {
ClientID string `query:"client_id"`
ClientSecret string `query:"client_secret"`
}
func CreateClient(c *fiber.Ctx) error {
// 验证请求参数
req := new(CreateClientReq)
if err := c.QueryParser(req); err != nil {
return err
}
if req.ClientID == "" {
return fiber.NewError(fiber.StatusBadRequest, "client_id不能为空")
}
if req.ClientSecret == "" {
return fiber.NewError(fiber.StatusBadRequest, "client_secret不能为空")
}
// 创建客户端
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(req.ClientSecret), bcrypt.DefaultCost)
if err != nil {
return err
}
client := &models.Client{
ClientID: req.ClientID,
ClientSecret: string(hashedSecret),
Name: "默认客户端 - " + time.Now().String(),
Spec: 0,
GrantCode: true,
GrantClient: true,
GrantRefresh: true,
Version: 0,
}
err = q.Client.Create(client)
if err != nil {
return err
}
return c.JSON(client)
}

107
web/handlers/login.go Normal file
View File

@@ -0,0 +1,107 @@
package handlers
import (
"errors"
"platform/web/models"
q "platform/web/queries"
"platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
type LoginReq struct {
Username string `json:"username"`
Password string `json:"password"`
Remember bool `json:"remember"`
}
type LoginResp struct {
Token string `json:"token"`
Expires int64 `json:"expires"`
}
func Login(c *fiber.Ctx) error {
// 验证请求参数
req := new(LoginReq)
if err := c.BodyParser(req); err != nil {
return err
}
if req.Username == "" {
return fiber.NewError(fiber.StatusBadRequest, "手机号不能为空")
}
if req.Password == "" {
return fiber.NewError(fiber.StatusBadRequest, "验证码不能为空")
}
return loginByPhone(c, req)
}
func loginByPhone(c *fiber.Ctx, req *LoginReq) error {
// 验证验证码
ok, err := services.Verifier.VerifySms(c.Context(), req.Username, req.Password)
if err != nil {
return err
}
if !ok {
return fiber.NewError(fiber.StatusBadRequest, "验证码错误")
}
// 查找用户 todo 获取权限信息
var tx = q.Q.Begin()
var user *models.User
user, err = tx.User.
Where(tx.User.Phone.Eq(req.Username)).
Take()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// 如果用户不存在,初始化用户 todo 保存默认权限信息
if user == nil {
user = &models.User{
Phone: req.Username,
}
}
// 更新用户的登录时间
user.LastLogin = time.Now()
user.LastLoginHost = c.IP()
user.LastLoginAgent = c.Get("User-Agent")
if err := tx.User.Omit(q.User.AdminID).Save(user); err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
// 保存到会话
auth := services.AuthContext{
Permissions: map[string]struct{}{
"user": {},
},
Payload: services.Payload{
Type: services.PayloadUser,
Id: user.ID,
},
}
duration := time.Hour * 24
if req.Remember {
duration *= 7
}
token, err := services.Session.Create(c.Context(), auth)
if err != nil {
return err
}
return c.JSON(LoginResp{
Token: token.AccessToken,
Expires: token.AccessTokenExpires.Unix(),
})
}

243
web/handlers/oauth.go Normal file
View File

@@ -0,0 +1,243 @@
package handlers
import (
"encoding/base64"
"errors"
"platform/web/models"
q "platform/web/queries"
"platform/web/services"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// region Token
type TokenReq struct {
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
GrantType TokenGrantType `json:"grant_type" form:"grant_type"`
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
RefreshToken string `json:"refresh_token" form:"refresh_token"`
Scope string `json:"scope" form:"scope"`
}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
ExpiresIn int `json:"expires_in"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
type TokenGrantType string
const (
AuthorizationCode = TokenGrantType("authorization_code")
ClientCredentials = TokenGrantType("client_credentials")
RefreshToken = TokenGrantType("refresh_token")
)
// Token 处理 OAuth2.0 授权请求
func Token(c *fiber.Ctx) error {
// 验证请求参数
req := new(TokenReq)
if err := c.BodyParser(req); err != nil {
return sendError(c, services.ErrOauthInvalidRequest, "无法解析请求参数")
}
if req.GrantType == "" {
return sendError(c, services.ErrOauthInvalidRequest, "缺少必要参数grant_type")
}
// 基于授权类型处理请求
switch req.GrantType {
case AuthorizationCode:
return authorizationCode(c, req)
case ClientCredentials:
return clientCredentials(c, req)
case RefreshToken:
return refreshToken(c, req)
default:
return sendError(c, services.ErrOauthUnsupportedGrantType)
}
}
// 授权码
func authorizationCode(c *fiber.Ctx, req *TokenReq) error {
if req.Code == "" {
return sendError(c, services.ErrOauthInvalidRequest, "缺少必要参数code")
}
client, err := protect(c, services.GrantTypeAuthorizationCode, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
token, err := services.Auth.OauthAuthorizationCode(c.Context(), client, req.Code, req.RedirectURI, req.CodeVerifier)
if err != nil {
return sendError(c, err.(services.AuthServiceOauthError))
}
return sendSuccess(c, token)
}
// 客户端凭证
func clientCredentials(c *fiber.Ctx, req *TokenReq) error {
client, err := protect(c, services.GrantTypeClientCredentials, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
scope := strings.Split(req.Scope, ",")
token, err := services.Auth.OauthClientCredentials(c.Context(), client, scope)
if err != nil {
return sendError(c, err.(services.AuthServiceOauthError))
}
return sendSuccess(c, token)
}
// 刷新令牌
func refreshToken(c *fiber.Ctx, req *TokenReq) error {
if req.RefreshToken == "" {
return sendError(c, services.ErrOauthInvalidRequest, "缺少必要参数refresh_token")
}
client, err := protect(c, services.GrantTypeRefreshToken, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
scope := strings.Split(req.Scope, ",")
token, err := services.Auth.OauthRefreshToken(c.Context(), client, req.RefreshToken, scope)
if err != nil {
return sendError(c, err.(services.AuthServiceOauthError))
}
return sendSuccess(c, token)
}
// 检查客户端凭证
func protect(c *fiber.Ctx, grant services.GrantType, clientId, clientSecret string) (*models.Client, error) {
header := c.Get("Authorization")
if header != "" {
basic := strings.TrimPrefix(header, "Basic ")
if basic != "" {
base, err := base64.URLEncoding.DecodeString(basic)
if err != nil {
return nil, err
}
parts := strings.SplitN(string(base), ":", 2)
if len(parts) == 2 {
clientId = parts[0]
clientSecret = parts[1]
}
}
}
// 查找客户端
if clientId == "" {
return nil, services.ErrOauthInvalidRequest
}
client, err := q.Client.Where(q.Client.ClientID.Eq(clientId)).Take()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, services.ErrOauthInvalidClient
}
return nil, err
}
// 验证客户端状态
if client.Status != 1 {
return nil, services.ErrOauthUnauthorizedClient
}
// 验证授权类型
switch grant {
case services.GrantTypeAuthorizationCode:
if !client.GrantCode {
return nil, services.ErrOauthUnauthorizedClient
}
case services.GrantTypeClientCredentials:
if !client.GrantClient || client.Spec != 0 {
return nil, services.ErrOauthUnauthorizedClient
}
case services.GrantTypeRefreshToken:
if !client.GrantRefresh {
return nil, services.ErrOauthUnauthorizedClient
}
}
// 如果客户端是 confidential验证 client_secret失败返回错误
if client.Spec == 0 {
if clientSecret == "" {
return nil, services.ErrOauthInvalidRequest
}
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, services.ErrOauthInvalidClient
}
}
return client, nil
}
// 发送成功响应
func sendSuccess(c *fiber.Ctx, details *services.TokenDetails) error {
return c.JSON(TokenResp{
AccessToken: details.AccessToken,
TokenType: "Bearer",
ExpiresIn: int(time.Until(details.AccessTokenExpires).Seconds()),
RefreshToken: details.RefreshToken,
})
}
// 发送错误响应
func sendError(c *fiber.Ctx, err error, description ...string) error {
var sErr services.AuthServiceOauthError
if errors.As(err, &sErr) {
status := fiber.StatusBadRequest
var desc string
switch {
case errors.Is(sErr, services.ErrOauthInvalidRequest):
desc = "无效的请求"
case errors.Is(sErr, services.ErrOauthInvalidClient):
status = fiber.StatusUnauthorized
desc = "无效的客户端凭证"
case errors.Is(sErr, services.ErrOauthInvalidGrant):
desc = "无效的授权凭证"
case errors.Is(sErr, services.ErrOauthInvalidScope):
desc = "无效的授权范围"
case errors.Is(sErr, services.ErrOauthUnauthorizedClient):
desc = "未授权的客户端"
case errors.Is(sErr, services.ErrOauthUnsupportedGrantType):
desc = "不支持的授权类型"
}
if len(description) > 0 {
desc = description[0]
}
return c.Status(status).JSON(TokenErrResp{
Error: string(sErr),
Description: desc,
})
}
return err
}
// endregion

44
web/handlers/verifier.go Normal file
View File

@@ -0,0 +1,44 @@
package handlers
import (
"errors"
"platform/web/services"
"regexp"
"strconv"
"github.com/gofiber/fiber/v2"
)
type VerifierReq struct {
Purpose services.VerifierSmsPurpose `json:"purpose"`
Phone string `json:"phone"`
}
func SmsCode(c *fiber.Ctx) error {
// 解析请求参数
req := new(VerifierReq)
if err := c.BodyParser(req); err != nil {
return err
}
match, err := regexp.MatchString(`^1[3-9]\d{9}$`, req.Phone)
if err != nil {
return err
}
if !match {
return fiber.NewError(fiber.StatusBadRequest, "手机号格式错误")
}
// 发送身份验证码
err = services.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
if err != nil {
var sErr services.VerifierServiceSendLimitErr
if errors.As(err, &sErr) {
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
}
return err
}
// 发送成功
return nil
}