背景:
开发中可能会存在客户端与服务器交互过程中数据安全问题,现在比较常见的方式有文件的签名校验,方式有很多,但是我感觉针对不同的业务需求我们用不同的加密校验方式:
比如Oauth2: 该方式适合第三方授权获取用户信息的业务需求,原理认证的Token存储在服务器端,然后用户根据服务器分配的ClientId和CllientSecret进行获取对应的Token信息,然后每次访问服务器将Token放在请求头,服务器验证是否有效,如果有效则返回对应的请求信息。
JWT: 是一种Json Web Token是一种验证前后端数据传输中Json 的数据安全。原理很简单不过方式和Oauth2相反,但是我感觉这种还是挺安全的,那就是服务器根据自己自定义的Secret进行数据加盐(加密),然后根据指定的加密算法进行加密同时生成的Token不会存储在服务器中,会直接返回给客户端,然后客户端每次请求的时候带上token,服务器根据自己的加盐方式和加密算法就可以反解获得想要的用户数据。
下面重点讲解JWT的开发使用:
步骤:
1. 依赖包下载:
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
2. 路由设计:(主要针对通用的JWT方式:有login登录,refresh刷新,sayHello业务处理)
func main() {
r := gin.Default()
r.GET("/login", login)
r.GET("/verify", verify)
r.GET("/refresh", refresh)
r.GET("/sayHello", sayHello)
r.Run(":9090")
//测试URL实例
//http://localhost:9090/login?username=dong&password=123456
//http://localhost:9090/verify?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTIyMTAsImlhdCI6MTU2MDUwODYxMCwidXNlcl9pZCI6MSwicGFzc3dvcmQiOiIxMjM0NTYiLCJ1c2VybmFtZSI6ImRvbmciLCJmdWxsX25hbWUiOiJkb25nIiwicGVybWlzc2lvbnMiOltdfQ.Esh1Zge0vO1BAW1GeR5wurWP3H1jUIaMf3tcSaUwkzA
//http://localhost:9090/refresh?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTIyNDMsImlhdCI6MTU2MDUwODYxMCwidXNlcl9pZCI6MSwicGFzc3dvcmQiOiIxMjM0NTYiLCJ1c2VybmFtZSI6ImRvbmciLCJmdWxsX25hbWUiOiJkb25nIiwicGVybWlzc2lvbnMiOltdfQ.Xkb_J8MWXkwGUcBF9bpp2Ccxp8nFPtRzFzOBeboHmg0
}
3. gin的路由设计方式和Beego的很相似: relativePath 表示路径, handlers 表示路由下执行的方法。
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
4. 设置相关的变量和结构:
JWTClaims: 是服务器加密的原文,解密后也是该信息,根据需求可以自行定义(比如下面我添加了role角色属性,用于后面的权限管理,我之前的博客有关于权限管理的设计Casbin),但是必须包含 jwt.StandardClaims子结构体。
/**
* @Author GCG
* @Description: 功能描述(根据自己的需求设计相关结构,用于再次获取用户权限的信息 jwt.StandardClaims必须要含有)
* @Date : 2020/11/12
*/
type JWTClaims struct { // token里面添加用户信息,验证token后可能会用到用户信息
jwt.StandardClaims
UserID int `json:"user_id"`
Password string `json:"password"`
Username string `json:"username"`
Role string `josn:"role"`
FullName string `json:"full_name"`
Permissions []string `json:"permissions"`
}
/**
* @Author GCG
* @Description: 功能描述(全局变量秘钥和失效时长)
* @Date : 2020/11/12
*/
var (
Secret = "dong_tech" // 加盐:签名秘钥
ExpireTime = 10 // token有效期 10秒
)
5. 分解介绍各个函数:
1) 首先就是login()登录获取Token的业务:
/**
* @Author GCG
* @Description: 功能描述(通过Login登录获取token)
* @Date : 2020/11/12
*/
func login(c *gin.Context) {
username := c.Query("username")
password := c.Query("password")
role := c.Query("role")
fmt.Println("username=", username)
fmt.Println("password=", password)
fmt.Println("role=", role)
claims := &JWTClaims{
UserID: 1,
Username: username,
Password: password,
Role: role,
FullName: username,
Permissions: []string{},
}
claims.IssuedAt = time.Now().Unix() // 封装claims从1970到现在的总秒数表示token获取的时间
claims.ExpiresAt = time.Now().Add(time.Second * time.Duration(ExpireTime)).Unix() // 封装claims表示token失效的时间
signedToken, err := getToken(claims) //获得token签名的认证
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
c.String(http.StatusOK, signedToken)
}
login()里面主要的token获取是通过getToken方式:
/**
* @Author GCG
* @Description: 功能描述(获取Token函数)
* @Date : 2020/11/12
*/
func getToken(claims *JWTClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) //根据指定的签名方法,和claims进行加密,生成token
signedToken, err := token.SignedString([]byte(Secret)) //生成的Token 进行秘钥签名使用Secret进行加密
if err != nil {
return "", errors.New(ErrorReason_ServerBusy)
}
return signedToken, nil
}
运行结果返回:
2). 我们介绍sayHello() 和 verify()业务,他们是相同的设计方式,我们讲一下最复杂的sayHello有权限分配的业务。
/**
* @Author GCG
* @Description: 功能描述(业务测试,用于真正的开发业务验证是否用户合法,然后执行Hello业务)
* @Date : 2020/11/12
*/
func sayHello(c *gin.Context) {
strToken := c.Query("token")
fmt.Println("token=", strToken)
claim, err := verifyAction(strToken)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
if claim.Role == "admin" {
fmt.Println("该用户是管理员可以执行相关的业务")
fmt.Println("--------------执行Hello业务--------------")
}
c.String(http.StatusOK, "hello,", claim.Role)
}
sayHello() 里面主要的获取用户信息的函数是verifyAction()用于解密验证用户的信息。
/**
* @Author GCG
* @Description: 功能描述(通过用户发送的token进行验证获取用户信息)
* @Date : 2020/11/12
*/
func verifyAction(strToken string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(strToken, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(Secret), nil //该函数是实现将前端携带的Token进行相关的解密,获得加密前的token数据
})
if err != nil {
return nil, errors.New(ErrorReason_ReLogin)
}
claims, ok := token.Claims.(*JWTClaims) // 根据解密的token获取claims声明信息
if !ok {
return nil, errors.New(ErrorReason_ReLogin)
}
if err := token.Claims.Valid(); err != nil {
return nil, errors.New(ErrorReason_ReLogin)
}
return claims, nil
}
verifyAction()里面还有一个核心那就是 token.Claims.Valid() (该函数是验证token是否失效)
失效的原理:很简单,那就是在login登录的时候获取的token时候我们已经设定了失效的时长,上面是10秒。然后当执行该函数的时候,会自动和的判断当前的时间和过期时间是否超期,未超期,则表示正常使用,否则会报错返回,最后执行错误响应 errors.New(ErrorReason_ReLogin)
执行效果:
3)最后的功能就是刷新token的函数 refresh():该方法就是在旧的token还没有失效的时候,进行刷新获取最新的Token返回和login的返回是一样的都是最新的Token。
/**
* @Author GCG
* @Description: 功能描述(刷新JWT的token)
* @Date : 2020/11/12
*/
func refresh(c *gin.Context) {
strToken := c.Query("token")
claims, err := verifyAction(strToken)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
claims.ExpiresAt = time.Now().Unix() + (claims.ExpiresAt - claims.IssuedAt) // 失效的时间
signedToken, err := getToken(claims)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
c.String(http.StatusOK, signedToken)
}
注意:不过我们发现获取的最新Token和还没有过期的Token都是可以用的,这个和Oauth2方式不同,因为我们没有存储Token所以没法办法将还没有过期的Token失效。
附录(全部代码如下所示):
package main
import (
"encoding/json"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func main() {
r := gin.Default()
r.GET("/login", login)
r.GET("/verify", verify)
r.GET("/refresh", refresh)
r.GET("/sayHello", sayHello)
r.Run(":9090")
//测试URL实例
//http://localhost:9090/login?username=dong&password=123456
//http://localhost:9090/verify?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTIyMTAsImlhdCI6MTU2MDUwODYxMCwidXNlcl9pZCI6MSwicGFzc3dvcmQiOiIxMjM0NTYiLCJ1c2VybmFtZSI6ImRvbmciLCJmdWxsX25hbWUiOiJkb25nIiwicGVybWlzc2lvbnMiOltdfQ.Esh1Zge0vO1BAW1GeR5wurWP3H1jUIaMf3tcSaUwkzA
//http://localhost:9090/refresh?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTIyNDMsImlhdCI6MTU2MDUwODYxMCwidXNlcl9pZCI6MSwicGFzc3dvcmQiOiIxMjM0NTYiLCJ1c2VybmFtZSI6ImRvbmciLCJmdWxsX25hbWUiOiJkb25nIiwicGVybWlzc2lvbnMiOltdfQ.Xkb_J8MWXkwGUcBF9bpp2Ccxp8nFPtRzFzOBeboHmg0
}
const (
ErrorReason_ServerBusy = "服务器繁忙"
ErrorReason_ReLogin = "请重新登陆"
)
/**
* @Author GCG
* @Description: 功能描述(业务测试,用于真正的开发业务验证是否用户合法,然后执行Hello业务)
* @Date : 2020/11/12
*/
func sayHello(c *gin.Context) {
strToken := c.Query("token")
fmt.Println("token=", strToken)
claim, err := verifyAction(strToken)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
if claim.Role == "admin" {
fmt.Println("该用户是管理员可以执行相关的业务")
fmt.Println("--------------执行Hello业务--------------")
}
data, _ := json.Marshal(claim)
c.String(http.StatusOK, "用户的信息:", string(data))
}
/**
* @Author GCG
* @Description: 功能描述(根据自己的需求设计相关结构,用于再次获取用户权限的信息 jwt.StandardClaims必须要含有)
* @Date : 2020/11/12
*/
type JWTClaims struct { // token里面添加用户信息,验证token后可能会用到用户信息
jwt.StandardClaims
UserID int `json:"user_id"`
Password string `json:"password"`
Username string `json:"username"`
Role string `josn:"role"`
FullName string `json:"full_name"`
Permissions []string `json:"permissions"`
}
/**
* @Author GCG
* @Description: 功能描述(全局变量秘钥和失效时长)
* @Date : 2020/11/12
*/
var (
Secret = "dong_tech" // 加盐:签名秘钥
ExpireTime = 100 // token有效期 10秒
)
/**
* @Author GCG
* @Description: 功能描述(通过Login登录获取token)
* @Date : 2020/11/12
*/
func login(c *gin.Context) {
username := c.Query("username")
password := c.Query("password")
role := c.Query("role")
fmt.Println("username=", username)
fmt.Println("password=", password)
fmt.Println("role=", role)
claims := &JWTClaims{
UserID: 1,
Username: username,
Password: password,
Role: role,
FullName: username,
Permissions: []string{},
}
claims.IssuedAt = time.Now().Unix() // 封装claims从1970到现在的总秒数表示token获取的时间
claims.ExpiresAt = time.Now().Add(time.Second * time.Duration(ExpireTime)).Unix() // 封装claims表示token失效的时间
signedToken, err := getToken(claims) //获得token签名的认证
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
c.String(http.StatusOK, signedToken)
}
/**
* @Author GCG
* @Description: 功能描述(验证token的使用测试)
* @Date : 2020/11/12
*/
func verify(c *gin.Context) {
strToken := c.Query("token")
fmt.Println("token=", strToken)
claim, err := verifyAction(strToken)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
c.String(http.StatusOK, "verify,", claim.Username, claim.Role)
}
/**
* @Author GCG
* @Description: 功能描述(刷新JWT的token)
* @Date : 2020/11/12
*/
func refresh(c *gin.Context) {
strToken := c.Query("token")
claims, err := verifyAction(strToken)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
claims.ExpiresAt = time.Now().Unix() + (claims.ExpiresAt - claims.IssuedAt) // 失效的时间
signedToken, err := getToken(claims)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
c.String(http.StatusOK, signedToken)
}
/**
* @Author GCG
* @Description: 功能描述(通过用户发送的token进行验证获取用户信息)
* @Date : 2020/11/12
*/
func verifyAction(strToken string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(strToken, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(Secret), nil //该函数是实现将前端携带的Token进行相关的解密,获得加密前的token数据
})
if err != nil {
return nil, errors.New(ErrorReason_ReLogin)
}
claims, ok := token.Claims.(*JWTClaims) // 根据解密的token获取claims声明信息
if !ok {
return nil, errors.New(ErrorReason_ReLogin)
}
if err := token.Claims.Valid(); err != nil {
return nil, errors.New(ErrorReason_ReLogin)
}
return claims, nil
}
/**
* @Author GCG
* @Description: 功能描述(获取Token函数)
* @Date : 2020/11/12
*/
func getToken(claims *JWTClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) //根据指定的签名方法,和claims进行加密,生成token
signedToken, err := token.SignedString([]byte(Secret)) //生成的Token 进行秘钥签名使用Secret进行加密
if err != nil {
return "", errors.New(ErrorReason_ServerBusy)
}
return signedToken, nil
}