跳到主要内容

鉴权

一 什么是鉴权

在网站中,有些页面是登录后的用户才能访问的,由于http是无状态的协议,我们无法确认用户的状态(如是否登录)。这时候浏览器在访问这些页面时,需要额外传输一些用户的账户信息给后台,让后台知道该用户是否登录、是哪个用户在访问。

cookie是浏览器实现的技术,在浏览器中可以存储用户是否登录的凭证,每次请求都会将该凭证发送给服务器。

cookie实现鉴权步骤:

  • 用户登录成功后,后端向浏览器设置一个cookie:username=lisi
  • 每次请求,浏览器会自动把该cookie发送给服务端
  • 服务端处理请求时,从cookie中取出username,就知道是哪个用户了
  • 如果没过期,则鉴权通过,过期了,则重定向到登录页

Go中使用Cookie:

// 登录时设置cookie
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
cookie := http.Cookie{Name: "username", Value: "张三", Expires: expiration}
http.SetCookie(w, &cookie)

// 再次访问时,获取浏览器传递的cookie
// 获取cookie方式一
username, _ := r.Cookie("username")
// 获取cookie方式二
for _, cookie := range r.Cookies() {
fmt.Println(cookie.Username)
}

但是这样做,风险很大,黑客很容易知道cookie中传递的内容,即用户的真实账户信息。

二 session

2.1 session原理

为了解决cookie的安全问题,基于cookie,衍生了session技术。session技术将用户的信息存储在了服务端,保证了安全,其实现步骤为:

  • 服务端设置cookie时,不再存储username,而是存储一个随机生成的字符串,比如32位的uuid,服务端额外存储一个uuid与用户名的映射
  • 用户再次请求时,会自动把cookie中的uuid带入给服务器
  • 服务器使用uuid进行鉴权

一般上述的uuid在cookie中存储的键都是sid(session_id),也就是常说的session方案,服务端此时需要额外开辟空间存储sid与用户真实信息的对应映射。

2.2 session实现

如果要手动实现session,需要注意以下方面:

  • 全局session管理器:
  • 保证sessionid 的全局唯一性
  • 为每个客户关联一个session
  • session 过期处理
  • session 的存储(可以存储到内存、文件、数据库等)

关于session数据(sid与真实用户的映射)的存储,可以存放在服务端的一个文件中,比如该session第三方库:https://github.com/gorilla/sessions

使用示例:

package main

import(
"fmt"
"net/http"
"github.com/gorilla/sessions"
)

// 利用cookie方式创建session,秘钥为 mykey
var store = sessions.NewCookieStore([]byte("mykey"))

func setSession(w http.ResponseWriter, r *http.Request){
session, _ := store.Get(r, "sid")
session.Values["username"] = "张三"
session.Save(r, w)
}

func profile(w http.ResponseWriter, r *http.Request){

session, _ := store.Get(r, "sid")

if session.Values["username"] == nil {
fmt.Fprintf(w, `未登录,请前往 localhost:8080/setSession`)
return
}

fmt.Fprintf(w, `已登录,用户是:%s`, session.Values["username"])
return
}

func main() {

// 访问隐私页面
http.HandleFunc("/profile", profile)

// 设置session
http.HandleFunc("/setSession", setSession)

server := http.Server{
Addr: ":8080",
}
server.ListenAndServe()

}

在企业级开发中,经常使用额外的数据库redis来存储session数据。

2.3 禁用cookie时候session方案

以上方式中,生成的sid都存储在cookie中,如果用户禁用了cookie,则每次请求服务端无法收到sid!我们需要想别的办法来让浏览器的每次请求都携带上sid,常用方式是URL重写:在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。

三 JWT

3.1 jwt介绍

session将数据存储在了服务端,无端造成了服务端空间浪费,可否像cookie那样将用户数据存储在客户端,而不被黑客破解到呢?

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

3.2 jwt构成

一个 JWT token包含3部分:

  • header: 告诉我们使用的算法和 token 类型
  • Payload: 必须使用 sub key 来指定用户 ID, 还可以包括其他信息比如 email, username 等.
  • Signature: 用来保证 JWT 的真实性. 可以使用不同算法

header:

// base64编码的字符串`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9`
// 这里规定了加密算法,hash256
{
"alg": "HS256",
"typ": "JWT"
}

payload:

{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
// base64编码的字符串`eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9`

这里的内容没有强制要求,因为 Payload 就是为了承载内容而存在的,不过想用规范的话也可以参考下面的:

* iss: jwt签发者
* sub: jwt所面向的用户
* aud: 接收jwt的一方
* exp: jwt的过期时间,这个过期时间必须要大于签发时间
* nbf: 定义在什么时间之前,该jwt都是不可用的.
* iat: jwt的签发时间
* jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

signature是用 header + payload + secret组合起来加密的,公式是:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)

这里 secret就是自己定义的一个随机字符串,这一个过程只能发生在 server 端,会随机生成一个 hash 值,这样组合起来之后就是一个完整的 jwt 了:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.4c9540f793ab33b13670169bdf444c1eb1c37047f18e861981e14e34587b1e04

3.3 jwt执行流程

![] (jwt.jpg)

3.4 Go使用jwt

整体操作步骤:

  • 1.从request获取tokenstring
  • 2.将tokenstring转化为未解密的token对象
  • 3.将未解密的token对象解密得到解密后的token对象
  • 4.从解密后的token对象里取参数

使用第三方包:go get github.com/dgrijalva/jwt-go 示例:


// 生成Token:
// SecretKey 是一个 const 常量
func CreateToken(SecretKey []byte, issuer string, Uid uint, isAdmin bool) (tokenString string, err error) {
claims := &jwtCustomClaims{
jwt.StandardClaims{
ExpiresAt: int64(time.Now().Add(time.Hour * 72).Unix()),
Issuer: issuer,
},
Uid,
isAdmin,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err = token.SignedString(SecretKey)
return
}

// 解析Token
func ParseToken(tokenSrt string, SecretKey []byte) (claims jwt.Claims, err error) {
var token *jwt.Token
token, err = jwt.Parse(tokenSrt, func(*jwt.Token) (interface{}, error) {
return SecretKey, nil
})
claims = token.Claims
return
}