Update
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
---
|
||||
tags: [go, auth, security, jwt, session, authorization, assignment]
|
||||
create time: 2026-04-17
|
||||
---
|
||||
|
||||
# Go 语言用户认证实现指南
|
||||
|
||||
## 概述
|
||||
|
||||
用户认证是 Web 应用的核心安全机制。本文介绍在 Go 中实现用户认证的常见模式和最佳实践。
|
||||
|
||||
## 认证方式流程
|
||||
|
||||
### Session-Based 认证
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant S as Server
|
||||
participant DB as Database
|
||||
|
||||
C->>S: POST /login (username, password)
|
||||
S->>DB: 验证凭证
|
||||
DB-->>S: 用户信息
|
||||
S->>S: 创建 Session
|
||||
S-->>C: Set-Cookie: session_id=xxx
|
||||
Note over C: 存储到浏览器
|
||||
|
||||
C->>S: GET /protected + Cookie
|
||||
S->>DB: 根据 session_id 查询
|
||||
DB-->>S: Session 数据
|
||||
S-->>C: 返回受保护资源
|
||||
```
|
||||
|
||||
### JWT Token 认证
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant S as Server
|
||||
participant Token as JWT Token
|
||||
|
||||
C->>S: POST /login (credentials)
|
||||
S->>S: 验证用户
|
||||
S->>S: 生成 JWT
|
||||
Token->>S: eyJhbGciOiJIUzI1NiIs...
|
||||
S-->>C: {access_token: "...", refresh_token: "..."}
|
||||
|
||||
C->>S: GET /api + Authorization: Bearer {token}
|
||||
S->>S: 验证 Token 签名和过期
|
||||
S-->>C: 返回 API 数据
|
||||
```
|
||||
|
||||
## 对比分析
|
||||
|
||||
| 特性 | Session | JWT |
|
||||
|-----|--------|-----|
|
||||
| 存储位置 | 服务器 | 客户端 |
|
||||
| 状态性 | 有状态 | 无状态 |
|
||||
| 分布式支持 | 需要共享机制 | 原生支持 |
|
||||
| 可撤销性 | 容易 | 困难 |
|
||||
| 适用场景 | 传统网页 | RESTful API |
|
||||
|
||||
## OAuth 2.0 流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant A as App
|
||||
participant P as Provider (Google/GitHub)
|
||||
|
||||
U->>A: 点击登录
|
||||
A->>P: 重定向到授权页
|
||||
U->>P: 同意授权
|
||||
P->>A: 重定向 + code
|
||||
A->>P: 用 code 换取 access_token
|
||||
P-->>A: {access_token, refresh_token}
|
||||
A->>P: 使用 token 获取用户信息
|
||||
P-->>A: 用户信息
|
||||
A-->>U: 登录成功
|
||||
```
|
||||
|
||||
## Go 语言实现方案
|
||||
|
||||
### Session-Based 实现
|
||||
|
||||
#### 使用 gorilla/sessions
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
var store = sessions.NewCookieStore([]byte("secret-key"))
|
||||
|
||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 验证用户凭证
|
||||
if validateUser(r) {
|
||||
session, _ := store.Get(r, "session-name")
|
||||
session.Values["user_id"] = "123"
|
||||
session.Values["authenticated"] = true
|
||||
session.Save(r, w)
|
||||
}
|
||||
}
|
||||
|
||||
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := store.Get(r, "session-name")
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JWT Implementation
|
||||
|
||||
#### 标准流程
|
||||
|
||||
```go
|
||||
package authenticator
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type JWTAuthenticator struct {
|
||||
secretKey []byte
|
||||
}
|
||||
|
||||
func NewJWTAuthenticator(secretKey string) *JWTAuthenticator {
|
||||
return &JWTAuthenticator{
|
||||
secretKey: []byte(secretKey),
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 Token
|
||||
func (j *JWTAuthenticator) GenerateToken(userID, username string) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(j.secretKey)
|
||||
}
|
||||
|
||||
// 验证 Token
|
||||
func (j *JWTAuthenticator) ValidateToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return j.secretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
|
||||
// 中间件
|
||||
func (j *JWTAuthenticator) AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := j.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存入上下文
|
||||
ctx := context.WithValue(r.Context(), "user", claims)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth 2.0 集成
|
||||
|
||||
```go
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
var (
|
||||
googleOauthConfig = &oauth2.Config{
|
||||
RedirectURL: "http://localhost:8080/callback",
|
||||
ClientID: "your-client-id",
|
||||
ClientSecret: "your-client-secret",
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
)
|
||||
|
||||
func HandleGoogleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
url := googleOauthConfig.AuthCodeURL("state-token")
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func HandleGoogleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
token, err := googleOauthConfig.Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to exchange token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 token 获取用户信息
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## JWT 结构
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[JWT Token] --> B[Header]
|
||||
A --> C[Payload]
|
||||
A --> D[Signature]
|
||||
|
||||
B --> B1[alg: HS256]
|
||||
B --> B2[typ: JWT]
|
||||
|
||||
C --> C1[user_id]
|
||||
C --> C2[exp: 时间戳]
|
||||
C --> C3[iat: 颁发时间]
|
||||
|
||||
D --> D1[Header + Payload]
|
||||
D --> D2[Secret Key]
|
||||
```
|
||||
|
||||
## Authorization(授权)
|
||||
|
||||
**认证 vs 授权**:
|
||||
- **认证(Authentication)**:你是谁?解决身份验证问题
|
||||
- **授权(Authorization)**:你能做什么?解决权限控制问题
|
||||
|
||||
### Authorization 头格式
|
||||
|
||||
| 方式 | 格式 | 示例 |
|
||||
|-----|------|------|
|
||||
| Bearer Token | `Bearer <token>` | `Bearer eyJhbGciOiJIUzI1NiIs...` |
|
||||
| Basic Auth | `Basic <credentials>` | `Basic YWxhZGRpbjpvcGVuc2VzYW1l` |
|
||||
| API Key | `ApiKey <key>` | `ApiKey abc123xyz` |
|
||||
|
||||
### 在 Go 中处理 Authorization
|
||||
|
||||
```go
|
||||
// Bearer Token 解析
|
||||
func parseBearerToken(header string) (string, error) {
|
||||
parts := strings.SplitN(header, " ", 2)
|
||||
if parts[0] != "Bearer" || len(parts) < 2 {
|
||||
return "", errors.New("invalid authorization format")
|
||||
}
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
// Basic Auth 解析
|
||||
func parseBasicAuth(header string) (username, password string, err error) {
|
||||
parts := strings.SplitN(header, " ", 2)
|
||||
if parts[0] != "Basic" || len(parts) < 2 {
|
||||
return "", "", errors.New("invalid basic auth format")
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
credentials := string(decoded)
|
||||
idx := strings.Index(credentials, ":")
|
||||
if idx == -1 {
|
||||
return "", "", errors.New("invalid credentials format")
|
||||
}
|
||||
|
||||
return credentials[:idx], credentials[idx+1:], nil
|
||||
}
|
||||
```
|
||||
|
||||
**详细信息**:💡 详见 [[CS/NET/Authorization]] - Authorization 机制详解(RBAC、ABAC、策略引擎)
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
### 密码安全
|
||||
|
||||
```go
|
||||
package auth
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// 加密密码
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
```
|
||||
|
||||
### 密钥管理
|
||||
|
||||
- ✅ 使用环境变量存储密钥
|
||||
- ✅ 使用专业密钥管理服务(HashiCorp Vault)
|
||||
- ❌ 不将密钥写入代码库
|
||||
- ❌ 不在日志中打印密钥
|
||||
|
||||
### HTTPS 强制
|
||||
|
||||
```go
|
||||
func RedirectHTTPS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Scheme != "https" {
|
||||
httpsURL := "https://" + r.Host + r.URL.RequestURI()
|
||||
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 常见安全威胁与防护
|
||||
|
||||
### CSRF 攻击流程与防护
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as 攻击者
|
||||
participant U as 用户
|
||||
participant S as 服务器
|
||||
|
||||
%%% 受害场景 %%%
|
||||
U->>U: 已登录网站 S,持有 Cookie
|
||||
A->>U: 诱导点击恶意链接
|
||||
U->>S: POST /transfer (自动携带 Cookie)
|
||||
S-->>U: 转账成功 ⚠️
|
||||
|
||||
%%% 防护方案 %%%
|
||||
Note over S: CSRF Token 模式
|
||||
U->>S: GET /form
|
||||
S-->>U: HTML + CSRF Token
|
||||
U->>S: POST + Token in data
|
||||
S->>S: 验证 Token
|
||||
S-->>U: 请求通过 ✓
|
||||
```
|
||||
|
||||
### 威胁防护清单
|
||||
|
||||
| 威胁类型 | 防护措施 |
|
||||
|---------|---------|
|
||||
| SQL 注入 | 使用参数化查询、ORM |
|
||||
| XSS 攻击 | 输入验证、输出编码 |
|
||||
| CSRF 攻击 | CSRF Token、SameSite Cookie |
|
||||
| 重放攻击 | Timestamp、Nonce 验证 |
|
||||
| Token 窃取 | HTTPS、短期 Token、刷新机制 |
|
||||
|
||||
## 性能优化
|
||||
|
||||
### JWT 优化
|
||||
|
||||
```go
|
||||
// 1. 缓存已验证 Token
|
||||
type CacheAuthenticator struct {
|
||||
jwt *JWTAuthenticator
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
// 2. 使用 Key ID 轮换密钥
|
||||
type KeyRotation struct {
|
||||
currentKey []byte
|
||||
oldKeys [][]byte
|
||||
}
|
||||
|
||||
// 3. 缩短 Token 生命周期,使用 Refresh Token
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
```
|
||||
|
||||
## 推荐库
|
||||
|
||||
| 库名 | 用途 | 特点 |
|
||||
|-----|------|------|
|
||||
| gorilla/sessions | Session 管理 | 功能完善,支持多种存储 |
|
||||
| golang-jwt/jwt | JWT 处理 | 轻量级,标准实现 |
|
||||
| golang.org/x/oauth2 | OAuth 2.0 | 官方实现,支持主流平台 |
|
||||
| golang.org/x/crypto | 密码加密 | 包含 bcrypt、scrypt 等算法 |
|
||||
|
||||
## 学习资源
|
||||
|
||||
- [JWT 官方文档](https://jwt.io/)
|
||||
- [OWASP 认证备忘单](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
||||
- [Go Web Examples - Authentication](https://gowebexamples.com/routes/)
|
||||
|
||||
## 相关笔记
|
||||
|
||||
- [[CS/NET/Authorization]] - Authorization 授权机制详解
|
||||
- [[CS/NET/HTTPS]] - HTTPS 原理
|
||||
- [[CS/OS/进程线程]] - 并发安全
|
||||
- [[CS/DB/SQL安全]] - SQL 注入防护
|
||||
Reference in New Issue
Block a user