当前位置: 移动技术网 > IT编程>脚本编程>Go语言 > 详解Go-JWT-RESTful身份认证教程

详解Go-JWT-RESTful身份认证教程

2020年03月09日  | 移动技术网IT编程  | 我要评论

1.什么是jwt

jwt(json web token)是一个非常轻巧的规范,这个规范允许我们使用jwt在用户和服务器之间传递安全可靠的信息,
一个jwt由三部分组成,header头部、claims载荷、signature签名,

jwt原理类似我们加盖公章或手写签名的的过程,合同上写了很多条款,不是随便一张纸随便写啥都可以的,必须要一些证明,比如签名,比如盖章,jwt就是通过附加签名,保证传输过来的信息是真的,而不是伪造的,

它将用户信息加密到token里,服务器不保存任何用户信息,服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证,

2.jwt构成

一个jwt由三部分组成,header头部、claims载荷、signature签名,

  • header头部:头部,表明类型和加密算法
  • claims载荷:声明,即载荷(承载的内容)
  • signature签名:签名,这一部分是将header和claims进行base64转码后,并用header中声明的加密算法加盐(secret)后构成,即:
let tmpstr = base64(header)+base64(claims)
let signature = encrypt(tmpstr,secret)
//最后三者用"."连接,即:
let token = base64(header)+"."+base64(claims)+"."+signature

3.javascript提取jwt字符串荷载信息

jwt里面payload可以包含很多字段,字段越多你的token字符串就越长.
你的http请求通讯的发送的数据就越多,回到之接口响应时间等待稍稍的变长一点点.

一下代码就是前端javascript从payload获取登录的用户信息.
当然后端middleware也可以直接解析payload获取用户信息,减少到数据库中查询user表数据.接口速度会更快,数据库压力更小.
后端检查jwt身份验证时候当然会校验payload和signature签名是否合法.

let tokenstring = 'eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjlehaioje1njc3nzc5njisimp0asi6ijuilcjpyxqioje1njc2ote1njisimlzcyi6imzlbgl4lm1vam90di5jbiisimlkijo1lcjjcmvhdgvkx2f0ijoimjaxos0wos0wnvqxmto1njo1os41nji1ndcwodyrmdg6mdailcj1cgrhdgvkx2f0ijoimjaxos0wos0wnvqxnjo1odoymc41ntyxnjawotirmdg6mdailcj1c2vybmftzsi6imvyawmilcjuawnrx25hbwuioiiilcjlbwfpbci6ijeymzq1nkbxcs5jb20ilcjtb2jpbguioiiilcjyb2xlx2lkijo4lcjzdgf0dxmiojasimf2yxrhcii6ii8vdgvjac5tb2pvdhyuy24vyxnzzxrzl2ltywdll2f2yxrhcl8zlnbuzyisinjlbwfyayi6iiisimzyawvuzf9pzhmiom51bgwsimthcm1hijowlcjjb21tzw50x2lkcyi6bnvsbh0.tgjukvue9jvjzda42igfh_5jiembo5yzbzdqlnag6kq'
function parsetokengetuser(jwttokenstring) {
  let base64url = jwttokenstring.split('.')[1];
  let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
  let jsonpayload = decodeuricomponent(atob(base64).split('').map(function (c) {
    return '%' + ('00' + c.charcodeat(0).tostring(16)).slice(-2);
  }).join(''));
  let user = json.parse(jsonpayload);

  localstorage.setitem("token", jwttokenstring);
  localstorage.setitem("expire_ts", user.exp);
  localstorage.setitem("user", jsonpayload);
  return user;
}
parsetokengetuser(tokenstring)

复制上面javascript代码到浏览器console中执行就可以解析出用户信息了! 当然你要可以使用在线工具来解析jwt token的payload荷载
jwt在线解析工具

4. go语言gin框架实现jwt用户认证

接下来我将使用最受欢迎的和 dgrijalva/jwt-go

