详解Go-JWT-RESTful身份认证教程
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.logincommenter
和 internal.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 interface
的 valid() 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),
- 用户输入登录信息
- 服务器验证登录信息是否正确,如果正确就创建一个session,并把session存入数据库
- 服务器端会向客户端返回带有sessionid的cookie
- 在接下来的请求中,服务器将把sessionid与数据库中的相匹配,如果有效则处理该请求
- 如果用户登出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身份认证教程
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: golang协程池设计详解