fix all issues

This commit is contained in:
2025-12-04 10:59:46 +08:00
parent e4946b7ad7
commit ca49e8e24b
4 changed files with 184 additions and 86 deletions
+43 -3
View File
@@ -2,13 +2,33 @@ package handlers
import (
"authorization/helper"
"authorization/middleware"
"authorization/models"
"authorization/services"
"encoding/json"
"net/http"
)
// AuthorizeHandler godoc
// @Summary Check user authorization
// @Description Validates if a user has permission to perform an action on a resource
// @Tags authorization
// @Accept json
// @Produce json
// @Param request body models.AuthorizationRequest true "Authorization request"
// @Success 200 {object} models.AuthorizationResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Security BearerToken
// @Router /v1/auth/check [post]
func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
// Get claims from JWT middleware
claims, ok := middleware.GetClaims(r)
if !ok {
helper.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var request models.AuthorizationRequest
@@ -18,9 +38,29 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
return
}
allowed := services.Authorize()
if !allowed {
helper.RespondWithError(w, http.StatusForbidden, "Access denied")
// Validate request
if request.UserID == "" || request.Resource == "" || request.Action == "" {
helper.RespondWithError(w, http.StatusBadRequest, "Missing required fields")
return
}
allowed, reason := services.Authorize(claims, &request)
if !allowed {
response := models.AuthorizationResponse{
Allowed: false,
Reason: reason,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(response)
return
}
// Success response
response := models.AuthorizationResponse{
Allowed: true,
Reason: "Access granted",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
+7
View File
@@ -179,6 +179,13 @@ func main() {
log.Fatal("GO_ENV is not set in main. Please set the GO_ENV environment variable.")
}
// Validate JWT_KEY is set
jwtKey := os.Getenv("JWT_KEY")
if jwtKey == "" {
log.Fatal("JWT_KEY is not set. Please set the JWT_KEY environment variable.")
}
log.Println("JWT_KEY loaded successfully")
DSN := os.Getenv("DSN")
if DSN == "" {
log.Fatal("Sentry DSN is not set. Please set the DSN environment variable.")
+97 -80
View File
@@ -31,7 +31,6 @@ var (
jwtSecretError error
// Pre-allocate error messages to avoid repeated allocations
errMissingAuth = "missing authorization header"
errInvalidAuthFormat = "invalid authorization header format"
errInvalidToken = "Invalid token"
errExpiredToken = "Invalid or expired token"
@@ -74,103 +73,121 @@ func cleanExpiredTokens() {
}
}
// extractBearerToken extracts token from Authorization header
func extractBearerToken(authHeader string) (string, bool) {
if authHeader == "" || len(authHeader) < 8 || authHeader[:7] != "Bearer " {
return "", false
}
return authHeader[7:], true
}
// checkTokenCache retrieves token from cache if valid
func checkTokenCache(tokenString string) (*models.Claims, bool) {
tokenCacheMutex.RLock()
defer tokenCacheMutex.RUnlock()
cached, exists := tokenCache[tokenString]
if !exists {
return nil, false
}
if time.Now().Before(cached.ExpiresAt) {
return cached.Claims, true
}
// Token expired, will be cleaned up later
return nil, false
}
// removeExpiredCacheEntry removes a single expired token from cache
func removeExpiredCacheEntry(tokenString string) {
tokenCacheMutex.Lock()
defer tokenCacheMutex.Unlock()
delete(tokenCache, tokenString)
}
// parseAndValidateToken parses JWT token and validates it
func parseAndValidateToken(tokenString string) (*models.Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &models.Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return getJWTSecret()
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
claims, ok := token.Claims.(*models.Claims)
if !ok {
return nil, fmt.Errorf("invalid claims")
}
return claims, nil
}
// cacheToken stores validated token in cache
func cacheToken(tokenString string, claims *models.Claims) {
expiresAt := time.Now().Add(5 * time.Minute)
if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(expiresAt) {
expiresAt = claims.ExpiresAt.Time
}
tokenCacheMutex.Lock()
defer tokenCacheMutex.Unlock()
// Limit cache size
if len(tokenCache) > 10000000 {
count := 0
for k := range tokenCache {
delete(tokenCache, k)
count++
if count >= 1000000 {
break
}
}
}
tokenCache[tokenString] = &models.CacheEntry{
Claims: claims,
ExpiresAt: expiresAt,
}
}
// JWTAuth is a middleware that validates JWT tokens with caching for high-frequency requests
func JWTAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get the Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
// Extract token from header
tokenString, ok := extractBearerToken(r.Header.Get("Authorization"))
if !ok {
helper.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
// Fast path: check if header has Bearer prefix without allocation
if len(authHeader) < 8 || authHeader[:7] != "Bearer " {
helper.RespondWithError(w, http.StatusUnauthorized, errInvalidAuthFormat)
// Check cache first
if claims, found := checkTokenCache(tokenString); found {
ctx := buildContext(r.Context(), claims)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
tokenString := authHeader[7:] // Skip "Bearer " without strings.Split allocation
// Check cache first (read lock)
tokenCacheMutex.RLock()
if cached, exists := tokenCache[tokenString]; exists {
if time.Now().Before(cached.ExpiresAt) {
claims := cached.Claims
tokenCacheMutex.RUnlock()
// Add claims to context and proceed
ctx := buildContext(r.Context(), claims)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// Token expired in cache, remove it
tokenCacheMutex.RUnlock()
tokenCacheMutex.Lock()
delete(tokenCache, tokenString)
tokenCacheMutex.Unlock()
} else {
tokenCacheMutex.RUnlock()
}
// Parse and validate the token
token, err := jwt.ParseWithClaims(tokenString, &models.Claims{}, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Get cached JWT secret
return getJWTSecret()
})
// Parse and validate token
claims, err := parseAndValidateToken(tokenString)
if err != nil {
helper.RespondWithError(w, http.StatusUnauthorized, errExpiredToken)
return
}
// Check if token is valid
if !token.Valid {
helper.RespondWithError(w, http.StatusUnauthorized, errInvalidToken)
return
}
// Extract claims
claims, ok := token.Claims.(*models.Claims)
if !ok {
helper.RespondWithError(w, http.StatusUnauthorized, errInvalidClaims)
return
}
// Cache the validated token
expiresAt := time.Now().Add(5 * time.Minute) // Cache for 5 minutes
if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(expiresAt) {
expiresAt = claims.ExpiresAt.Time
}
cacheToken(tokenString, claims)
tokenCacheMutex.Lock()
// Limit cache size to prevent memory issues
if len(tokenCache) > 10000000 {
// Remove oldest 10% when cache is full
count := 0
for k := range tokenCache {
delete(tokenCache, k)
count++
if count >= 1000000 {
break
}
}
}
tokenCache[tokenString] = &models.CacheEntry{
Claims: claims,
ExpiresAt: expiresAt,
}
tokenCacheMutex.Unlock()
// Add claims to request context
// Add claims to context and proceed
ctx := buildContext(r.Context(), claims)
// Call the next handler with updated context
next.ServeHTTP(w, r.WithContext(ctx))
}
}
+37 -3
View File
@@ -1,6 +1,40 @@
package services
func Authorize() bool {
// Authorization logic here
return true
import (
"authorization/models"
"strings"
)
// Authorize checks if the user has permission to perform the action on the resource
func Authorize(claims *models.Claims, request *models.AuthorizationRequest) (bool, string) {
// Verify the user ID matches the JWT claims
if claims.UserID != request.UserID {
return false, "User ID mismatch"
}
// Admin role has access to everything
if strings.ToLower(claims.Role) == "admin" {
return true, "Admin access granted"
}
// Add your custom authorization logic here
// Example: Role-based access control
switch strings.ToLower(claims.Role) {
case "user":
// Users can only read their own resources
if request.Action == "read" && strings.Contains(request.Resource, claims.UserID) {
return true, "User read access granted"
}
return false, "Insufficient permissions"
case "moderator":
// Moderators can read and update
if request.Action == "read" || request.Action == "update" {
return true, "Moderator access granted"
}
return false, "Moderators cannot perform this action"
default:
return false, "Unknown role"
}
}