feat(auth): JWT(HS256 stdlib) + RBAC(owner/admin/viewer) + API key + 密码 hash;config 校验
This commit is contained in:
1 parent
7f7cb8fbf7
commit
a3f6f4f687
10 files changed
+1116
No files matched your search
@@ -0,0 +1,99 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// claimsCtxKey is the context key for a validated Claims set by Authorize.
|
||||
type claimsCtxKey struct{}
|
||||
|
||||
// ClaimsFromContext returns the validated Claims stored by Authorize.
|
||||
func ClaimsFromContext(ctx context.Context) (*Claims, bool) {
|
||||
claims, ok := ctx.Value(claimsCtxKey{}).(*Claims)
|
||||
return claims, ok
|
||||
}
|
||||
|
||||
// Authorize returns an HTTP middleware that requires a valid Bearer JWT whose
|
||||
// role satisfies at least one of the required roles. With no required roles it
|
||||
// admits any authenticated call. A missing/invalid/expired token yields 401
|
||||
// (ErrUnauthorized); an authenticated token with insufficient privilege yields
|
||||
// 403 (ErrForbidden). The validated claims are stored in the request context
|
||||
// for downstream handlers.
|
||||
func (s *JWTService) Authorize(required ...Role) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, ok := bearerToken(r)
|
||||
if !ok {
|
||||
s.log.Debug("missing bearer token")
|
||||
writeAuthError(w, http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
claims, err := s.Verify(token)
|
||||
if err != nil {
|
||||
s.log.Debug("token verify failed", "err", err)
|
||||
writeAuthError(w, http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
if !satisfies(claims.Role, required) {
|
||||
s.log.Debug("insufficient role", "role", claims.Role)
|
||||
writeAuthError(w, http.StatusForbidden, ErrForbidden)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), claimsCtxKey{}, claims)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// satisfies reports whether role has enough privilege for at least one of the
|
||||
// required roles. An empty required list means any authenticated role passes.
|
||||
func satisfies(role Role, required []Role) bool {
|
||||
if len(required) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, r := range required {
|
||||
if HasRole(role, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// bearerToken extracts the token from an Authorization: Bearer header.
|
||||
func bearerToken(r *http.Request) (string, bool) {
|
||||
const prefix = "Bearer "
|
||||
h := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(h, prefix) {
|
||||
return "", false
|
||||
}
|
||||
token := strings.TrimSpace(h[len(prefix):])
|
||||
return token, token != ""
|
||||
}
|
||||
|
||||
// writeAuthError writes an {error:{code,message}} JSON response matching the
|
||||
// project's client contract. No token material is ever logged.
|
||||
func writeAuthError(w http.ResponseWriter, status int, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"error": map[string]any{
|
||||
"code": authErrorCode(status),
|
||||
"message": err.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// authErrorCode maps an HTTP status to a stable error code string.
|
||||
func authErrorCode(status int) string {
|
||||
switch status {
|
||||
case http.StatusUnauthorized:
|
||||
return "unauthorized"
|
||||
case http.StatusForbidden:
|
||||
return "forbidden"
|
||||
default:
|
||||
return http.StatusText(status)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user