Предыстория: В сегодняшней среде, где интерфейс и сервер разделены и разработаны, нам необходимо решить некоторые проблемы, связанные с входом в систему, пост-аутентификацией и аутентификацией.Обычным решением является использование заголовка запроса для переноса токена. В этой статье в основном используется язык Голанг.jwt-goДля реализации бэкэнда логики аутентификации токена.
JSON Web Token(JWT)
Это стандартная спецификация для аутентификации и аутентификации между клиентом и сервером HTTP.Использование JWT позволяет нам передавать безопасную и надежную информацию между пользователями и серверами.
начать учитьсяJWTПеред этим мы можем сначала понять несколько ранних вариантов.
Разница между токеном, файлом cookie и сеансом
Cookie
Файлы cookie всегда хранятся в клиенте, по месту хранения в клиенте их можно разделить на内存Cookie
и硬盘Cookie
.
Файл cookie памяти поддерживается браузером и хранится в памяти, он исчезает после закрытия браузера, и время его существования непродолжительно. Файл cookie жесткого диска хранится на жестком диске и имеет срок действия. Если пользователь не очистит его вручную или не будет достигнуто время истечения срока действия, файл cookie жесткого диска не будет удален, и время его существования является долгосрочным. Поэтому по времени существования его можно разделить на非持久Cookie和持久Cookie
.
Файл cookie — это очень специфическая вещь, которая относится к типу данных, которые могут постоянно храниться в браузере, и представляет собой только функцию хранения данных, реализованную браузером.
cookie由服务器生成,发送给浏览器
, браузер сохраняет файл cookie в текстовом файле в определенном каталоге в виде пары «ключ-значение» и отправляет файл cookie на сервер при следующем запросе того же веб-сайта. Поскольку файлы cookie хранятся на клиенте, браузеры добавляют некоторые ограничения, чтобы гарантировать, что файлы cookie не могут быть использованы злонамеренно и не занимают слишком много места на диске, поэтому количество файлов cookie для каждого домена ограничено.
Session
Сессия буквально означает сессию, которая в основном используется для идентификации личности. Например, когда служба API без сохранения состояния запрашивает базу данных несколько раз, как она узнает, что это один и тот же пользователь? Это можно сделать с помощью механизма сеанса. Сервер должен знать, кто в данный момент отправляет запрос самому себе.
Чтобы различать запросы клиентов,服务端会给具体的客户端生成身份标识session
, и тогда каждый раз, когда клиент отправляет запрос на сервер, он будет приносить этот «идентификатор личности», и сервер будет знать, от кого пришел запрос.
Что касается того, как клиент сохраняет идентификатор, может быть много способов.Для браузеров обычно используетсяcookie
Путь
Сервер использует сессию для временного сохранения пользовательской информации на сервере, и она будет уничтожена, когда пользователь покинет сайт.Этот метод хранения учетных данных более безопасен, чем cookie, но сессия имеет недостаток: если веб-сервер выполняет балансировка нагрузки, следующая Когда запрос операции переходит на другой сервер, сессия будет потеряна.
Поэтому компании обычно используютredis,memcached
Промежуточное программное обеспечение кэша используется для обеспечения совместного использования сеансов. В настоящее время веб-сервер полностью не имеет состояния. Все учетные данные пользователя могут быть доступны через общие сеансы. Механизм истечения срока действия и уничтожения текущего сеанса должен контролироваться пользователем.
Token
Токен означает «токен», который является методом аутентификации личности пользователя.Простейший токен состоит из:uid(用户唯一标识)
+time(当前时间戳)
+sign(签名,由token的前几位+盐以哈希算法压缩成一定长度的十六进制字符串)
, а так же в токен можно поставить неизменяемые параметры
Здесь мы в основном хотим говорить оJson Web Token
, что является темой этой статьи: JWT
Введение в Json-Web-Token (JWT)
Вообще говоря, после того, как пользователь зарегистрируется и войдет в систему, генерируется и возвращается браузеру токен jwt, который передается, когда браузер запрашивает данные с сервера.token
, на стороне сервера используетсяsignature
Декодируйте метод, определенный в , а затем проанализируйте и проверьте токен.
Компоненты токена JWT
- заголовок: используется для указания используемого алгоритма (HMAC SHA256 RSA) и типа токена (например, JWT)
- Полезная нагрузка: Содержит объявления (требования), объявления обычно представляют собой объявления информации о пользователе или других данных, таких как идентификатор пользователя, имя, адрес электронной почты и т. д. Объявления можно разделить на три типа: зарегистрированные, общедоступные, частные
- подпись: используется для обеспечения подлинности JWT, могут использоваться разные алгоритмы
header
{
"alg": "HS256",
"typ": "JWT"
}
Base64 кодирует приведенный выше json, чтобы получить первую часть JWT.
payload
- зарегистрированные заявки: предопределенные заявки, обычно содержат некоторые предопределенные поля, такие как время истечения срока действия, тема и т. д. (iss: эмитент, exp: срок действия, sub: тема, aud: аудитория)
- Общественные претензии: определенные поля могут быть расположены раскрыты
- частные претензии: используются для обмена информацией между сторонами, которые используют их единообразно
{
"sub": "xxx-api",
"name": "bgbiao.top",
"admin": true
}
После того, как base64 кодирует json в части полезной нагрузки, вы можете получить вторую часть JWT.
注意:
Не размещайте конфиденциальную информацию в заголовке и полезной нагрузке, если только сама информация не была десенсибилизирована.
signature
Чтобы получить часть подписи, должен быть закодированный заголовок и полезная нагрузка, а также секретный ключ.Алгоритм подписи использует тот, который указан в заголовке, а затем подписывает его.
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
подпись用于验证消息在传递过程中有没有被更改
, а для токенов, подписанных закрытым ключом, он также может проверить, является ли отправитель JWT тем, за кого он себя выдает.
существуетjwt.ioНа веб-сайте предоставляются некоторые инструменты для кодирования, проверки и генерации токенов JWT.
На следующем рисунке показаны компоненты типичного jwt-токена.
Когда использовать JWT
- Авторизация: в типичном сценарии запрошенный пользователем токен содержит маршруты, службы и ресурсы, разрешенные токеном. Единый вход на самом деле является широко используемой функцией JWT.
- Обмен информацией: веб-токены JSON, безусловно, являются отличным способом безопасной передачи информации между сторонами.Поскольку JWT могут быть подписаны, например, с помощью пар открытого и закрытого ключей, вы можете убедиться, что отправитель является тем, за кого себя выдает. Кроме того, поскольку подпись рассчитывается с использованием заголовка и полезной нагрузки, вы также можете убедиться, что содержимое не было подделано.
Как работают JWT (JSON Web Tokens)
Поэтому в основном весь процесс делится на два этапа: на первом этапе клиент получает токен с сервера, а на втором этапе клиент берет токен для запроса соответствующих ресурсов.
Обычно важнее то, как сервер генерирует токен в соответствии с заданными правилами.
Во время аутентификации, после того как пользователь успешно вошел в систему со своими учетными данными, будет возвращен веб-токен JSON.
После этого токен является учетными данными пользователя, и вы должны быть очень осторожны, чтобы предотвратить проблемы с безопасностью.
В общем, не следует хранить токен дольше, чем он вам нужен.
Всякий раз, когда пользователь хочет получить доступ к защищенному маршруту или ресурсу, пользовательский агент (обычно браузер) должен передать JWT, как правило, в заголовке авторизации, используя схему Bearer:Authorization: Bearer <token>
Защищенный маршрут на сервере проверит, действителен ли JWT в заголовке авторизации, и если он действителен, пользователь может получить доступ к защищенному ресурсу. Если JWT содержит достаточно необходимых данных, то потребность в запросах к базе данных для некоторых операций может быть уменьшена, хотя это может быть не всегда так.
Если токен отправляется в заголовке авторизации, то совместное использование ресурсов между источниками (CORS) не будет проблемой, поскольку оно не использует файлы cookie.
- Клиент запрашивает авторизацию из интерфейса авторизации
- После авторизации сервер возвращает клиенту токен доступа.
- Клиент использует токен доступа для доступа к защищенному ресурсу
Аутентификация личности Iken и аутентификация на основе сервера
1. На основе сервера аутентификации
Я упомянул разницу между сеансом, файлом cookie и токеном.В прошлом традиционный метод заключался в аутентификации пользователя на основе сеанса, хранящегося на сервере, но обычно возникают следующие проблемы:
- Сеансы: после прохождения аутентификации данные сеанса пользователя должны быть сохранены в памяти.По мере увеличения числа аутентифицированных пользователей накладные расходы памяти будут большими.
- Масштабируемость: поскольку сеанс хранится в памяти, масштабируемость будет ограничена, хотя Redis и memcached можно использовать для кэширования данных позже.
- CORS: когда несколько терминалов обращаются к одним и тем же данным, они могут столкнуться с проблемой запрещенных запросов.
- CSRF: пользователи уязвимы для CSRF-атак
2. Сходства и различия между сеансом и токеном JWT
Оба могут хранить информацию, связанную с пользователем, но сеанс хранится на сервере, а JWT хранится на клиенте.
3. Как работает аутентификация на основе токенов
Аутентификация на основе токенов не имеет состояния, и никакая информация о пользователе не хранится на сервере или в сеансе (это хорошее решение проблемы общих сеансов).
- Пользователь запрашивает получение токена с логином и паролем (appId, appKey могут использоваться в данных интерфейса)
- Сервер проверяет учетные данные пользователя и возвращает токен пользователю или клиенту.
- Клиент сохраняет токен и переносит токен в заголовке запроса.
- Сервер проверяет токен и возвращает данные
注意:
- Каждый последующий запрос от клиента должен использовать токен
- Токен должен быть размещен в шапке
- Сервер необходимо настроить для приема запросов для всех доменов:
Access-Control-Allow-Origin: *
4. Преимущества использования токена
- Без сохранения состояния и масштабируемость
- Безопасность: предотвратить атаки CSRF; повторно аутентифицируйте после действия токена
5. Разница между JWT и OAuth
- 1.OAuth2 — это фреймворк авторизации, JWT — это протокол аутентификации
- 2. Независимо от того, какой метод вы используете, не забудьте использовать HTTPS для обеспечения безопасности данных.
- 3.OAuth2 используется в
使用第三方账号登录的情况
(Например, используйте weibo, qq, github для входа в приложение) иJWT是用在前后端分离
, используется, когда вам нужно просто защитить фоновый API
Интегрируйте JWT с помощью Gin framework
На языке Голанг,jwt-goБиблиотека предоставляет некоторые инструменты для кодирования и проверки jwt, поэтому мы можем легко использовать библиотеку для реализации аутентификации токена.
Кроме того, мы также знаемginФреймворк поддерживает определяемое пользователем промежуточное ПО, мы вполне можем инкапсулировать логику, связанную с jwt, в промежуточное ПО, а затем аутентифицировать конкретный интерфейс.
пользовательское промежуточное ПО
Во фреймворке gin проще настроить промежуточное ПО, просто вернитеgin.HandlerFunc
Это полное определение промежуточного программного обеспечения.
Затем мы сначала определяем промежуточное ПО для аутентификации jwt.
// 定义一个JWTAuth的中间件
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 通过http header中的token解析来认证
token := c.Request.Header.Get("token")
if token == "" {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "请求未携带token,无权限访问",
"data": nil,
})
c.Abort()
return
}
log.Print("get token: ", token)
// 初始化一个JWT对象实例,并根据结构体方法来解析token
j := NewJWT()
// 解析token中包含的相关信息(有效载荷)
claims, err := j.ParserToken(token)
if err != nil {
// token过期
if err == TokenExpired {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "token授权已过期,请重新申请授权",
"data": nil,
})
c.Abort()
return
}
// 其他错误
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
"data": nil,
})
c.Abort()
return
}
// 将解析后的有效载荷claims重新写入gin.Context引用对象中
c.Set("claims", claims)
}
}
Определить логику кодирования и декодирования jwt
Согласно упомянутым ранее компонентам jwt-токена иjwt-go
В соответствующих определениях мы можем использовать следующие методы для генерации токенов.
// 定义一个jwt对象
type JWT struct {
// 声明签名信息
SigningKey []byte
}
// 初始化jwt对象
func NewJWT() *JWT {
return &JWT{
[]byte("bgbiao.top"),
}
}
// 自定义有效载荷(这里采用自定义的Name和Email作为有效载荷的一部分)
type CustomClaims struct {
Name string `json:"name"`
Email string `json:"email"`
// StandardClaims结构体实现了Claims接口(Valid()函数)
jwt.StandardClaims
}
// 调用jwt-go库生成token
// 指定编码的算法为jwt.SigningMethodHS256
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
// https://gowalker.org/github.com/dgrijalva/jwt-go#Token
// 返回一个token的结构体指针
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// token解码
func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
// https://gowalker.org/github.com/dgrijalva/jwt-go#ParseWithClaims
// 输入用户自定义的Claims结构体对象,token,以及自定义函数来解析token字符串为jwt的Token结构体指针
// Keyfunc是匿名函数类型: type Keyfunc func(*Token) (interface{}, error)
// func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {}
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
// https://gowalker.org/github.com/dgrijalva/jwt-go#ValidationError
// jwt.ValidationError 是一个无效token的错误结构
if ve, ok := err.(*jwt.ValidationError); ok {
// ValidationErrorMalformed是一个uint常量,表示token不可用
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, fmt.Errorf("token不可用")
// ValidationErrorExpired表示Token过期
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
return nil, fmt.Errorf("token过期")
// ValidationErrorNotValidYet表示无效token
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, fmt.Errorf("无效的token")
} else {
return nil, fmt.Errorf("token不可用")
}
}
}
// 将token中的claims信息解析出来并断言成用户自定义的有效载荷结构
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("token无效")
}
Определить логику аутентификации при входе
Следующая часть - это специфическая логика обычного API. Например, когда пользователь может быть проверен, запрос аутентификации не генерируется для генерации токена.
// 定义登陆逻辑
// model.LoginReq中定义了登陆的请求体(name,passwd)
func Login(c *gin.Context) {
var loginReq model.LoginReq
if c.BindJSON(&loginReq) == nil {
// 登陆逻辑校验(查库,验证用户是否存在以及登陆信息是否正确)
isPass, user, err := model.LoginCheck(loginReq)
// 验证通过后为该次请求生成token
if isPass {
generateToken(c, user)
} else {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "验证失败" + err.Error(),
"data": nil,
})
}
} else {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "用户数据解析失败",
"data": nil,
})
}
}
// token生成器
// md 为上面定义好的middleware中间件
func generateToken(c *gin.Context, user model.User) {
// 构造SignKey: 签名和解签名需要使用一个值
j := md.NewJWT()
// 构造用户claims信息(负荷)
claims := md.CustomClaims{
user.Name,
user.Email,
jwtgo.StandardClaims{
NotBefore: int64(time.Now().Unix() - 1000), // 签名生效时间
ExpiresAt: int64(time.Now().Unix() + 3600), // 签名过期时间
Issuer: "bgbiao.top", // 签名颁发者
},
}
// 根据claims生成token对象
token, err := j.CreateToken(claims)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
"data": nil,
})
}
log.Println(token)
// 封装一个响应数据,返回用户名和token
data := LoginResult{
Name: user.Name,
Token: token,
}
c.JSON(http.StatusOK, gin.H{
"status": 0,
"msg": "登陆成功",
"data": data,
})
return
}
Определить обычный для проверки
// 定义一个普通controller函数,作为一个验证接口逻辑
func GetDataByTime(c *gin.Context) {
// 上面我们在JWTAuth()中间中将'claims'写入到gin.Context的指针对象中,因此在这里可以将之解析出来
claims := c.MustGet("claims").(*md.CustomClaims)
if claims != nil {
c.JSON(http.StatusOK, gin.H{
"status": 0,
"msg": "token有效",
"data": claims,
})
}
}
// 在主函数中定义路由规则
router := gin.Default()
v1 := router.Group("/apis/v1/")
{
v1.POST("/register", controller.RegisterUser)
v1.POST("/login", controller.Login)
}
// secure v1
sv1 := router.Group("/apis/v1/auth/")
// 加载自定义的JWTAuth()中间件,在整个sv1的路由组中都生效
sv1.Use(md.JWTAuth())
{
sv1.GET("/time", controller.GetDataByTime)
}
router.Run(":8081")
Проверьте интерфейс после использования JWT
# 运行项目
$ go run main.go
127.0.0.1
13306
root:bgbiao.top@tcp(127.0.0.1:13306)/test_api?charset=utf8mb4&parseTime=True&loc=Local
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /apis/v1/register --> warnning-trigger/controller.RegisterUser (3 handlers)
[GIN-debug] POST /apis/v1/login --> warnning-trigger/controller.Login (3 handlers)
[GIN-debug] GET /apis/v1/auth/time --> warnning-trigger/controller.GetDataByTime (4 handlers)
[GIN-debug] Listening and serving HTTP on :8081
# 注册用户
$ curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{
"name": "hahaha1",
"password": "hahaha1",
"email": "hahaha1@bgbiao.top",
"phone": 10000000000
}' \
'http://localhost:8081/apis/v1/register'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 15 Mar 2020 07:09:28 GMT
Content-Length: 41
{"data":null,"msg":"success ","status":0}%
# 登陆用户以获取token
$ curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{
"name":"hahaha1",
"password":"hahaha1"
}' \
'http://localhost:8081/apis/v1/login'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 15 Mar 2020 07:10:41 GMT
Content-Length: 290
{"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImhhaGFoYTEiLCJlbWFpbCI6ImhhaGFoYTFAYmdiaWFvLnRvcCIsImV4cCI6MTU4NDI1OTg0MSwiaXNzIjoiYmdiaWFvLnRvcCIsIm5iZiI6MTU4NDI1NTI0MX0.HNXSKISZTqzjKd705BOSARmgI8FGGe4Sv-Ma3_iK1Xw","name":"hahaha1"},"msg":"登陆成功","status":0}
# 访问需要认证的接口
# 因为我们对/apis/v1/auth/的分组路由中加载了jwt的middleware,因此该分组下的api都需要使用jwt-token认证
$ curl http://localhost:8081/apis/v1/auth/time
{"data":null,"msg":"请求未携带token,无权限访问","status":-1}%
# 使用token认证
$ curl http://localhost:8081/apis/v1/auth/time -H 'token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImhhaGFoYTEiLCJlbWFpbCI6ImhhaGFoYTFAYmdiaWFvLnRvcCIsImV4cCI6MTU4NDI1OTg0MSwiaXNzIjoiYmdiaWFvLnRvcCIsIm5iZiI6MTU4NDI1NTI0MX0.HNXSKISZTqzjKd705BOSARmgI8FGGe4Sv-Ma3_iK1Xw'
{"data":{"userName":"hahaha1","email":"hahaha1@bgbiao.top","exp":1584259841,"iss":"bgbiao.top","nbf":1584255241},"msg":"token有效","status":0}%
В этой статье используетсяmdniceнабор текста