场景描述
给你一个场景,一个用户登录之后,进入到主页,但是HTTP是无连接的,所以服务器这个时候根本不知道进入到这个页面的是谁,那么你要怎么解决这个问题呢?
一般有两种方法,一种是 Cookie+Session 、另一种是 JWT+Token、甚至可以见到两者都用的情况。
Token是什么
Token = 登录成功后的"身份证"
在客户端登录成功后、服务端生成一个token 返回给客户端、客户端以后每次请求都带上它、然后服务端通过 token 判断:你是谁你有没有过期你有没有权限。
JWT+token
JWT 是一种 Token 的 **实现方式 / 规范 ,**是目前最常用的方式。
它规定了:Token 长什么样(Header.Payload.Signature)、怎么签名、怎么校验、里面可以放哪些字段(claims)
他包含三个部分:
Header(头部):说明签名算法,比如 HS256
Payload(负载):用户信息 / claims
Signature(签名):防篡改
其中负载里面的claims如下:
Go
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
IsAdmin int `json:"is_admin"`
// 上面三条都是你自己定义的,需要用Token来传的用户信息
jwt.RegisteredClaims
}
处理流程+代码实现
1. 用户登录
校验用户名/密码
生成 Token(通常是 JWT)
2. 客户端保存 Token
放在**
Authorization: Bearer <token>**里(这是个标准的请求头)3. 客户端访问受保护接口
请求头携带 Token (
Authorization)3. 服务端验证 Token
是否存在
是否被篡改
是否过期
是否有权限
5. 通过 → 继续处理 失败 → 返回
401 / 403
准备工作
从 gIthub 拉取jwt包
Go
"github.com/golang-jwt/jwt/v5"
创建 jwt.go
Go
package utils
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 这是用来给 JWT Token 签名的密钥,用于验证 Token 的真实性和完整性,可以从生产环境获取,我这里直接设定了
var jwtKey = []byte("secret_key_123456")
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
IsAdmin int `json:"is_admin"`
jwt.RegisteredClaims
}
// 生成 token
func GenerateToken(userID int, username string, isAdmin int) (string, error) {
claims := &Claims{
UserID: userID,
Username: username,
IsAdmin: isAdmin, // 前三个字段代表token代表什么
RegisteredClaims: jwt.RegisteredClaims{ // 标准字段
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 过期时间
IssuedAt: jwt.NewNumericDate(time.Now()), // 什么时候签发
Issuer: "go_web", // 谁签发的
},
}
// header+payload生成,jwtKey签名最终得到token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}
// 解析 token
func ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("token无效")
}
创建 中间件
Go
package middleware
import (
"context"
"go_web/response"
"go_web/utils"
"net/http"
"strings"
)
type contextKey string
const ClaimsKey = contextKey("claims")
type ClaimsContext struct {
UserID int
Username string
IsAdmin int
}
func JwtMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" {
response.WriteJson(w, 401, 1, "请先登录")
return
}
token := strings.TrimSpace(auth)
claims, err := utils.ParseToken(token)
if err != nil {
response.WriteJson(w, 401, 1, "token无效")
return
}
ctx := context.WithValue(r.Context(), ClaimsKey, &ClaimsContext{
UserID: claims.UserID,
Username: claims.Username,
IsAdmin: claims.IsAdmin,
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
路由器使用中间件
Go
apiMux := http.NewServeMux()
apiMux.Handle("/users", middleware.JwtMiddleware(http.HandlerFunc(userController.Users)))
前端登录 + 保存Token
javascript
try {
// 向服务器发送登录请求
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});
// 得到服务端返回的信息
const data = await res.json();
// code = 0 登录成功, 存在data.data, 且data.token存在
if (data.code === 0 && data.data && data.data.token) {
// 保存 Token
localStorage.setItem("token", data.data.token);
location.href = "/index";
} else {
localStorage.removeItem("token");
alert(data.msg || "登录失败");
}
} catch (e) {
alert("登录失败,请重试");
}
Token后端登录验证 + 返回Token
建议分层处理:
| 分层 | 职责 | 对应你的代码 |
|---|---|---|
| 控制器层(Controller) | 接收前端请求、解析参数、返回响应、处理 HTTP 相关逻辑(如 Cookie) | AuthController + LoginAPI 方法 |
| 服务层(Service) | 封装核心业务逻辑(如用户校验、密码对比、权限判断) | service.AuthService + Login 方法 |
| 数据访问层(DAO) | 操作数据库(如查询用户、验证用户名密码) | (隐藏在 AuthService 内部) |
这里是一个链式结构:
Go
type AuthController struct {
AuthService *service.AuthService
}
type AuthService struct {
Repo *repository.Repo
}
type Repo struct{}
结构关系图:(上层依赖下层,下层不依赖上层)
HTTP Request
↓
AuthController
↓
AuthService
↓
Repo
↓
DB / Redis
这种设计目的:
职责清晰
-
Controller:接请求
-
Service:业务
-
Repo:数据
易维护
-
改数据库 → 只动 Repo
-
改业务规则 → 只动 Service
-
改接口 → 只动 Controller
易测试
-
Service 可以 mock Repo
-
Controller 可以 mock Service
然后看登录处理:
Go
func (c *AuthController) Login(w http.ResponseWriter, r *http.Request) {
var loginUser model.AuthUser
err := json.NewDecoder(r.Body).Decode(&loginUser)
if err != nil {
logger.Error("JSON 解码失败:%v | 原始请求体:%v", err, r.Body)
response.WriteJson(w, 400, 1, "JSON格式错误")
return
}
user, err := c.AuthService.Login(loginUser.Username, loginUser.Password)
if err != nil {
logger.Info("%s 登录失败:%v", loginUser.Username, err)
response.WriteJson(w, 200, 1, err.Error())
return
}
// 用数据库里的 is_admin 生成 JWT
token, err := utils.GenerateToken(
user.ID,
user.Username,
user.IsAdmin,
)
if err != nil {
response.WriteJson(w, 500, 1, "生成 token 失败")
return
}
logger.Info("%s 登录成功,is_admin=%d", user.Username, user.IsAdmin)
response.WriteData(w, map[string]string{
"token": token,
})
}
客户端携带Token请求后端
Go
async function authFetch(url, options = {}) {
const token = localStorage.getItem("token");
// 默认 headers
const headers = {
...(options.headers || {}),
"Authorization": token || "",
};
// 如果是 JSON body,自动补 Content-Type
if (options.body && !(options.body instanceof FormData)) {
headers["Content-Type"] = headers["Content-Type"] || "application/json";
}
let response;
try {
response = await fetch(url, {
...options,
headers,
});
} catch (err) {
alert("网络异常,请检查网络连接");
throw err;
}
// token 失效 / 未登录
if (response.status === 401) {
localStorage.removeItem("token");
alert("登录已过期,请重新登录");
window.location.href = "/login";
return;
}
// 非 JSON(例如 204)
const contentType = response.headers.get("Content-Type") || "";
if (!contentType.includes("application/json")) {
return null;
}
const result = await response.json();
return result;
}
/** ==== JWT + Token + 权限控制 ==== **/
const token = localStorage.getItem('token');
if (!token) {
location.href = "/login";
}
// 解析 token
function parseJwt(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonStr = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonStr);
}
后端验证
Go
func (c *UserController) Users(w http.ResponseWriter, r *http.Request) {
// 不是重复校验token,而是防止路由配置不经过中间件,一层保护
claimsValue := r.Context().Value(middleware.ClaimsKey)
claims, ok := claimsValue.(*middleware.ClaimsContext)
if !ok {
response.WriteJson(w, 401, 1, "未认证")
return
}
switch r.Method {
case http.MethodGet:
c.Search(w, r)
case http.MethodPost:
if claims.IsAdmin != 1 {
response.WriteJson(w, 403, 1, "没有权限")
return
}
c.AddUser(w, r)
case http.MethodPut:
if claims.IsAdmin != 1 {
response.WriteJson(w, 403, 1, "没有权限")
return
}
c.UpdateUser(w, r, claims)
case http.MethodDelete:
if claims.IsAdmin != 1 {
response.WriteJson(w, 403, 1, "没有权限")
return
}
c.DeleteUser(w, r, claims)
}
}