https://www.acurd.com
go并不像php那样原生就支持session,我们可以根据自己的需求实现一个session库,也可以使用第三方的库,这也再次说明了session并不是什么神秘的技术,而是基于服务端对客户端cookie的读写来实现的。
这里我们使用第三方库来演示一下
package main
import (
"fmt"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"log"
"net/http"
)
var store = sessions.NewCookieStore([]byte("secret-key"))
// 自定义一个账户和密码
var name, pwd = "acurd", "acurdpwd"
func LoginHandler(w http.ResponseWriter, r *http.Request) {
//获取session
session, _ := store.Get(r, "session_id")
//打印session信息
fmt.Printf("%+v", session.Values)
if !session.IsNew {
fmt.Fprintf(w, "你已经登录了
")
return
}
r.ParseForm() // 解析参数,默认是不会解析的
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
//有的话校验登录
username := r.FormValue("username")
password := r.FormValue("password")
if username == name && password == pwd {
session.Values["username"] = username
// 将session保存
err := session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "登录成功
")
return
}
//没有的话提示输入用户名密码
fmt.Fprintf(w, "用户名或密码失败
")
}
func main() {
// 创建路由
r := mux.NewRouter()
r.HandleFunc("/login", LoginHandler)
err := http.ListenAndServe(":8002", r)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
cookie+session这种方式的缺点
我们先来看一个token长什么样子,比如这个eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFjdXJkIiwiVWlkIjoxMiwic3ViIjoiVG9rZW4iLCJleHAiOjE2ODc4Njk2NjYsImlhdCI6MTY4Nzg2NjA2Nn0._hIRJDrLV8co1gZPKKxG2AyMVFRapl-nt-Kqbb-r8bg
,我们看到这个token被.
分割成了三部分,那么这三部分代表什么含义呢?我们先来还原一下这三部分怎么来的
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
是对算法和type的加密,加密算法是base64.urlencode
,相关代码如下:func TestDemoTwo28(t *testing.T) {
type JWTHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
header := JWTHeader{
Alg: "HS256",
Typ: "JWT",
}
headerBytes, _ := json.Marshal(header)
headerBase64 := base64.RawURLEncoding.EncodeToString(headerBytes) // 对字节数组进行 Base64 编码
fmt.Println(headerBase64) // 输出编码后的 JWT Header
}
eyJVc2VybmFtZSI6ImFjdXJkIiwiVWlkIjoxMiwic3ViIjoiVG9rZW4iLCJleHAiOjE2ODc4Njk2NjYsImlhdCI6MTY4Nzg2NjA2Nn0
其实也是base64.urlencode
加密得来的,加密的对象就是我们存储的数据,咱们可以用解码的方式解析一下看看相关代码如下
func TestDemoTwo28(t *testing.T) {
str := "eyJVc2VybmFtZSI6ImFjdXJkIiwiVWlkIjoxMiwic3ViIjoiVG9rZW4iLCJleHAiOjE2ODc4Njk2NjYsImlhdCI6MTY4Nzg2NjA2Nn0"
decodeString, _ := base64.RawURLEncoding.DecodeString(str)
fmt.Printf("%s", string(decodeString))
}
执行结果如下
通过第一部分和第二部分我们看到了,这是一种可逆的加密,所以不要放重要数据(也可以再次对payload的数据对称加密,保证数据不外泄),比如登录密码,手机号。接下来看第三部分。这里我贴一份可逆加密的代码
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"fmt"
)
func main() {
// 原始数据
source := "Hello, world!"
fmt.Println("原文:", source)
// 密钥,必须是 16、24 或 32 字节
key := "example key 1234"
// 加密
encryptCode := AESEncrypt([]byte(source), []byte(key))
fmt.Println("密文(byte):", encryptCode)
// 使用 hex 编码打印
fmt.Println("密文(hex):", hex.EncodeToString(encryptCode))
// 使用 base64 编码打印
fmt.Println("密文(base64):", base64.StdEncoding.EncodeToString(encryptCode))
// 解密
decryptCode := AESDecrypt(encryptCode, []byte(key))
fmt.Println("解密后的原文:", string(decryptCode))
}
// AESEncrypt 使用 AES 加密数据
func AESEncrypt(data, key []byte) []byte {
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
ciphertext := make([]byte, aes.BlockSize+len(data))
iv := ciphertext[:aes.BlockSize]
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], data)
return ciphertext
}
// AESDecrypt 使用 AES 解密数据
func AESDecrypt(data, key []byte) []byte {
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
if len(data) < aes.BlockSize {
panic("ciphertext too short")
}
iv := data[:aes.BlockSize]
data = data[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(data, data)
return data
}
_hIRJDrLV8co1gZPKKxG2AyMVFRapl-nt-Kqbb-r8bg
这个数值是怎么来的呢?就是基于前两段的值+秘钥计算得来,具体算法如下HMACSHA256( base64UrlEncode(header) + "." +base64UrlEncode(payload), secret )
,这一部分才是校验我们数据的准确性,比如数据是否早到篡改。我们可以通过https://jwt.io/
更直观的观察结果
这个时候,是不是上来就是一句卧槽,原来token是可以被解析的,那我的token岂不是不安全,说的对,token只是实现登录认证,并不保证你的数据不外泄,所以我们不能把重要的信息放到token里面去。但是如果你篡改了数据,伪造登录,由于别人没有你的秘钥,导致第三段校验失败,就可以拦截非法登录了。
其实了解了jwt的实现机制,我们自己也可以定义一套自己的token机制来实现登录功能。
下面我们来看一下别人已经造好的轮子是怎么用的。
我们使用go的第三方包来实现jwt登录功能
怎么找包要认真说,还真是一个技术活,你要是网上随便搜一个,那么第一安全性,可靠性都可能会有问题,更别谈后期的维护了。那么一般我们怎么找包呢?github上面通过 语言+关键词来搜索,一般使用star最多的就行了,比如我们在github 搜 go jwt
我们看到第一个和第二个其实是一个仓库,所以我们使用github.com/golang-jwt/jwt/v5
这个包
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"log"
"net/http"
"time"
)
// 定义自己的秘钥 所有的服务必须用一个秘钥才能正确解析token
var privateKey = []byte("my_secret_key")
// 自定义一个账户和密码 用户uid 这里简单举个例子,一般是去数据库校验
var name, pwd, uid = "acurd", "acurdpwd", 12
// UserClaims 我们声明一个结构体,里面包含我们想要保存的信息
type UserClaims struct {
Username string
Uid int64
jwt.RegisteredClaims // 内嵌标准的声明
}
// GenToken 生成token
func GenToken(username string, uid int64) (string, error) {
//初始化结构体
claims := UserClaims{
Uid: uid,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
//设置过期时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 3600)),
//颁发时间
IssuedAt: jwt.NewNumericDate(time.Now()),
//主题
Subject: "Token",
},
}
//生成token 使用hs256 加密 结构体,然后再用秘钥对其做数字签名
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(privateKey)
}
// 解析token
func parseToken(tokenString string) (*UserClaims, error) {
claims := new(UserClaims)
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
//建议token是否有效
if token.Valid {
return claims, nil
}
return nil, err
}
func keyFunc(token *jwt.Token) (interface{}, error) {
return privateKey, nil
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
//限制post提交
if r.Method != "POST" {
fmt.Fprintf(w, "非法登录
")
return
}
//获取token
auth := r.Header.Get("Authorization")
if len(auth) > 0 { //刺入还可以加入对
//打印token信息
fmt.Println(auth)
//校验token
claims, err := parseToken(auth)
if err != nil {
fmt.Fprintf(w, "解析token失败
")
return
}
//
fmt.Fprintf(w, "您已经登录了
相关token解析如下:%+v", claims)
return
}
r.ParseForm() // 解析参数,默认是不会解析的
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
//有的话校验登录
username := r.FormValue("username")
password := r.FormValue("password")
if username == name && password == pwd {
// 生成token并返回
token, err := GenToken(username, int64(uid))
if err != nil {
fmt.Fprintf(w, "生成token失败
")
}
// 将token保存
fmt.Fprintf(w, "登录成功
请保存好你的token:%s", token)
return
}
//没有的话提示输入用户名密码
fmt.Fprintf(w, "用户名或密码失败
")
}
func main() {
// 创建路由
r := mux.NewRouter()
r.HandleFunc("/login", LoginHandler)
err := http.ListenAndServe(":8002", r)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
场景1:我们用QQ或者微信,不可能每打开一次都执行一次登录操作,而是好长时间不用,比如一个月没有打开QQ,那么你再次打开QQ,可能就需要登录了,但是如果你经常打开QQ,可能好几个月都不需要登录,那么这是怎么实现的呢?
场景2:你设计了一套jwt的登录系统,token有效期是60分钟,用户在58分钟的时候打开了网页,在61分钟的时候点击提交,发现自己被退出登录了,你说用户气不气?
基于上面的两种场景,我们提出来refresh_token的机制,其实就是多了一个刷新token的触发机制,第一次登录的时候,我们给用户颁发两个token ,一个access_token,有效期比较短,比如是4小时,还有一个refresh_token ,就是刷新token,假设有效期是24小时。
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"log"
"net/http"
"time"
)
// 定义自己的秘钥 所有的服务必须用一个秘钥才能正确解析token
var privateKey = []byte("my_secret_key")
// 自定义一个账户和密码 用户uid 这里简单举个例子,一般是去数据库校验
var name, pwd, uid = "acurd", "acurdpwd", 12
// UserClaims 我们声明一个结构体,里面包含我们想要保存的信息
type UserClaims struct {
Username string
Uid int64
jwt.RegisteredClaims // 内嵌标准的声明
}
// RefreshClaims 用来生成refresh token
type RefreshClaims struct {
jwt.RegisteredClaims // 内嵌标准的声明
}
// GenToken 生成token
func GenToken(username string, uid int64) (string, error) {
//初始化结构体
claims := UserClaims{
Uid: uid,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
//设置过期时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 30)),
//颁发时间
IssuedAt: jwt.NewNumericDate(time.Now()),
//主题
Subject: "Token",
},
}
//生成token 使用hs256 加密 结构体,然后再用秘钥对其做数字签名
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(privateKey)
}
// 解析token
func parseToken(tokenString string) (*UserClaims, error) {
claims := new(UserClaims)
_, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
return claims, err
}
func keyFunc(token *jwt.Token) (interface{}, error) {
return privateKey, nil
}
// GenRefreshToken 生成token
func GenRefreshToken() (string, error) {
//初始化结构体
claims := UserClaims{
RegisteredClaims: jwt.RegisteredClaims{
//设置过期时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 3600)),
//颁发时间
IssuedAt: jwt.NewNumericDate(time.Now()),
//主题
Subject: "RefreshToken",
},
}
//生成token 使用hs256 加密 结构体,然后再用秘钥对其做数字签名
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(privateKey)
}
// 解析token
func parseRefreshToken(tokenString string) (*RefreshClaims, error) {
claims := new(RefreshClaims)
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
//建议token是否有效
if token.Valid {
return claims, nil
}
return nil, err
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
//限制post提交
if r.Method != "POST" {
fmt.Fprintf(w, "非法登录
")
return
}
//获取token
auth := r.Header.Get("Authorization")
tokenRefresh := r.Header.Get("AuthorizationRef")
fmt.Println(auth)
fmt.Println(tokenRefresh)
if len(auth) > 0 {
// 解析refresh token
claimsR, err := parseRefreshToken(tokenRefresh)
if err != nil {
fmt.Fprintf(w, "token 错误或过期%s", err)
return
}
fmt.Printf("claimsR %v", claimsR)
//解析token
claims, err := parseToken(auth)
fmt.Printf("claims %+v err:%v", claims, err)
//token未过期
if claims != nil && !claims.Expired() {
fmt.Fprintf(w, "您已经登录了
相关token解析如下:%+v", claims)
return
}
token, _ := GenToken(claims.Username, int64(uid))
refreshToken, _ := GenRefreshToken()
// 将token保存
fmt.Fprintf(w, "已经更新token
请保存好你的token:%s;refreshToken:%s", token, refreshToken)
return
}
r.ParseForm() // 解析参数,默认是不会解析的
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
//有的话校验登录
username := r.FormValue("username")
password := r.FormValue("password")
if username == name && password == pwd {
// 生成token并返回
token, _ := GenToken(username, int64(uid))
refreshToken, _ := GenRefreshToken()
// 将token保存
fmt.Fprintf(w, "登录成功
请保存好你的token:%s;refreshToken:%s", token, refreshToken)
return
}
//没有的话提示输入用户名密码
fmt.Fprintf(w, "用户名或密码失败
")
}
// Expired 检查自定义的token结构体是否过期
func (token *UserClaims) Expired() bool {
fmt.Println(time.Now().Unix())
fmt.Println(token.ExpiresAt.Unix())
return time.Now().Unix() > token.ExpiresAt.Unix()
}
func main() {
// 创建路由
r := mux.NewRouter()
r.HandleFunc("/login", LoginHandler)
err := http.ListenAndServe(":8002", r)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
由于jwt是无状态的,而且token是在登录时生成的,所以会有以下两个问题