这两个package来演示怎么使用jwt身份认证.

4.1 登录接口

4.1.1 登录接口路由(login-route)

  r := gin.new()
  r.maxmultipartmemory = 32 << 20
  //sever static file in http's root path
  binstaticmiddleware, err := felixbin.newginstaticbinmiddleware("/")
  if err != nil {
    return err
  }
  //支持跨域
  mwcors := cors.new(cors.config{
    alloworigins:   []string{"*"},
    allowmethods:   []string{"put", "patch", "post", "get", "delete"},
    allowheaders:   []string{"origin", "authorization", "content-type"},
    exposeheaders:  []string{"content-type"},
    allowcredentials: true,
    alloworiginfunc: func(origin string) bool {
      return true
    },
    maxage: 2400 * time.hour,
  })
  r.use(binstaticmiddleware, mwcors)


  {
    r.post("comment-login", internal.logincommenter)    //评论用户登陆
    r.post("comment-register", internal.registercommenter) //评论用户注册
  }

  api := r.group("api")
  api.post("admin-login", internal.loginadmin) //管理后台登陆

internal.logincommenterinternal.loginadmin 这两个方法是一样的,
只需要关注其中一个就可以了,我们就关注internal.logincommenter

4.1.2 登录login handler

编写登录的handler

func logincommenter(c *gin.context) {
  var mdl model.user
  err := c.shouldbind(&mdl)
  if handleerror(c, err) {
    return
  }
  //获取ip
  ip := c.clientip()
  //roleid 8 是评论系统的用户
  data, err := mdl.login(ip, 8)
  if handleerror(c, err) {
    return
  }
  jsondata(c, data)
}

其中最关键的是mdl.login(ip, 8)这个函数

  • 1.数据库查询用户
  • 2.校验用户role_id
  • 3.比对密码
  • 4.防止密码泄露(清空struct的属性)
  • 5.生成jwt-string
//login
func (m *user) login(ip string, roleid uint) (string, error) {
  m.id = 0
  if m.password == "" {
    return "", errors.new("password is required")
  }
  inputpassword := m.password
  //获取登录的用户
  err := db.where("username = ? or email = ?", m.username, m.username).first(&m).error
  if err != nil {
    return "", err
  }
  //校验用户角色
  if (m.roleid & roleid) != roleid {
    return "", fmt.errorf("not role of %d", roleid)
  }
  //验证密码
  //password is set to bcrypt check
  if err := bcrypt.comparehashandpassword([]byte(m.hashedpassword), []byte(inputpassword)); err != nil {
    return "", err
  }
  //防止密码泄露
  m.password = ""
  //生成jwt-string
  return jwtgeneratetoken(m, time.hour*24*365)
}

4.1.2 生成jwt-string(核心代码)

1.自定义payload结构体,不建议直接使用 dgrijalva/jwt-go jwt.standardclaims结构体.因为他的payload包含的用户信息太少.

2.实现 type claims interfacevalid() error 方法,自定义校验内容

3.生成jwt-string jwtgeneratetoken(m *user,d time.duration) (string, error)

package model

import (
  "errors"
  "fmt"
  "time"

  "github.com/dgrijalva/jwt-go"
  "github.com/sirupsen/logrus"
)

var appsecret = ""//viper.getstring会设置这个值(32byte长度)
var appiss = "github.com/libragen/felix"//这个值会被viper.getstring重写

//自定义payload结构体,不建议直接使用 dgrijalva/jwt-go `jwt.standardclaims`结构体.因为他的payload包含的用户信息太少.
type userstdclaims struct {
  jwt.standardclaims
  *user
}
//实现 `type claims interface` 的 `valid() error` 方法,自定义校验内容
func (c userstdclaims) valid() (err error) {
  if c.verifyexpiresat(time.now().unix(), true) == false {
    return errors.new("token is expired")
  }
  if !c.verifyissuer(appiss, true) {
    return errors.new("token's issuer is wrong")
  }
  if c.user.id < 1 {
    return errors.new("invalid user in jwt")
  }
  return
}

