在前后端分离的项目中, 越来越多的项目采用 JWT 代替传统的 cookie , 这里我们来使用 JWT 结合 Gin 来作为一个登录授权和权限校验.
:key: 什么是 JWT
JWT 的全称叫做 JSON web TOKEN, 在目前前后端系统中使用较多.
JWT 构成
JWT 是由三段构成的. 分别是 HEADER,PAYLOAD,VERIFY SIGNATURE, 它们生成的信息通过 . 分割.
HEADER
header 是由 一个 typ 和 alg 组成, typ 会指明为 JWT, 而 alg 是所使用的加密算法.
- {
- "alg": "HS256",
- "typ": "JWT"
- }
PAYLOAD
payload 是 JWT 的载体, 也就是我们要承载的信息. 这段信息是我们可以自定义的, 可以定义我们要存放什么信息, 那些字段. 该部分信息不宜过多, 它会影响 JWT 生成的大小, 还有就是请勿将敏感数据存入该部分, 该端数据前端是可以解析获取 token 内信息的.
官方给了七个默认字段, 我们可以不全部使用, 也可以加入我们需要的字段.
名称 | 含义 |
---|---|
Audience | 表示 JWT 的受众 |
ExpiresAt | 失效时间 |
Id | 签发编号 |
IssuedAt | 签发时间 |
Issuer | 签发人 |
NotBefore | 生效时间 |
Subject | 主题 |
VERIFY SIGNATURE
这也是 JWT 的最后一段, 该部分是由算法计算完成的.
对刚刚的 header 进行 base64Url 编码, 对 payload 进行 base64Url 编码, 两端完成编码后通过 . 进行连接起来.
base64UrlEncode(header).base64UrlEncode(payload)
完成上述步骤后, 就要通过我们 header 里指定的加密算法对上部分进行加密, 同时我们还要插入我们的一个密钥, 来确保我的 JWT 签发是安全的.
这便是我们的第三部分.
当三部分都完成后, 通过使用 . 将三部分分割, 生成了上图所示的 JWT .
JWT 登录原理
简单的说就是当用户登录的时候, 服务器校验登录名称和密码是否正确, 正确的话, 会生成 JWT 返回给客户端. 客户端获取到 JWT 后要进行保存, 之后的每次请求都会讲 JWT 携带在头部, 每次服务器都会获取头部的 JWT 是否正确, 如果正确则正确执行该请求, 否者验证失败, 重新登录.
:lock:Gin 生成 JWT
go 语言的 JWT 库有很多. https://jwt.io/ 上也给出了很多 . 这里使用 jwt-go
"github.com/dgrijalva/jwt-go"
我们对登录方法进行改造.
- // 省略代码
- expiresTime := time.Now().Unix() + int64(config.OneDayOfHours)
- claims := jwt.StandardClaims{
- Audience: user.Username, // 受众
- ExpiresAt: expiresTime, // 失效时间
- Id: string(user.ID), // 编号
- IssuedAt: time.Now().Unix(), // 签发时间
- Issuer: "gin hello", // 签发人
- NotBefore: time.Now().Unix(), // 生效时间
- Subject: "login", // 主题
- }
- var jwtSecret = []byte(config.Secret)
- tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- // 省略代码
这里的 config.OneDayOfHours 设定了过期时间, 这里设定了一天. 通过 StandardClaims 生成标准的载体, 也就是上文提到的七个字段, 其中 编号设定为 用户 id. 其中的 jwtSecret 是我们设定的密钥,
我们这里通过 HS256 算法生成 tokenClaims , 这就是我们的 HEADER 部分和 PAYLOAD.
token, err := tokenClaims.SignedString(jwtSecret)
这样便生成了我们的 token . 我们要将我们的 token 和 Bearer 拼接在一起, 同时中间用空格隔开.
token = "Bearer"+ token
生成 Bearer Token .
当我们用户进行登录的时候, 就可以通过该片段生成 JWT.
下面是完整代码:
- func CreateJwt(ctx *gin.Context) {
- // 获取用户
- user := &model.User{}
- result := &model.Result{
- Code: 200,
- Message: "登录成功",
- Data: nil,
- }
- if e := ctx.BindJSON(&user); e != nil {
- result.Message = "数据绑定失败"
- result.Code = http.StatusUnauthorized
- ctx.JSON(http.StatusUnauthorized, gin.H{
- "result": result,
- })
- }
- u := user.QueryByUsername()
- if u.Password == user.Password {
- expiresTime := time.Now().Unix() + int64(config.OneDayOfHours)
- claims := jwt.StandardClaims{
- Audience: user.Username, // 受众
- ExpiresAt: expiresTime, // 失效时间
- Id: string(user.ID), // 编号
- IssuedAt: time.Now().Unix(), // 签发时间
- Issuer: "gin hello", // 签发人
- NotBefore: time.Now().Unix(), // 生效时间
- Subject: "login", // 主题
- }
- var jwtSecret = []byte(config.Secret)
- tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- if token, err := tokenClaims.SignedString(jwtSecret); err == nil {
- result.Message = "登录成功"
- result.Data = "Bearer" + token
- result.Code = http.StatusOK
- ctx.JSON(result.Code, gin.H{
- "result": result,
- })
- } else {
- result.Message = "登录失败"
- result.Code = http.StatusOK
- ctx.JSON(result.Code, gin.H{
- "result": result,
- })
- }
- } else {
- result.Message = "登录失败"
- result.Code = http.StatusOK
- ctx.JSON(result.Code, gin.H{
- "result": result,
- })
- }
- }
通过 .http 请求测试, 结果如下
- {
- "result": {
- "code": 200,
- "message": "登录成功",
- "data": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjMiLCJleHAiOjE1NjQ3OTY0MzksImp0aSI6Ilx1MDAwMCIsImlhdCI6MTU2NDc5NjQxOSwiaXNzIjoiZ2luIGhlbGxvIiwibmJmIjoxNTY0Nzk2NDE5LCJzdWIiOiJsb2dpbiJ9.CpacmfBSMgmK2TgrT-KwNB60bsvwgyryGQ0pWZr8laU"
- }
- }
这个便完成了 token 的生成.
:closed_lock_with_key:Gin 校验 Token
那么, 接下来就需要完成 token 的验证.
还记得之前我们验证用户是否授权采用的办法吗? 是的, 在中间件里查看用户 cookie. 同样的方法, 我们这里校验用户 JWT 是否有效.
编写我们的中间件.
新建立 middleware/Auth.go
首先先编写我们的解析 token 方法, parseToken()
- func parseToken(token string) (*jwt.StandardClaims, error) {
- jwtToken, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (i interface{}, e error) {
- return []byte(config.Secret), nil
- })
- if err == nil && jwtToken != nil {
- if claim, ok := jwtToken.Claims.(*jwt.StandardClaims); ok && jwtToken.Valid {
- return claim, nil
- }
- }
- return nil, err
- }
通过传入我们的 token , 来对 token 进行解析.
完整的中间件代码
- func Auth() gin.HandlerFunc {
- return func(context *gin.Context) {
- result := model.Result{
- Code: http.StatusUnauthorized,
- Message: "无法认证, 重新登录",
- Data: nil,
- }
- auth := context.Request.Header.Get("Authorization")
- if len(auth) == 0 {
- context.Abort()
- context.JSON(http.StatusUnauthorized, gin.H{
- "result": result,
- })
- }
- auth = strings.Fields(auth)[1]
- // 校验 token
- _, err := parseToken(auth)
- if err != nil {
- context.Abort()
- result.Message = "token 过期" + err.Error()
- context.JSON(http.StatusUnauthorized, gin.H{
- "result": result,
- })
- } else {
- println("token 正确")
- }
- context.Next()
- }
- }
首先在请求头获取 token , 然后对先把 token 进行解析, 将 Bearer 和 JWT 拆分出来, 将 JWT 进行校验.
我们只需要对我们需要校验的路由进行添加中间件校验即可.
- router.GET("/", middleware.Auth(), func(context *gin.Context) {
- context.JSON(http.StatusOK, time.Now().Unix())
- })
当我们访问 / 的时候就需要携带 token 了
- GET http://localhost:8080
- Content-Type: application/JSON
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjMiLCJleHAiOjE1NjQ3OTQzNjIsImp0aSI6Ilx1MDAwMCIsImlhdCI6MTU2NDc5NDM0MiwiaXNzIjoiZ2luIGhlbGxvIiwibmJmIjoxNTY0Nzk0MzQyLCJzdWIiOiJsb2dpbiJ9.uQxGMsftyVFtYIGwQVm1QB2djw-uMfDbw81E5LMjliU
来源: http://www.tuicool.com/articles/mMzUnyF