JWT
JWT 场景
最最常见场景:接口认证、登录验证
实现目标
- 实现用户登录验证
- 实现Access Token、Refresh Token,在Refresh Token未过期情况下,不会登出,前端可以实现续签Access Token逻辑
- 关键API Endpoint进行权限隔离、实现 简单保护
实现过程
Gin中间件
package middleware
import (
hrequest "gitlab-ecs.litatom.com/infra/hermes/request"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"gitlab-ecs.litatom.com/infra/saplat/handler/request"
)
const (
GinHeaderAuthorizationKey = "Authorization"
)
var JWTKey = []byte("my_secret_key")
// JwtMiddleware 检查JWT Token的中间件
func JWTMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
var (
req = hrequest.HGin{C: c}
tokenString = c.GetHeader(GinHeaderAuthorizationKey)
)
// 如果Header中没有Token,则返回错误
if tokenString == "" {
req.FailResponse("Authorization header is required")
c.Abort()
return
}
claims := &request.JWTTokenClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return JWTKey, nil
})
if err != nil || !token.Valid {
req.FailResponse("Invalid or expired token")
c.Abort()
return
}
c.Next()
}
}
Gin Router
RegisterOAUTHV2 := r.Group("/api/oauth/v2")
authR.RegisterOAUTHRouterV2(RegisterOAUTHV2)
func RegisterOAUTHRouterV2(rp *gin.RouterGroup) {
rp.POST("/token/create", auth.OAUTHService.AuthV2)
rp.POST("/token/refresh", auth.OAUTHService.RefreshTokenV2)
}
OAuth逻辑
创建JWT Token「Access Token、Refresh Token」
// CreateToken 创建Access Token和Refresh Token
func (os *oauthService) CreateTokenV2(c *gin.Context) {
var (
req = hrequest.HGin{C: c}
)
accessToken, err := os.CreateAccessTokenV2(c)
if err != nil {
return
}
refreshToken, err := os.CreateRefreshTokenV2(c)
if err != nil {
return
}
req.Response(200, 0, "success", response.JWTTokenResponse{AccessToken: accessToken, RefreshToken: refreshToken})
}
// CreateToken 创建Access Token
func (os *oauthService) CreateRefreshTokenV2(c *gin.Context) (string, error) {
v, b := c.Get(GinCredentialsKey)
if !b {
fmt.Println("get username err: nil ")
}
expirationTime := time.Now().Add(conf.JWTRefreshTokenExpirationTime)
refreshClaims := &request.JWTTokenClaims{
Username: v.(request.JWTTokenCredentials).Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString(JWTKey)
if err != nil {
fmt.Printf("failed to create refresh token: %s\n", err.Error())
}
return refreshTokenString, nil
}
// CreateToken 创建Refresh Token
func (os *oauthService) CreateAccessTokenV2(c *gin.Context) (string, error) {
v, b := c.Get(GinCredentialsKey)
if !b {
fmt.Println("get username err: nil ")
}
expirationTime := time.Now().Add(conf.JWTAccessTokenExpirationTime)
claims := &request.JWTTokenClaims{
Username: v.(request.JWTTokenCredentials).Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
Issuer: conf.AppID, // 签发人
// ID: "1",
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
accessTokenString, err := accessToken.SignedString(JWTKey)
if err != nil {
fmt.Printf("failed to create access token: %s\n", err.Error())
}
return accessTokenString, nil
}
登录判断
// Auth 用于登录并生成JWT Token
func (os *oauthService) AuthV2(c *gin.Context) {
var (
creds request.JWTTokenCredentials
req = hrequest.HGin{C: c}
)
if err := c.ShouldBindJSON(&creds); err != nil {
req.FailResponse(err.Error())
return
}
// 在这里可以添加验证用户名和密码的逻辑
// 假设验证成功,生成Token
c.Set(GinCredentialsKey, creds)
at, err := os.CreateAccessTokenV2(c)
if err != nil {
req.FailResponse(err.Error())
return
}
rt, err := os.CreateRefreshTokenV2(c)
if err != nil {
req.FailResponse(err.Error())
return
}
req.Response(200, 0, "success", response.JWTTokenResponse{AccessToken: at, RefreshToken: rt})
}
刷新Token处理
- 判断用户刷新提交的身份和待刷新Access Token身份是否一致
- 确保Access有效性、时效性
// Refresh 处理Token刷新
func (os *oauthService) RefreshTokenV2(c *gin.Context) {
var (
req = hrequest.HGin{C: c}
rtr request.JWTRefreshTokenRequest
)
// requestedUsername := c.PostForm("username") // 假设前端发送的刷新请求中包含了用户名
if err := c.BindJSON(&rtr); err != nil {
req.FailResponse(err.Error())
}
// 验证Refresh Token中的用户名是否与请求中的用户名匹配
// if rtr.Credentials.Username != requestedUsername {
// req.FailResponse("Username mismatch")
// return
// }
refreshClaims := &request.JWTTokenClaims{}
token, err := jwt.ParseWithClaims(rtr.RefreshToken, refreshClaims, func(token *jwt.Token) (interface{}, error) {
return JWTKey, nil
})
if err != nil {
// 解析错误,可能是Refresh Token无效
req.FailResponse("Invalid refresh token")
return
}
if !token.Valid || time.Now().After(refreshClaims.ExpiresAt.Time) {
// Refresh Token无效或已过期
req.FailResponse("Expired or invalid refresh token")
return
}
currentClaims := &request.JWTTokenClaims{}
currentAccessToken := rtr.AccessToken
_, err = jwt.ParseWithClaims(currentAccessToken, currentClaims, func(token *jwt.Token) (interface{}, error) {
return JWTKey, nil
})
// 如果当前Access Token仍然有效,则返回它
if err == nil && time.Now().Before(currentClaims.ExpiresAt.Time) {
req.Response(200, 0, "success", response.JWTTokenResponse{AccessToken: currentAccessToken})
return
}
c.Set(GinCredentialsKey, rtr.Credentials)
// Token验证通过,且未过期,生成新的Access Token
newAccessToken, err := os.CreateAccessTokenV2(c)
if err != nil {
req.FailResponse(err.Error())
return
}
req.Response(200, 0, "success", response.JWTTokenResponse{AccessToken: newAccessToken})
}
全局变量
const (
JWTTokenSecretKey = "my_secret_key" // JWT密钥
JWTAccessTokenExpirationTime = 1 * time.Hour // JWT Access Token有效期
JWTRefreshTokenExpirationTime = 168 * time.Hour // JWT Refresh Token有效期
AppID = "x"
)
引入JWT
// 登录端点,用于生成Token
r.POST("/login", auth.OAUTHService.AuthV2)
// Ping test
r.GET("/ping", middleware.JWTMiddleware(), func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
效果
未携带Token「如未登录状态」
执行登录获取Token
直接调用Token 创建接口
携带Token
刷新Token
若Token未过期,返回当前Token
若Token过期,Refresh未过期,不会登出,会返回新的一次性Access Token 同时会校验申请刷新用户和提交的用户是否一致
Q&A
与前端对接
- 服务端生成的 jwt 返回客户端可以存到 cookie 也可以存到 localStorage 中(相比 cookie 容量大),存在 cookie 中需加上 HttpOnly 的标记,可以防止 XSS) 攻击。
- 尽量用 https 带证书网址访问。
- 传参时,按照约束
前端一般携带 Bearer + Token
golang构建JWT
- Payload支持默认字段,也支持自定义私有字段 jwt.MapClaims
- 标准解析 jwt.ParseWithClaims 、标准字段 jwt.StandardClaims
是前端存储localStorage还是后端发送Cookies
JWT (JSON Web Token) 的使用,确实存在两种主流策略来处理其存储问题,分别是前端存储(如在 localStorage)和通过 HTTP cookies 由服务器端设置。
- 前端存储:
-
当服务端返回JWT后,前端应用可以选择将其存放在
localStorage或sessionStorage中,便于随后的请求中添加到 HTTP 请求头(一般是Authorization请求头)。 -
优点是:易于实现,尤其是在单页应用(SPA)中。这种方式对于单页应用(SPA)比较方便,因为JavaScript代码可以轻松管理Token。
-
缺点是:如前所述,
localStorage容易受到 XSS 攻击。存储在这里的JWT可以通过JavaScript访问到,这意味着如果网站上出现了跨站脚本(XSS)漏洞,攻击者可能能够获取这些Token。
- 后端设置 HTTP-only Cookie:
-
在这种方案中,JWT 由服务端通过设置一个 HTTP-only 的 Cookie 返回给客户端,客户端浏览器会自动处理 Cookie,并在随后的请求中包含这个 Cookie。
-
优点是:增加了安全性,因为这个 JWT 对于前端JavaScript是不可访问的,减少了XSS攻击窃取Token的风险。如果Cookie设置为
Secure,则仅能在HTTPS连接中传输,增加安全性。还可以设置SameSite属性,来进一步加强对跨站请求伪造(CSRF)攻击的防御。 -
缺点是:如果没有正确地设置
SameSite属性,可能会让应用面临CSRF攻击的风险。
从安全的角度来说,服务器端通过设置HTTP-only和Secure属性的Cookie来存储JWT是一种更为推荐的做法。这样做的一个额外好处是,能够利用浏览器的同源策略来增强安全防护,因为HTTP-only Cookie不会被客户端JavaScript访问,同时Secure标记确保仅通过HTTPS发送Cookie。
当然,这也意味着服务端需要负责管理Token的创建、发送、验证以及续订等工作流程。前端应用无需管理JWT的存储,它只需要确保发送请求时浏览器能够包括这个Cookie。
无论采用哪种方法,安全的HTTP通信(如使用HTTPS)都是强烈推荐的,能有效防止MITM(中间人攻击)对通信内容的截取。同时确保您的网站安全,没有XSS、CSRF等漏洞,遵守安全的编码实践。
将JWT存储在Cookies中,确实可以为客户端的Token提供额外的安全层。但是,您依然需要在服务器端进行认证和授权检查,因为通过Cookies只是一种传输认证数据的方式,而不是认证本身。
为了最大化安全性,可以考虑以下做法:
-
使用
HttpOnly和Secure标志的Cookies存储Token。 -
设置合适的
SameSite属性以降低CSRF的风险,如SameSite=Lax或SameSite=Strict。 -
如果使用Cookies存储Token,确保您的应用程序正确地验证和更新了它们,并在不使用时删除Cookies。
-
对Token的生成和验证使用合适的秘钥管理策略,并定期旋转秘钥。
-
不要在Token中存储敏感信息,因为JWT通常可以被客户端读取到。
-
对您的应用应用严格的CORS策略,避免来自不被信任来源的请求。
-
定时刷新Token,限制Token的有效期,并在服务器端验证Token的有效性。