func jwtgeneratetoken(m *user,d time.duration) (string, error) {
  m.password = ""
  expiretime := time.now().add(d)
  stdclaims := jwt.standardclaims{
    expiresat: expiretime.unix(),
    issuedat: time.now().unix(),
    id:    fmt.sprintf("%d", m.id),
    issuer:  appiss,
  }

  uclaims := userstdclaims{
    standardclaims: stdclaims,
    user:      m,
  }

  token := jwt.newwithclaims(jwt.signingmethodhs256, uclaims)
  // sign and get the complete encoded token as a string using the secret
  tokenstring, err := token.signedstring([]byte(appsecret))
  if err != nil {
    logrus.witherror(err).fatal("config is wrong, can not generate jwt")
  }
  return tokenstring, err
}


//jwtparseuser 解析payload的内容,得到用户信息
//gin-middleware 会使用这个方法
func jwtparseuser(tokenstring string) (*user, error) {
  if tokenstring == "" {
    return nil, errors.new("no token is found in authorization bearer")
  }
  claims := userstdclaims{}
  _, err := jwt.parsewithclaims(tokenstring, &claims, func(token *jwt.token) (interface{}, error) {
    if _, ok := token.method.(*jwt.signingmethodhmac); !ok {
      return nil, fmt.errorf("unexpected signing method: %v", token.header["alg"])
    }
    return []byte(appsecret), nil
  })
  if err != nil {
    return nil, err
  }
  return claims.user, err
}

4.2 jwt中间件(middleware)

1.从url-query的_t获取jwt-string或者从请求头 authorization中获取jwt-string

2.model.jwtparseuser(token)解析jwt-string获取user结构体(减少中间件查询数据库的操作和时间)

3.设置用户信息到gin.context 其他的handler通过gin.context.get(contextkeyuserobj),在进行用户type assert得到model.user 结构体.

4.使用了jwt-middle之后的handle从gin.context中获取用户信息

package internal

import (
  "net/http"
  "strings"

  "github.com/libragen/felix/model"
  "github.com/gin-gonic/gin"
)

const contextkeyuserobj = "autheduserobj"
const bearerlength = len("bearer ")

func ctxtokentouser(c *gin.context, roleid uint) {
  token, ok := c.getquery("_t")
  if !ok {
    htoken := c.getheader("authorization")
    if len(htoken) < bearerlength {
      c.abortwithstatusjson(http.statuspreconditionfailed, gin.h{"msg": "header authorization has not bearer token"})
      return
    }
    token = strings.trimspace(htoken[bearerlength:])
  }
  usr, err := model.jwtparseuser(token)
  if err != nil {
    c.abortwithstatusjson(http.statuspreconditionfailed, gin.h{"msg": err.error()})
    return
  }
  if (usr.roleid & roleid) != roleid {
    c.abortwithstatusjson(http.statuspreconditionfailed, gin.h{"msg": "roleid 没有权限"})
    return
  }

  //store the user model in the context
  c.set(contextkeyuserobj, *usr)
  c.next()
  // after request
}

func mwuseradmin(c *gin.context) {
  ctxtokentouser(c, 2)
}

func mwusercomment(c *gin.context) {
  ctxtokentouser(c, 8)
}

使用了jwt-middle之后的handle从gin.context中获取用户信息,

func mwuserid(c *gin.context) (uint, error) {
  v,exist := c.get(contextkeyuserobj)
  if !exist {
    return 0,errors.new(contextkeyuserobj + " not exist")
  }
  user, ok := v.(model.user)
  if ok {
    return user.id, nil
  }
  return 0,errors.new("can't convert to user struct")
}

4.2 使用jwt中间件

