feat: Implement Oauth2 login log service and repository

- Added Oauth2LogLoginService for managing user authorization logs.
- Implemented methods for inserting logs, cleaning logs, and exporting log data.
- Created a new file for Oauth2 login log service.

refactor: Remove unused open_api module

- Deleted the open_api.go file as it was not utilized in the project.

fix: Update error codes in SysProfileController

- Changed error codes for binding errors and user authentication errors to more descriptive values.

fix: Update cache handling in SysConfig and SysDictType services

- Modified Redis set operations to include expiration time for cached values.

refactor: Update middleware authorization checks

- Replaced PreAuthorize middleware with AuthorizeUser across multiple routes in system and tool modules for consistency.

chore: Clean up trace and ws modules

- Updated middleware authorization in trace and ws modules to use AuthorizeUser.
This commit is contained in:
TsMask
2025-04-27 11:07:34 +08:00
parent b29a36e7b5
commit 56991a0b49
72 changed files with 2334 additions and 873 deletions

View File

@@ -2,8 +2,8 @@ package constants
// 缓存的key常量
const (
// CACHE_LOGIN_TOKEN 登录用户
CACHE_LOGIN_TOKEN = "login_tokens"
// CACHE_TOKEN_DEVICE 登录用户令牌标识
CACHE_TOKEN_DEVICE = "token_devices"
// CACHE_CAPTCHA_CODE 验证码
CACHE_CAPTCHA_CODE = "captcha_codes"
// CACHE_SYS_CONFIG 参数管理
@@ -16,6 +16,10 @@ const (
CACHE_RATE_LIMIT = "rate_limit"
// CACHE_PWD_ERR_COUNT 登录账户密码错误次数
CACHE_PWD_ERR_COUNT = "pwd_err_count"
// CACHE_OAUTH2_DEVICE 授权客户端令牌标识
CACHE_OAUTH2_DEVICE = "oauth2_devices"
// CACHE_OAUTH2_CODE 客户端授权码
CACHE_OAUTH2_CODE = "oauth2_codes"
// CACHE_I18N 国际化语言管理
CACHE_I18N = "i18n"
// CACHE_NE_INFO 网元信息管理

View File

@@ -10,5 +10,8 @@ const (
// CTX_LOGIN_USER 上下文信息-登录用户
const CTX_LOGIN_USER = "ctx:login_user"
// CTX_LOGIN_OAUTH2 上下文信息-认证客户端
const CTX_LOGIN_OAUTH2 = "ctx:login_oauth2"
// 启动-引导系统初始
const LAUNCH_BOOTLOADER = "bootloader"

View File

@@ -3,19 +3,19 @@ package constants
// 令牌常量信息
// HEADER_PREFIX 令牌-请求头标识前缀
const HEADER_PREFIX = "Bearer "
const HEADER_PREFIX = "Bearer"
// HEADER_KEY 令牌-请求头标识
const HEADER_KEY = "Authorization"
// JWT_UUID 令牌-JWT唯一标识字段
const JWT_UUID = "uuid"
// JWT_DEVICE_ID 令牌-JWT设备标识字段
const JWT_DEVICE_ID = "device_id"
// JWT_USER_ID 令牌-JWT标识用户主键字段
const JWT_USER_ID = "user_id"
// JWT_USER_NAME 令牌-JWT标识用户登录账号字段
const JWT_USER_NAME = "user_name"
// JWT_CLIENT_ID 令牌-JWT标识客户端ID字段
const JWT_CLIENT_ID = "client_id"
// NMS北向使用-数据响应字段和请求头授权
const ACCESS_TOKEN = "accessToken"

View File

@@ -16,6 +16,21 @@ import (
// Redis连接实例
var rdbMap = make(map[string]*redis.Client)
// 声明定义限流脚本命令
var rateLimitCommand = redis.NewScript(`
local key = KEYS[1]
local time = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local current = redis.call('get', key);
if current and tonumber(current) >= count then
return tonumber(current);
end
current = redis.call('incr', key)
if tonumber(current) == 1 then
redis.call('expire', key, time)
end
return tonumber(current);`)
// Connect 连接Redis实例
func Connect() {
ctx := context.Background()
@@ -33,7 +48,7 @@ func Connect() {
// 测试数据库连接
pong, err := rdb.Ping(ctx).Result()
if err != nil {
logger.Fatalf("failed error redis connect: %s is %v", k, err)
logger.Fatalf("Ping redis %s is %v", k, err)
}
logger.Infof("redis %s %d %s connection is successful.", k, client["db"].(int), pong)
rdbMap[k] = rdb
@@ -139,6 +154,39 @@ func CommandStats(source string) []map[string]string {
return statsObjArr
}
// Has 判断是否存在
func Has(source string, keys ...string) (int64, error) {
// 数据源
rdb := RDB(source)
if rdb == nil {
return 0, fmt.Errorf("redis not client")
}
ctx := context.Background()
exists, err := rdb.Exists(ctx, keys...).Result()
if err != nil {
return 0, err
}
return exists, nil
}
// SetExpire 设置过期时间
func SetExpire(source, key string, expiration time.Duration) error {
// 数据源
rdb := RDB(source)
if rdb == nil {
return fmt.Errorf("redis not client")
}
ctx := context.Background()
err := rdb.Expire(ctx, key, expiration).Err()
if err != nil {
logger.Errorf("redis Expire err %v", err)
return err
}
return nil
}
// GetExpire 获取键的剩余有效时间(秒)
func GetExpire(source string, key string) (int64, error) {
// 数据源
@@ -227,41 +275,8 @@ func Get(source, key string) (string, error) {
return v, nil
}
// Has 判断是否存在
func Has(source string, keys ...string) (int64, error) {
// 数据源
rdb := RDB(source)
if rdb == nil {
return 0, fmt.Errorf("redis not client")
}
ctx := context.Background()
exists, err := rdb.Exists(ctx, keys...).Result()
if err != nil {
return 0, err
}
return exists, nil
}
// Set 设置缓存数据
func Set(source, key string, value any) error {
// 数据源
rdb := RDB(source)
if rdb == nil {
return fmt.Errorf("redis not client")
}
ctx := context.Background()
err := rdb.Set(ctx, key, value, 0).Err()
if err != nil {
logger.Errorf("redis Set err %v", err)
return err
}
return nil
}
// SetByExpire 设置缓存数据与过期时间
func SetByExpire(source, key string, value any, expiration time.Duration) error {
func Set(source, key string, value any, expiration time.Duration) error {
// 数据源
rdb := RDB(source)
if rdb == nil {
@@ -271,7 +286,7 @@ func SetByExpire(source, key string, value any, expiration time.Duration) error
ctx := context.Background()
err := rdb.Set(ctx, key, value, expiration).Err()
if err != nil {
logger.Errorf("redis SetByExpire err %v", err)
logger.Errorf("redis Set err %v", err)
return err
}
return nil
@@ -329,18 +344,3 @@ func RateLimit(source, limitKey string, time, count int64) (int64, error) {
}
return result.(int64), err
}
// 声明定义限流脚本命令
var rateLimitCommand = redis.NewScript(`
local key = KEYS[1]
local time = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local current = redis.call('get', key);
if current and tonumber(current) >= count then
return tonumber(current);
end
current = redis.call('incr', key)
if tonumber(current) == 1 then
redis.call('expire', key, time)
end
return tonumber(current);`)

View File

@@ -0,0 +1,66 @@
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
"be.ems/src/framework/constants"
"be.ems/src/framework/reqctx"
"be.ems/src/framework/resp"
"be.ems/src/framework/token"
)
// AuthorizeOauth2 客户端授权认证校验
//
// scope 客户端授权范围,例如:[]string{"read","write"}
func AuthorizeOauth2(scope []string) gin.HandlerFunc {
return func(c *gin.Context) {
// 获取请求头标识信息
tokenStr := reqctx.Authorization(c)
if tokenStr == "" {
c.JSON(401, resp.CodeMsg(401003, "authorization token is empty"))
c.Abort() // 停止执行后续的处理函数
return
}
// 验证令牌
claims, err := token.Oauth2TokenVerify(tokenStr, "access")
if err != nil {
c.JSON(401, resp.CodeMsg(401001, err.Error()))
c.Abort() // 停止执行后续的处理函数
return
}
// 获取缓存的用户信息
info := token.Oauth2InfoGet(claims)
if info.ClientId == "" {
c.JSON(401, resp.CodeMsg(401002, "invalid login user information"))
c.Abort() // 停止执行后续的处理函数
return
}
c.Set(constants.CTX_LOGIN_OAUTH2, info)
// 客户端权限校验
if scope != nil {
var hasScope bool = false
for _, item := range info.Scope {
for _, v := range scope {
if item == v {
hasScope = true
break
}
}
}
if !hasScope {
msg := fmt.Sprintf("unauthorized access %s %s", c.Request.Method, c.Request.RequestURI)
c.JSON(403, resp.CodeMsg(403001, msg))
c.Abort() // 停止执行后续的处理函数
return
}
}
// 调用下一个处理程序
c.Next()
}
}

View File

@@ -1,17 +1,17 @@
package middleware
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
"be.ems/src/framework/config"
"be.ems/src/framework/constants"
"be.ems/src/framework/i18n"
"be.ems/src/framework/reqctx"
"be.ems/src/framework/resp"
"be.ems/src/framework/token"
"be.ems/src/framework/utils/parse"
"github.com/gin-gonic/gin"
)
/**无Token可访问白名单 */
@@ -27,7 +27,7 @@ var URL_WHITE_LIST = []string{
"/oauth/token",
}
// PreAuthorize 用户身份授权认证校验
// AuthorizeUser 用户身份授权认证校验
//
// 只需含有其中角色 "hasRoles": {"xxx"},
//
@@ -36,7 +36,7 @@ var URL_WHITE_LIST = []string{
// 同时匹配其中角色 "matchRoles": {"xxx"},
//
// 同时匹配其中权限 "matchPerms": {"xxx"},
func PreAuthorize(options map[string][]string) gin.HandlerFunc {
func AuthorizeUser(options map[string][]string) gin.HandlerFunc {
return func(c *gin.Context) {
// 登录认证,默认打开
enable := parse.Boolean(config.Get("serverLoginAuth"))
@@ -51,12 +51,9 @@ func PreAuthorize(options map[string][]string) gin.HandlerFunc {
return
}
language := reqctx.AcceptLanguage(c)
requestURI := c.Request.RequestURI
// 判断白名单
isWhite := false
requestURI := c.Request.RequestURI
for _, w := range URL_WHITE_LIST {
if strings.Contains(requestURI, w) {
isWhite = true
@@ -71,42 +68,39 @@ func PreAuthorize(options map[string][]string) gin.HandlerFunc {
// 获取请求头标识信息
tokenStr := reqctx.Authorization(c)
if tokenStr == "" {
c.JSON(401, resp.CodeMsg(401, i18n.TKey(language, "app.common.err401")))
c.JSON(401, resp.CodeMsg(401003, "authorization token is empty"))
c.Abort() // 停止执行后续的处理函数
return
}
// 验证令牌
claims, err := token.Verify(tokenStr)
claims, err := token.UserTokenVerify(tokenStr, "access")
if err != nil {
c.JSON(401, resp.CodeMsg(401, err.Error()))
c.JSON(401, resp.CodeMsg(401001, err.Error()))
c.Abort() // 停止执行后续的处理函数
return
}
// 获取缓存的用户信息
loginUser := token.Info(claims)
if loginUser.UserId <= 0 {
c.JSON(401, resp.CodeMsg(401, i18n.TKey(language, "app.common.err401")))
info := token.UserInfoGet(claims)
if info.UserId <= 0 {
c.JSON(401, resp.CodeMsg(401002, "invalid login user information"))
c.Abort() // 停止执行后续的处理函数
return
}
// 检查刷新有效期后存入上下文
token.RefreshIn(&loginUser)
c.Set(constants.CTX_LOGIN_USER, loginUser)
c.Set(constants.CTX_LOGIN_USER, info)
// 登录用户角色权限校验
if options != nil {
var roles []string
for _, item := range loginUser.User.Roles {
for _, item := range info.User.Roles {
roles = append(roles, item.RoleKey)
}
perms := loginUser.Permissions
perms := info.Permissions
verifyOk := verifyRolePermission(roles, perms, options)
if !verifyOk {
msg := i18n.TTemplate(language, "app.common.err403", map[string]any{"method": c.Request.Method, "requestURI": requestURI})
c.JSON(403, resp.CodeMsg(403, msg))
msg := fmt.Sprintf("unauthorized access %s %s", c.Request.Method, c.Request.RequestURI)
c.JSON(403, resp.CodeMsg(403001, msg))
c.Abort() // 停止执行后续的处理函数
return
}
@@ -125,7 +119,7 @@ func PreAuthorize(options map[string][]string) gin.HandlerFunc {
//
// options 参数
func verifyRolePermission(roles, perms []string, options map[string][]string) bool {
// 直接放行 管理员角色或任意权限
// 直接放行 系统管理员角色或任意权限
if contains(roles, constants.SYS_ROLE_SYSTEM_KEY) || contains(perms, constants.SYS_PERMISSION_SYSTEM) {
return true
}

View File

@@ -134,7 +134,7 @@ func OperateLog(options Options) gin.HandlerFunc {
contentDisposition := c.Writer.Header().Get("Content-Disposition")
contentType := c.Writer.Header().Get("Content-Type")
content := contentType + contentDisposition
msg := fmt.Sprintf(`{"status":"%d","size":"%d","content-type":"%s"}`, status, c.Writer.Size(), content)
msg := fmt.Sprintf(`{"status":"%d","size":%d,"content-type":"%s"}`, status, c.Writer.Size(), content)
operaLog.OperaMsg = msg
}

View File

@@ -77,7 +77,7 @@ func RepeatSubmit(interval int64) gin.HandlerFunc {
logger.Errorf("RepeatSubmit rp json marshal err: %v", err)
}
// 保存请求时间和参数
redis.SetByExpire("", repeatKey, string(rpJSON), time.Duration(interval)*time.Second)
redis.Set("", repeatKey, string(rpJSON), time.Duration(interval)*time.Second)
// 调用下一个处理程序
c.Next()

View File

@@ -76,7 +76,7 @@ func RepeatSubmit(interval int64) gin.HandlerFunc {
logger.Errorf("RepeatSubmit rp json marshal err: %v", err)
}
// 保存请求时间和参数
_ = redis.SetByExpire("", repeatKey, string(rpJSON), time.Duration(interval)*time.Second)
_ = redis.Set("", repeatKey, string(rpJSON), time.Duration(interval)*time.Second)
// 调用下一个处理程序
c.Next()

View File

@@ -12,12 +12,12 @@ import (
)
// LoginUser 登录用户信息
func LoginUser(c *gin.Context) (token.TokenInfo, error) {
func LoginUser(c *gin.Context) (token.UserInfo, error) {
value, exists := c.Get(constants.CTX_LOGIN_USER)
if exists && value != nil {
return value.(token.TokenInfo), nil
return value.(token.UserInfo), nil
}
return token.TokenInfo{}, fmt.Errorf("invalid login user information")
return token.UserInfo{}, fmt.Errorf("invalid login user information")
}
// LoginUserToUserID 登录用户信息-用户ID
@@ -58,14 +58,14 @@ func LoginUserByContainRoles(c *gin.Context, target string) bool {
// LoginUserByContainPerms 登录用户信息-包含权限标识
func LoginUserByContainPerms(c *gin.Context, target string) bool {
loginUser, err := LoginUser(c)
info, err := LoginUser(c)
if err != nil {
return false
}
if config.IsSystemUser(loginUser.UserId) {
if config.IsSystemUser(info.UserId) {
return true
}
perms := loginUser.Permissions
perms := info.Permissions
for _, str := range perms {
if str == target {
return true

View File

@@ -73,11 +73,11 @@ func Authorization(c *gin.Context) string {
return ""
}
// 拆分 Authorization 请求头,提取 JWT 令牌部分
arr := strings.SplitN(authHeader, constants.HEADER_PREFIX, 2)
if len(arr) < 2 {
return ""
tokenStr := strings.Replace(authHeader, constants.HEADER_PREFIX, "", 1)
if len(tokenStr) > 64 {
return strings.TrimSpace(tokenStr) // 去除可能存在的空格
}
return arr[1]
return ""
}
// AcceptLanguage 解析客户端接收语言 zh中文 en: 英文

View File

@@ -1,9 +1,12 @@
package reqctx
import (
"fmt"
"github.com/gin-gonic/gin"
"be.ems/src/framework/ip2region"
"be.ems/src/framework/utils/crypto"
"be.ems/src/framework/utils/ua"
)
@@ -33,3 +36,9 @@ func UaOsBrowser(c *gin.Context) (string, string) {
}
return os, browser
}
// DeviceFingerprint 设备指纹信息
func DeviceFingerprint(c *gin.Context, v any) string {
str := fmt.Sprintf("%v:%s", v, c.Request.UserAgent())
return crypto.SHA256ToBase64(str)
}

View File

@@ -2,17 +2,17 @@ package resp
const (
// CODE_ERROR 响应-code错误失败
CODE_ERROR = 0
CODE_ERROR = 400001
// MSG_ERROR 响应-msg错误失败
MSG_ERROR = "error"
// CODE_SUCCESS 响应-msg正常成功
CODE_SUCCESS = 1
CODE_SUCCESS = 200001
// MSG_SUCCCESS 响应-code正常成功
MSG_SUCCCESS = "success"
// 响应-code加密数据
CODE_ENCRYPT = 2
CODE_ENCRYPT = 200999
// 响应-msg加密数据
MSG_ENCRYPT = "encrypt"
)

View File

@@ -0,0 +1,14 @@
package token
// Oauth2Info 第三方客户端令牌信息对象
type Oauth2Info struct {
DeviceId string `json:"deviceId"` // 用户设备标识
ClientId string `json:"clientId"` // 客户端ID
LoginTime int64 `json:"loginTime"` // 登录时间时间戳
ExpireTime int64 `json:"expireTime"` // 过期时间时间戳
LoginIp string `json:"loginIp"` // 登录IP地址 x.x.x.x
LoginLocation string `json:"loginLocation"` // 登录地点 xx xx
Browser string `json:"browser"` // 浏览器类型
OS string `json:"os"` // 操作系统
Scope []string `json:"scope"` // 权限列表
}

View File

@@ -0,0 +1,167 @@
package token
import (
"encoding/json"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"be.ems/src/framework/config"
"be.ems/src/framework/constants"
"be.ems/src/framework/database/redis"
"be.ems/src/framework/logger"
"be.ems/src/framework/utils/parse"
)
// Oauth2TokenCreate 生成令牌
// clientId 客户端ID
// deviceFingerprint 设备指纹 SHA256
// tokenType 令牌类型 access:访问令牌 refresh:刷新令牌
func Oauth2TokenCreate(clientId, deviceFingerprint, tokenType string) (string, int64) {
// 令牌算法 HS256 HS384 HS512
algorithm := config.Get("jwt.algorithm").(string)
var method *jwt.SigningMethodHMAC
switch algorithm {
case "HS512":
method = jwt.SigningMethodHS512
case "HS384":
method = jwt.SigningMethodHS384
default: // 包含HS256和其他所有情况
method = jwt.SigningMethodHS256
}
// 生成令牌设置密钥
secret := fmt.Sprint(config.Get("jwt.secret"))
// 设置令牌过期时间
now := time.Now()
exp := now
if tokenType == "access" {
expiresIn := time.Duration(parse.Number(config.Get("jwt.expiresIn")))
exp = now.Add(expiresIn * time.Minute)
secret = "Oauth2_Access:" + secret
}
if tokenType == "refresh" {
refreshIn := time.Duration(parse.Number(config.Get("jwt.refreshIn")))
exp = now.Add(refreshIn * time.Minute)
secret = "Oauth2_Refresh:" + secret
}
// 生成令牌负荷绑定uuid标识
jwtToken := jwt.NewWithClaims(method, jwt.MapClaims{
constants.JWT_DEVICE_ID: deviceFingerprint,
constants.JWT_CLIENT_ID: clientId,
"exp": exp.Unix(), // 过期时间
"iat": now.Unix(), // 签发时间
"nbf": now.Unix(), // 生效时间
})
tokenStr, err := jwtToken.SignedString([]byte(secret))
if err != nil {
logger.Infof("jwt sign err : %v", err)
return "", 0
}
expSeconds := int64(exp.Sub(now).Seconds())
return tokenStr, expSeconds
}
// Oauth2TokenVerify 校验令牌是否有效
// tokenType 令牌类型 access:访问令牌 refresh:刷新令牌
func Oauth2TokenVerify(tokenStr, tokenType string) (jwt.MapClaims, error) {
jwtToken, err := jwt.Parse(tokenStr, func(jToken *jwt.Token) (any, error) {
// 判断加密算法是预期的加密算法
if _, ok := jToken.Method.(*jwt.SigningMethodHMAC); ok {
secret := config.Get("jwt.secret").(string)
if tokenType == "access" {
secret = "Oauth2_Access:" + secret
}
if tokenType == "refresh" {
secret = "Oauth2_Refresh:" + secret
}
return []byte(secret), nil
}
return nil, jwt.ErrSignatureInvalid
})
if err != nil {
logger.Errorf("Token Verify Err: %v", err)
return nil, fmt.Errorf("token invalid")
}
// 如果解析负荷成功并通过签名校验
claims, ok := jwtToken.Claims.(jwt.MapClaims)
if ok && jwtToken.Valid {
return claims, nil
}
return nil, fmt.Errorf("token valid error")
}
// Oauth2InfoRemove 清除登录第三方客户端信息
func Oauth2InfoRemove(tokenStr string) (string, error) {
claims, err := Oauth2TokenVerify(tokenStr, "access")
if err != nil {
logger.Errorf("token verify err %v", err)
return "", err
}
deviceId, ok := claims[constants.JWT_DEVICE_ID]
if ok && deviceId != "" {
// 清除缓存KEY
tokenKey := constants.CACHE_OAUTH2_DEVICE + ":" + fmt.Sprint(deviceId)
return fmt.Sprint(claims[constants.JWT_CLIENT_ID]), redis.Del("", tokenKey)
}
return "", fmt.Errorf("token invalid")
}
// Oauth2InfoCreate 生成访问第三方客户端信息缓存
func Oauth2InfoCreate(info *Oauth2Info, deviceFingerprint string, ilobArr [4]string) {
info.DeviceId = deviceFingerprint
// 设置请求登录客户端
info.LoginIp = ilobArr[0]
info.LoginLocation = ilobArr[1]
info.OS = ilobArr[2]
info.Browser = ilobArr[3]
expiresIn := time.Duration(parse.Number(config.Get("jwt.expiresIn")))
now := time.Now()
exp := now.Add(expiresIn * time.Minute)
info.LoginTime = now.UnixMilli()
info.ExpireTime = exp.UnixMilli()
// 登录信息标识缓存
tokenKey := constants.CACHE_OAUTH2_DEVICE + ":" + info.DeviceId
jsonBytes, err := json.Marshal(info)
if err != nil {
return
}
_ = redis.Set("", tokenKey, string(jsonBytes), expiresIn*time.Minute)
}
// Oauth2InfoUpdate 更新访问第三方客户端信息缓存
func Oauth2InfoUpdate(info Oauth2Info) {
// 登录信息标识缓存
tokenKey := constants.CACHE_OAUTH2_DEVICE + ":" + info.DeviceId
jsonBytes, err := json.Marshal(info)
if err != nil {
return
}
expiresIn, _ := redis.GetExpire("", tokenKey)
expiration := time.Duration(expiresIn) * time.Second
_ = redis.Set("", tokenKey, string(jsonBytes), expiration)
}
// Oauth2InfoGet 缓存的登录第三方客户端信息
func Oauth2InfoGet(claims jwt.MapClaims) Oauth2Info {
info := Oauth2Info{}
deviceId := fmt.Sprint(claims[constants.JWT_DEVICE_ID])
tokenKey := constants.CACHE_OAUTH2_DEVICE + ":" + deviceId
hasKey, err := redis.Has("", tokenKey)
if hasKey > 0 && err == nil {
infoStr, err := redis.Get("", tokenKey)
if infoStr == "" || err != nil {
return info
}
if err := json.Unmarshal([]byte(infoStr), &info); err != nil {
logger.Errorf("info json err : %v", err)
return info
}
}
return info
}

View File

@@ -1,152 +0,0 @@
package token
import (
"encoding/json"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"be.ems/src/framework/config"
"be.ems/src/framework/constants"
"be.ems/src/framework/database/redis"
"be.ems/src/framework/logger"
"be.ems/src/framework/utils/generate"
)
// Remove 清除登录用户信息UUID
func Remove(token string) string {
claims, err := Verify(token)
if err != nil {
logger.Errorf("token verify err %v", err)
return ""
}
// 清除缓存KEY
uuid := claims[constants.JWT_UUID].(string)
tokenKey := constants.CACHE_LOGIN_TOKEN + ":" + uuid
hasKey, err := redis.Has("", tokenKey)
if hasKey > 0 && err == nil {
_ = redis.Del("", tokenKey)
}
return claims[constants.JWT_USER_NAME].(string)
}
// Create 令牌生成
func Create(tokenInfo *TokenInfo, ilobArr [4]string) string {
// 生成用户唯一token 32位
tokenInfo.UUID = generate.Code(32)
tokenInfo.LoginTime = time.Now().UnixMilli()
// 设置请求用户登录客户端
tokenInfo.LoginIp = ilobArr[0]
tokenInfo.LoginLocation = ilobArr[1]
tokenInfo.OS = ilobArr[2]
tokenInfo.Browser = ilobArr[3]
// 设置新登录IP和登录时间
tokenInfo.User.LoginIp = tokenInfo.LoginIp
tokenInfo.User.LoginTime = tokenInfo.LoginTime
// 设置用户令牌有效期并存入缓存
Cache(tokenInfo)
// 令牌算法 HS256 HS384 HS512
algorithm := config.Get("jwt.algorithm").(string)
var method *jwt.SigningMethodHMAC
switch algorithm {
case "HS512":
method = jwt.SigningMethodHS512
case "HS384":
method = jwt.SigningMethodHS384
case "HS256":
default:
method = jwt.SigningMethodHS256
}
// 生成令牌负荷绑定uuid标识
jwtToken := jwt.NewWithClaims(method, jwt.MapClaims{
constants.JWT_UUID: tokenInfo.UUID,
constants.JWT_USER_ID: tokenInfo.UserId,
constants.JWT_USER_NAME: tokenInfo.User.UserName,
"ait": tokenInfo.LoginTime,
})
// 生成令牌设置密钥
secret := config.Get("jwt.secret").(string)
tokenStr, err := jwtToken.SignedString([]byte(secret))
if err != nil {
logger.Infof("jwt sign err : %v", err)
return ""
}
return tokenStr
}
// Cache 缓存登录用户信息
func Cache(tokenInfo *TokenInfo) {
// 计算配置的有效期
expTime := config.Get("jwt.expiresIn").(int)
expTimestamp := time.Duration(expTime) * time.Minute
iatTimestamp := time.Now().UnixMilli()
tokenInfo.LoginTime = iatTimestamp
tokenInfo.ExpireTime = iatTimestamp + expTimestamp.Milliseconds()
tokenInfo.User.Password = ""
// 登录信息标识缓存
tokenKey := constants.CACHE_LOGIN_TOKEN + ":" + tokenInfo.UUID
jsonBytes, err := json.Marshal(tokenInfo)
if err != nil {
return
}
_ = redis.SetByExpire("", tokenKey, string(jsonBytes), expTimestamp)
}
// RefreshIn 验证令牌有效期相差不足xx分钟自动刷新缓存
func RefreshIn(loginUser *TokenInfo) {
// 相差不足xx分钟自动刷新缓存
refreshTime := config.Get("jwt.refreshIn").(int)
refreshTimestamp := time.Duration(refreshTime) * time.Minute
// 过期时间
expireTimestamp := loginUser.ExpireTime
currentTimestamp := time.Now().UnixMilli()
if expireTimestamp-currentTimestamp <= refreshTimestamp.Milliseconds() {
Cache(loginUser)
}
}
// Verify 校验令牌是否有效
func Verify(token string) (jwt.MapClaims, error) {
jwtToken, err := jwt.Parse(token, func(jToken *jwt.Token) (any, error) {
// 判断加密算法是预期的加密算法
if _, ok := jToken.Method.(*jwt.SigningMethodHMAC); ok {
secret := config.Get("jwt.secret").(string)
return []byte(secret), nil
}
return nil, jwt.ErrSignatureInvalid
})
if err != nil {
logger.Errorf("Token Verify Err: %v", err)
return nil, fmt.Errorf("token invalid")
}
// 如果解析负荷成功并通过签名校验
if claims, ok := jwtToken.Claims.(jwt.MapClaims); ok && jwtToken.Valid {
return claims, nil
}
return nil, fmt.Errorf("token valid error")
}
// Info 缓存的登录用户信息
func Info(claims jwt.MapClaims) TokenInfo {
tokenInfo := TokenInfo{}
uuid := claims[constants.JWT_UUID].(string)
tokenKey := constants.CACHE_LOGIN_TOKEN + ":" + uuid
hasKey, err := redis.Has("", tokenKey)
if hasKey > 0 && err == nil {
infoStr, err := redis.Get("", tokenKey)
if infoStr == "" || err != nil {
return tokenInfo
}
if err := json.Unmarshal([]byte(infoStr), &tokenInfo); err != nil {
logger.Errorf("info json err : %v", err)
return tokenInfo
}
}
return tokenInfo
}

View File

@@ -2,9 +2,9 @@ package token
import systemModel "be.ems/src/modules/system/model"
// TokenInfo 令牌信息对象
type TokenInfo struct {
UUID string `json:"uuid"` // 用户唯一标识
// UserInfo 系统用户令牌信息对象
type UserInfo struct {
DeviceId string `json:"deviceId"` // 用户设备标识
UserId int64 `json:"userId"` // 用户ID
DeptId int64 `json:"deptId"` // 部门ID
LoginTime int64 `json:"loginTime"` // 登录时间时间戳

View File

@@ -0,0 +1,173 @@
package token
import (
"encoding/json"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"be.ems/src/framework/config"
"be.ems/src/framework/constants"
"be.ems/src/framework/database/redis"
"be.ems/src/framework/logger"
"be.ems/src/framework/utils/parse"
)
// UserTokenCreate 生成令牌
// userId 用户ID
// deviceFingerprint 设备指纹 SHA256
// tokenType 令牌类型 access:访问令牌 refresh:刷新令牌
func UserTokenCreate(userId int64, deviceFingerprint, tokenType string) (string, int64) {
// 令牌算法 HS256 HS384 HS512
algorithm := config.Get("jwt.algorithm").(string)
var method *jwt.SigningMethodHMAC
switch algorithm {
case "HS512":
method = jwt.SigningMethodHS512
case "HS384":
method = jwt.SigningMethodHS384
default: // 包含HS256和其他所有情况
method = jwt.SigningMethodHS256
}
// 生成令牌设置密钥
secret := fmt.Sprint(config.Get("jwt.secret"))
// 设置令牌过期时间
now := time.Now()
exp := now
if tokenType == "access" {
expiresIn := time.Duration(parse.Number(config.Get("jwt.expiresIn")))
exp = now.Add(expiresIn * time.Minute)
secret = "User_Access:" + secret
}
if tokenType == "refresh" {
refreshIn := time.Duration(parse.Number(config.Get("jwt.refreshIn")))
exp = now.Add(refreshIn * time.Minute)
secret = "User_Refresh:" + secret
}
// 生成令牌负荷绑定uuid标识
jwtToken := jwt.NewWithClaims(method, jwt.MapClaims{
constants.JWT_DEVICE_ID: deviceFingerprint,
constants.JWT_USER_ID: userId,
"exp": exp.Unix(), // 过期时间
"iat": now.Unix(), // 签发时间
"nbf": now.Add(-10 * time.Second).Unix(), // 生效时间
})
tokenStr, err := jwtToken.SignedString([]byte(secret))
if err != nil {
logger.Infof("jwt sign err : %v", err)
return "", 0
}
expSeconds := int64(exp.Sub(now).Seconds())
return tokenStr, expSeconds
}
// UserTokenVerify 校验令牌是否有效
// tokenType 令牌类型 access:访问令牌 refresh:刷新令牌
func UserTokenVerify(tokenStr string, tokenType string) (jwt.MapClaims, error) {
jwtToken, err := jwt.Parse(tokenStr, func(jToken *jwt.Token) (any, error) {
// 判断加密算法是预期的加密算法
if _, ok := jToken.Method.(*jwt.SigningMethodHMAC); ok {
secret := config.Get("jwt.secret").(string)
if tokenType == "access" {
secret = "User_Access:" + secret
}
if tokenType == "refresh" {
secret = "User_Refresh:" + secret
}
return []byte(secret), nil
}
return nil, jwt.ErrSignatureInvalid
})
if err != nil {
logger.Errorf("Token Verify Err: %v", err)
return nil, fmt.Errorf("token invalid")
}
// 如果解析负荷成功并通过签名校验
claims, ok := jwtToken.Claims.(jwt.MapClaims)
if ok && jwtToken.Valid {
return claims, nil
}
return nil, fmt.Errorf("token valid error")
}
// UserInfoRemove 清除访问用户信息缓存
func UserInfoRemove(tokenStr string) (string, error) {
claims, err := UserTokenVerify(tokenStr, "access")
if err != nil {
logger.Errorf("token verify err %v", err)
return "", err
}
info := UserInfoGet(claims)
if info.User.UserName != "" {
// 清除缓存KEY
deviceId := fmt.Sprint(claims[constants.JWT_DEVICE_ID])
tokenKey := constants.CACHE_TOKEN_DEVICE + ":" + deviceId
return info.User.UserName, redis.Del("", tokenKey)
}
return "", fmt.Errorf("token invalid")
}
// UserInfoCreate 生成访问用户信息缓存
func UserInfoCreate(info *UserInfo, deviceFingerprint string, ilobArr [4]string) {
info.DeviceId = deviceFingerprint
// 设置请求用户登录客户端
info.LoginIp = ilobArr[0]
info.LoginLocation = ilobArr[1]
info.OS = ilobArr[2]
info.Browser = ilobArr[3]
expiresIn := time.Duration(parse.Number(config.Get("jwt.expiresIn")))
now := time.Now()
exp := now.Add(expiresIn * time.Minute)
info.LoginTime = now.UnixMilli()
info.ExpireTime = exp.UnixMilli()
// 设置新登录IP和登录时间
info.User.LoginIp = info.LoginIp
info.User.LoginTime = info.LoginTime
info.User.Password = ""
// 登录信息标识缓存
tokenKey := constants.CACHE_TOKEN_DEVICE + ":" + info.DeviceId
jsonBytes, err := json.Marshal(info)
if err != nil {
return
}
_ = redis.Set("", tokenKey, string(jsonBytes), expiresIn*time.Minute)
}
// UserInfoUpdate 更新访问用户信息缓存
func UserInfoUpdate(info UserInfo) {
info.User.Password = ""
// 登录信息标识缓存
tokenKey := constants.CACHE_TOKEN_DEVICE + ":" + info.DeviceId
jsonBytes, err := json.Marshal(info)
if err != nil {
return
}
expiresIn, _ := redis.GetExpire("", tokenKey)
expiration := time.Duration(expiresIn) * time.Second
_ = redis.Set("", tokenKey, string(jsonBytes), expiration)
}
// UserInfoGet 缓存的访问用户信息
func UserInfoGet(claims jwt.MapClaims) UserInfo {
info := UserInfo{}
deviceId := fmt.Sprint(claims[constants.JWT_DEVICE_ID])
tokenKey := constants.CACHE_TOKEN_DEVICE + ":" + deviceId
hasKey, err := redis.Has("", tokenKey)
if hasKey > 0 && err == nil {
infoStr, err := redis.Get("", tokenKey)
if infoStr == "" || err != nil {
return info
}
if err := json.Unmarshal([]byte(infoStr), &info); err != nil {
logger.Errorf("info json err : %v", err)
return info
}
}
return info
}

View File

@@ -0,0 +1,31 @@
package crypto
import (
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
)
// SHA256ToBase64 编码字符串
func SHA256ToBase64(str string) string {
hash := sha256.Sum256([]byte(str))
return base64.URLEncoding.EncodeToString(hash[:])
}
// SHA256Hmac HMAC-SHA256算法
func SHA256Hmac(key string, data string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
// MD5 md5加密
func MD5(str string) (md5str string) {
data := []byte(str)
has := md5.Sum(data)
md5str = fmt.Sprintf("%x", has)
return md5str
}