refactor user service methods, implement OIDC login and user management features, and enhance token handling

This commit is contained in:
2025-07-22 20:45:05 +08:00
parent f07200b0b9
commit cbe73121f2
17 changed files with 655 additions and 126 deletions

View File

@ -10,6 +10,7 @@ const (
RoleUser = "user"
RoleAdmin = "admin"
EnvKeyBaseUrl = "BASE_URL" // 环境变量基础URL
EnvKeyMode = "MODE" // 环境变量:运行模式
EnvKeyJwtSecrete = "JWT_SECRET" // 环境变量JWT密钥
EnvKeyPasswordSalt = "PASSWORD_SALT" // 环境变量:密码盐
@ -18,5 +19,9 @@ const (
EnvKeyRefreshTokenDuration = "REFRESH_TOKEN_DURATION" // 环境变量:刷新令牌有效期
EnvKeyRefreshTokenDurationWithRemember = "REFRESH_TOKEN_DURATION_WITH_REMEMBER" // 环境变量:记住我刷新令牌有效期
KVKeyEmailVerificationCode = "email_verification_code" // KV存储邮箱验证码
KVKeyEmailVerificationCode = "email_verification_code:" // KV存储邮箱验证码
KVKeyOidcState = "oidc_state:" // KV存储OIDC状态
OidcUri = "/user/oidc/login" // OIDC登录URI
DefaultBaseUrl = "http://localhost:3000" // 默认BaseUrl
)

View File

@ -11,6 +11,7 @@ func Custom(c *app.RequestContext, status int, message string, data any) {
"message": message,
"data": data,
})
c.Abort()
}
func Ok(c *app.RequestContext, message string, data any) {

View File

@ -36,7 +36,7 @@ func (c *Claims) ToString() (string, error) {
return token.SignedString([]byte(Env.Get(constant.EnvKeyJwtSecrete, "default_jwt_secret")))
}
// ParseJsonWebTokenWithoutState 解析JWT令牌不对有状态的Token进行状态检查
// ParseJsonWebTokenWithoutState 解析JWT令牌仅检查无状态下是否valid不对有状态的Token进行状态检查
func (j *jwtUtils) ParseJsonWebTokenWithoutState(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (any, error) {

72
pkg/utils/oidc.go Normal file
View File

@ -0,0 +1,72 @@
package utils
import (
"fmt"
"resty.dev/v3"
)
type oidcUtils struct{}
var Oidc = oidcUtils{}
// RequestToken 请求访问令牌
func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) {
client := resty.New()
tokenResp, err := client.R().
SetFormData(map[string]string{
"grant_type": "authorization_code",
"client_id": clientID,
"client_secret": clientSecret,
"code": code,
"redirect_uri": redirectURI,
}).
SetHeader("Accept", "application/json").
SetResult(&TokenResponse{}).
Post(tokenEndpoint)
if err != nil {
return nil, err
}
if tokenResp.StatusCode() != 200 {
return nil, fmt.Errorf("状态码: %d响应: %s", tokenResp.StatusCode(), tokenResp.String())
}
return tokenResp.Result().(*TokenResponse), nil
}
// RequestUserInfo 请求用户信息
func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) {
client := resty.New()
userInfoResp, err := client.R().
SetHeader("Authorization", "Bearer "+accessToken).
SetHeader("Accept", "application/json").
SetResult(&UserInfo{}).
Get(userInfoEndpoint)
if err != nil {
return nil, err
}
if userInfoResp.StatusCode() != 200 {
return nil, fmt.Errorf("状态码: %d响应: %s", userInfoResp.StatusCode(), userInfoResp.String())
}
return userInfoResp.Result().(*UserInfo), nil
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// UserInfo 定义用户信息结构
type UserInfo struct {
Sub string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture,omitempty"`
Groups []string `json:"groups,omitempty"` // 可选字段OIDC提供的用户组信息
}

20
pkg/utils/url.go Normal file
View File

@ -0,0 +1,20 @@
package utils
import "net/url"
type urlUtils struct{}
var Url = &urlUtils{}
func (u *urlUtils) BuildUrl(baseUrl string, queryParams map[string]string) string {
newUrl, err := url.Parse(baseUrl)
if err != nil {
return baseUrl
}
q := newUrl.Query()
for key, value := range queryParams {
q.Set(key, value)
}
newUrl.RawQuery = q.Encode()
return newUrl.String()
}