一下代码有两个jwt中间件的用法

  • internal.mwuseradmin 管理后台用户中间件
  • internal.mwusercommenter 评论用户中间件

package ssh2ws

import (
  "time"

  "github.com/libragen/felix/felixbin"
  "github.com/libragen/felix/model"
  "github.com/libragen/felix/ssh2ws/internal"
  "github.com/libragen/felix/wslog"
  "github.com/gin-contrib/cors"
  "github.com/gin-gonic/gin"
)

func runssh2ws(bindaddress, user, password, secret string, expire time.duration, verbose bool) error {
  err := model.creategoduser(user, password)
  if err != nil {
    return err
  }
  //config jwt variables
  model.appsecret = secret
  model.expiretime = expire
  model.appiss = "felix.mojotv.cn"
  if !verbose {
    gin.setmode(gin.releasemode)
  }
  r := gin.new()
  r.maxmultipartmemory = 32 << 20
  //sever static file in http's root path
  binstaticmiddleware, err := felixbin.newginstaticbinmiddleware("/")
  if err != nil {
    return err
  }

  mwcors := cors.new(cors.config{
    alloworigins:   []string{"*"},
    allowmethods:   []string{"put", "patch", "post", "get", "delete"},
    allowheaders:   []string{"origin", "authorization", "content-type"},
    exposeheaders:  []string{"content-type"},
    allowcredentials: true,
    alloworiginfunc: func(origin string) bool {
      return true
    },
    maxage: 2400 * time.hour,
  })
  r.use(binstaticmiddleware, mwcors)


  {
    r.post("comment-login", internal.logincommenter)    //评论用户登陆
    r.post("comment-register", internal.registercommenter) //评论用户注册
  }

  api := r.group("api")
  api.post("admin-login", internal.loginadmin) //管理后台登陆
  api.get("meta", internal.meta)

  //terminal log
  hub := wslog.newhub()
  go hub.run()

  {
    //websocket
    r.get("ws/hook", internal.mwuseradmin, internal.wslog(hub))
    r.get("ws/ssh/:id", internal.mwuseradmin, internal.wsssh)
  }
  //给外部调用
  {
    api.post("wslog/hook-api", internal.jwtmiddlewarewslog, internal.wsloghookapi(hub))
    api.get("wslog/hook", internal.mwuseradmin, internal.wsloghookall)
    api.post("wslog/hook", internal.mwuseradmin, internal.wsloghookcreate)
    api.patch("wslog/hook", internal.mwuseradmin, internal.wsloghookupdate)
    api.delete("wslog/hook/:id", internal.mwuseradmin, internal.wsloghookdelete)

    api.get("wslog/msg", internal.mwuseradmin, internal.wslogmsgall)
    api.post("wslog/msg-rm", internal.mwuseradmin, internal.wslogmsgdelete)
  }

  //评论
  {
    api.get("comment", internal.commentall)
    api.get("comment/:id/:action", internal.mwusercomment, internal.commentaction)
    api.post("comment", internal.mwusercomment, internal.commentcreate)
    api.delete("comment/:id", internal.mwuseradmin, internal.commentdelete)
  }
  {
    api.get("hacknews",internal.mwuseradmin, internal.hacknewall)
    api.patch("hacknews", internal.hacknewupdate)
    api.post("hacknews-rm", internal.hacknewrm)
  }

  authg := api.use(internal.mwuseradmin)
  {

    //create wslog hook

    authg.get("ssh", internal.sshall)
    authg.post("ssh", internal.sshcreate)
    authg.get("ssh/:id", internal.sshone)
    authg.patch("ssh", internal.sshupdate)
    authg.delete("ssh/:id", internal.sshdelete)

    authg.get("sftp/:id", internal.sftpls)
    authg.get("sftp/:id/dl", internal.sftpdl)
    authg.get("sftp/:id/cat", internal.sftpcat)
    authg.get("sftp/:id/rm", internal.sftprm)
    authg.get("sftp/:id/rename", internal.sftprename)
    authg.get("sftp/:id/mkdir", internal.sftpmkdir)
    authg.post("sftp/:id/up", internal.sftpup)

    authg.post("ginbro/gen", internal.ginbrogen)
    authg.post("ginbro/db", internal.ginbrodb)
    authg.get("ginbro/dl", internal.ginbrodownload)

    authg.get("ssh-log", internal.sshlogall)
    authg.delete("ssh-log/:id", internal.sshlogdelete)
    authg.patch("ssh-log", internal.sshlogupdate)

    authg.get("user", internal.userall)
    authg.post("user", internal.registercommenter)
    //api.get("user/:id", internal.sshall)
    authg.delete("user/:id", internal.userdelete)
    authg.patch("user", internal.userupdate)

  }

  if err := r.run(bindaddress); err != nil {
    return err
  }
  return nil
}

