中间件编程+jwt认证
在不改变原有方法的基础上,添加自己的业务逻辑。相当于grpc中的拦截器一样,在不改变grpc请求的同时,插入自己的业务。
简单例子
go
func Sum(a, b int) int {
return a + b
}
func LoggerMiddleware(in func(a, b int) int) func(a, b int) int {
return func(a, b int) int {
log.Println("logger middleware")
return in(a, b)
}
}
func main() {
f := Sum
LoggerMiddleware(f)(1, 2)
}
shell
D:\goProject\gobasic\goGin>go run main.go
2023/10/07 12:21:58 logger middleware
gin 中间件编程
Gin 内置支持了中间件的逻辑
使用use。或者路由分组的时候也可以使用
go
r := gin.New()
r.User(gin.Logger(), gin.Recovery())
跨域和jwt认证
什么是跨域
由于浏览器的同源策略限制,进而产生跨域拦截问题。同源策略是浏览器最核心也最基本的安全功能;所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
同源策略分为两种:
- DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
- XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。
同源策略在解决浏览器访问安全的同时,也带来了跨域问题,当一个请求url的协议
、域名
、端口
三者之间任意一个与当前页面url不同即为跨域。
使用cors解决跨域问题
CORS(Cross-origin resource sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。CORS 需要浏览器和服务器同时支持。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)
简单请求
- 请求方法为 HEAD、GET、POST中的一种。
- HTTP头信息不超过一下几种:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)
对于简单请求,浏览器回自动在请求的头部添加一个 Origin
字段来说明本次请求来自哪个源(协议 + 域名 + 端口),服务端则通过这个值判断是否接收本次请求 。如果 Origin
在许可范围内,则服务器返回的响应会多出几个头信息:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, Content-Length
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
实际上后续我们就是通过在服务端配置这些参数来处理跨域请求的。
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE ,或者 Content-Type 字段的类型是 application/json。 或者修改了请求头
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight),预检请求其实就是我们常说的 OPTIONS 请求,表示这个请求是用来询问的。头信息里面,关键字段 Origin ,表示请求来自哪个源,除 Origin 字段,"预检"请求的头信息包括两个特殊字段:
//该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法
Access-Control-Request-Method
//该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段.
Access-Control-Request-Headers
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest
请求,否则就报错。
golang配置cors解决跨域问题
上述介绍了两种跨域请求,其中出现了几种特殊的 Header 字段,CORS 就是通过配置这些字段来解决跨域问题的:
-
Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求 -
Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。 -
Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。 -
Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest 对象的response 只能拿到6个基本字段:Cache-Control 、Content-Language 、Content-Type 、Expires 、Last-Modified、Pragma 。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。 -
Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie
。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可。 -
Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒,在此期间,不用发出另一条预检请求
以gin框架为例子,配置跨域中间件
go
package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func Cors() gin.HandlerFunc {
return cors.New(cors.Config{
// 允许所有域名访问
AllowAllOrigins: true,
// 允许请求头中的字段,
AllowHeaders: []string{
"Origin", "Content-Length", "Content-Type",
},
// 允许请求的方法
AllowMethods: []string{
"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS",
},
})
}
然后在路由分组或者引擎初始化的时候可以添加。 一般在路由分组时候添加
JWT认证中间件
Json Web Token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准RFC7519。JWT一般可以用作独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,特别适用于分布式站点的登录场景。
JWT的构成
plaintext
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
如上面的例子所示,JWT就是一个字符串,由三部分构成:
- Header(头部)
- Payload(数据)
- Signature(签名)
header
JWT的头部承载两个信息:
-
声明类型,这里是JWT
-
声明加密的算法
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行Base64编码(该编码是可以对称解码的),构成了第一部分。
plaintext
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload
载荷就是存放有效信息的地方。定义细节如下:
iss:令牌颁发者。表示该令牌由谁创建,该声明是一个字符串
sub: Subject Identifier,iss提供的终端用户的标识,在iss范围内唯一,最长为255个ASCII个字符,区分大小写
aud:Audience(s),令牌的受众,分大小写的字符串数组
exp:Expiration time,令牌的过期时间戳。超过此时间的token会作废, 该声明是一个整数,是1970年1月1日以来的秒数
iat: 令牌的颁发时间,该声明是一个整数,是1970年1月1日以来的秒数
jti: 令牌的唯一标识,该声明的值在令牌颁发者创建的每一个令牌中都是唯一的,为了防止冲突,它通常是一个密码学随机值。这个值相当于向结构化令牌中加入了一个攻击者无法获得的随机熵组件,有利于防止令牌猜测攻击和重放攻击。
也可以新增用户系统需要使用的自定义字段
然后将其进行Base64编码,得到Jwt的第二部分:
plaintext
JTdCJTBBJTIwJTIwJTIyc3ViJTIyJTNBJTIwJTIyMTIzNDU2Nzg5MCUyMiUyQyUwQSUyMCUyMCUyMm5hbWUlMjIlM0ElMjAlMjJKb2huJTIwRG9lJTIyJTBBJTdE
Signature
这个部分需要Base64编码后的Header和Base64编码后的Payload使用 .
连接组成的字符串,然后通过Header中声明的加密方式进行加密($secret
表示用户的私钥),然后就构成了jwt的第三部分。
plaintext
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');
将这三部分用 .
连接成一个完整的字符串,就构成了 jwt
JWT几个特点
- JWT 默认是不加密,不能将秘密数据写入 JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用HTTPS 协议传输。
跨域配置 + jwt认证案例
jwt加签验签
go
package jwt_plugin
import "github.com/golang-jwt/jwt/v4"
// 实际项目放配置文件
var key = "zxvfffsa123"
// payload 添加自定义字段
type Data struct {
Name string `json:"name"`
Age int `json:"age"`
Gender int `json:"gender"`
jwt.RegisteredClaims
}
//签名
func Sign(data jwt.Claims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, data)
sign, err := token.SignedString([]byte(key))
if err != nil {
return "", err
}
return sign, err
}
// 验签
func Verify(sign string, data jwt.Claims) error {
_, err := jwt.ParseWithClaims(sign, data, func(token *jwt.Token) (interface{}, error) {
return []byte(key), nil
})
return err
}
登录认证
api + 路由
go
package routers
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"goGin/jwt_plugin"
"net/http"
"time"
)
func initLogin(group *gin.RouterGroup) {
v1 := group.Group("/v1")
{
// 正常应该post登录
v1.GET("/login", login.Login)
}
}
func Login(c *gin.Context) {
data := jwt_plugin.Data{
Name: "golang",
Age: 17,
Gender: 1,
RegisteredClaims: jwt.RegisteredClaims{
// 一小时过期
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
//签发时间
IssuedAt: jwt.NewNumericDate(time.Now()),
// 生效时间
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
sign, err := jwt_plugin.Sign(data)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK, gin.H{
"access_token": sign,
})
}
路由初始化
go
package routers
import (
"github.com/gin-gonic/gin"
"goGin/middleware"
"goGin/user"
)
func InitRouters(r *gin.Engine) {
//使用路由分组
// 添加跨域
api := r.Group("/api")
api.Use(middleware.Cors(), middleware.Auth())
initUser(api)
// 除了登录以外的 不需要认证
notAuthApi := r.Group("/api")
notAuthApi.Use(middleware.Cors())
initLogin(notAuthApi)
}
func initUser(group *gin.RouterGroup) {
// 路由分组
v1 := group.Group("/v1")
{
// /api/v1/course
// 路径携带参数
v1.GET("/user/:id", user.Get)
v1.POST("/user/:id", user.Add)
}
// v2版本
v2 := group.Group("v2")
{
v2.GET("/user/:id", user.GetV2)
v2.POST("/user/:id", user.AddV2)
}
}
其他API
user.go
go
package user
import (
"github.com/gin-gonic/gin"
"net/http"
)
type user struct {
Name string `json:"name" binding:"required, alphaunicode"`
Age int `json:"age" binding:"omitempty, number"` // 允许为空
Phone string `json:"phone" binding:"omitempty, e164"`
Email string `json:"email" binding:"omitempty, email"`
}
func Add(c *gin.Context) {
req := &user{}
err := c.ShouldBindJSON(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
}
// 都是gin.context作为入参
c.JSON(http.StatusOK, gin.H{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"req": req,
})
}
func Get(c *gin.Context) {
id := c.Param("id")
// 都是gin.context作为入参
c.JSON(http.StatusOK, gin.H{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"id": id,
})
}
func AddV2(c *gin.Context) {
// 都是gin.context作为入参
c.JSON(http.StatusOK, gin.H{
"method": c.Request.Method,
"path": c.Request.URL.Path,
})
}
func GetV2(c *gin.Context) {
authInfo, _:= c.Get("auth_info")
// 都是gin.context作为入参
c.JSON(http.StatusOK, gin.H{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"auth_info": authInfo,
})
}
测试
认证,验签
不带token