Golang 中使用 JWT 做用户认证
常见的认证方式
一般用户认证主流的方式大致上分为基于 session 和基于 token 这两种。
基于 sesion 的认证方式
- 用户向服务器发送用户名和密码。
- 服务器验证通过后,在当前对话(sesion)里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个 session_id,写入用户的 Cookie 或其他存储。
- 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
- 用户退出登录,服务器将对应 session_id 的数据清除。
这种方式服务端需要将 session_id 及相关的数据保存起来,在接收到用户请求时进行校验,比如可以存储到 Redis 中。
基于 token 的认证方式
- 用户向服务器发送用户名和密码。
- 服务器将相关数据,比如用户 ID,认证有效期等信息签名后生成 token 返回给客户端。
- 客户端将 token 写入本地存储。
- 用户随后的每一次请求,都将 token 附加到 header 中。
- 服务端获取到用户请求的 header,拿到用户数据并且做签名校验,如果校验成功则说明数据没有被篡改,是有效的,确认 token 在有效期内,用户数据就是有效的。
jwt 是基于 token 的认证方式的一种。这里我们使用 jwt-go 在 Golang 项目中使用 jwt。以下代码均为示例代码,部分内容有所删减,仅供参考。
生成 Token
服务器端需要提供一个登录接口用于用户登录。客户端提供用户名和密码,服务器端进行校验,如果校验通过,则生成 Token 返回给客户端。
import (
jwt "github.com/dgrijalva/jwt-go"
)
func GenerateToken(uid string, role int, expireDuration time.Duration) (string, error) {
expire := time.Now().Add(expireDuration)
// 将 uid,用户角色, 过期时间作为数据写入 token 中
token := jwt.NewWithClaims(jwt.SigningMethodHS256, util.LoginClaims{
Uid: uid,
Role: role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expire.Unix(),
},
})
// SecretKey 用于对用户数据进行签名,不能暴露
return token.SignedString([]byte(util.SecretKey))
}
func (ctl *LoginController) Login(rw http.ResponseWriter, req *http.Request) {
var u loginRequest
json.NewDecoder(req.Body).Decode(&u)
// 将用户传入的用户名和密码和数据库中的进行比对
user, err := ctl.db.GetUserByName(req.Context(), u.User)
if err != nil {
log.Warn("get user from db by name error: %v", err)
httputil.Error(rw, errors.ErrInternal)
return
}
if common.EncodePassowrd(u.Password, u.User) != user.Password {
log.Warn("name [%s] password incorrent", u.User)
httputil.Error(rw, errors.ErrLoginFailed)
return
}
// 生成返回给用户的 token
token, err := GenerateToken(user.UID, user.Role, 3*24*time.Hour)
if err != nil {
log.Warn("name [%s] generateToken error: %v", u.User, err)
httputil.Error(rw, errors.ErrInternal)
return
}
res := struct {
Token string `json:"token"`
}{
Token: token,
}
httputil.Reply(rw, &res)
}
校验 Token
这里要求客户端每次将通过登录接口获取到的 token 设置在发送请求的 Authorization
header 中。
func (a *AuthFilter) Filter(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
tokenStr := req.Header.Get("Authorization")
token, err := jwt.ParseWithClaims(tokenStr, &util.LoginClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(util.SecretKey), nil
}
if err != nil {
httputil.Error(rw, errors.ErrUnauthorized)
return
}
if claims, ok := token.Claims.(*util.LoginClaims); ok && token.Valid {
log.Infof("uid: %s, role: %v", claims.Uid, claims.Role)
} else {
httputil.Error(rw, errors.ErrUnauthorized)
return
}
next.ServeHTTP(rw, req)
}
}
注意点
- 由于 jwt 返回的 Token 中的数据仅做了 Base64 处理,没有加密,所以不应放入重要的信息。
- jwt Token 由于是无状态的,任何获取到此 Token 的人都可以访问,所以为了减少盗用,可以将 Token 有效期设置短一些。对一些重要的操作,尽量再次进行认证。
- 网站尽量使用 HTTPS,可以减少 Token 的泄漏。