5. cookie-session vs jwt

jwt和session有所不同,session需要在服务器端生成,服务器保存session,只返回给客户端sessionid,客户端下次请求时带上sessionid即可,因为session是储存在服务器中,有多台服务器时会出现一些麻烦,需要同步多台主机的信息,不然会出现在请求a服务器时能获取信息,但是请求b服务器身份信息无法通过,jwt能很好的解决这个问题,服务器端不用保存jwt,只需要保存加密用的secret,在用户登录时将jwt加密生成并发送给客户端,由客户端存储,以后客户端的请求带上,由服务器解析jwt并验证,这样服务器不用浪费空间去存储登录信息,不用浪费时间去做同步,

5.1 什么是cookie

基于cookie的身份验证是有状态的,这意味着验证的记录或者会话(session)必须同时保存在服务器端和客户端,服务器端需要跟踪记录session并存至数据库,
同时前端需要在cookie中保存一个sessionid,作为session的唯一标识符,可看做是session的“身份证”,

cookie,简而言之就是在客户端(浏览器等)保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过http协议将本地cookie内容发送给服务器,从而完成验证,或继续上一步操作,

5.2 什么是session

session,会话,简而言之就是在服务器上保存用户操作的历史信息,在用户登录后,服务器存储用户会话的相关信息,并为客户端指定一个访问凭证,如果有客户端凭此凭证发出请求,则在服务端存储的信息中,取出用户相关登录信息,
并且使用服务端返回的凭证常存储于cookie中,也可以改写url,将id放在url中,这个访问凭证一般来说就是sessionid,

5.3 cookie-session身份验证机制的流程

session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同,
session可以通过cookie来完成,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端,
因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appa主动设置域b cookie,让域b cookie获取;2. xss,在appa上通过javascript获取document.cookie,并传递给自己的appb),

  1. 用户输入登录信息
  2. 服务器验证登录信息是否正确,如果正确就创建一个session,并把session存入数据库
  3. 服务器端会向客户端返回带有sessionid的cookie
  4. 在接下来的请求中,服务器将把sessionid与数据库中的相匹配,如果有效则处理该请求
  5. 如果用户登出app,session会在客户端和服务器端都被销毁

5.4 cookie-session 和 jwt 使用场景

后端渲染html页面建议使用cookie-session认证

后按渲染页面可以很方便的写入/清除cookie到浏览器,权限控制非常方便.很少需要要考虑跨域ajax认证的问题.

app,web单页面应用,apis建议使用jwt认证

app、web apis等的兴起,基于token的身份验证开始流行,
当我们谈到利用token进行认证,我们一般说的就是利用json web tokens(jwts)进行认证,虽然有不同的方式来实现token,
事实上,jwts 已成为标准,因此在本文中将互换token与jwts,

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流, 谢谢大家对mojotv.cn的支持.喜欢这个网站麻烦帮忙添加到收藏夹,添加我的微信好友: felixarebest 微博账号: mojotech 向我提问.

原文地址:go进阶24:go-jwt restful身份认证教程

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网