文章目录
服务器存储与客户端存储
基于服务器的身份认证方式存在一些问题:
- Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。
- Scalability : 由于Session是在内存中的,这就带来一些扩展性的问题。
- CORS : 扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
- CSRF : 用户很容易受到CSRF攻击。
客户端存储
JWT与Session的差异 相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。
Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。
JWT的实现
-
playload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分 -
标准中注册的声明
公共的声明
私有的声明 -
标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
Python语言实现
# -*- coding: utf8 -*-
from typing import Optional
import datetime
import jwt
from werkzeug.local import LocalProxy
from flask import current_app, request, has_app_context, _app_ctx_stack
from . import exceptions
import os
registered_claims = {'iss', 'sub', 'aud', 'exp', 'nbf', 'iat', 'jti'}
class _AuthObject:
def __init__(self, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val)
def __getattr__(self, item):
return None
def __repr__(self):
return str(self.__dict__)
def encode_token(iss: Optional[str] = None,
expire: Optional[datetime.timedelta] = None,
**kwargs):
"""
encode jwt token
:param iss:
:param expire: timedelta object, set the expire time of jwt
:param kwargs:
:return:
"""
try:
header = {'algorithm': 'HS256', 'type': 'JWT'}
payload = {
'iat': datetime.datetime.utcnow(),
'iss': iss or 'website.com',
}
# update public claim names
payload.update(**kwargs)
# if set jwt expire time, update exp claim
if expire:
payload['exp'] = payload['iat'] + expire
# gen jwt token
token = jwt.encode(payload, os.getenv('SECRET_KEY'), headers=header)
return token
except Exception:
raise exceptions.Internal(message='无效token')
def decode_token(token, verify_exp: bool = False):
"""
decode jwt token
:param token:
:param verify_exp:
:return:
"""
try:
tmp = {}
payload = jwt.decode(token, os.getenv('SECRET_KEY'), options={
'verify_exp': verify_exp})
# get the public claim names
for field in payload.keys():
if field in registered_claims:
continue
tmp[field] = payload[field]
return tmp
except jwt.ExpiredSignatureError:
raise exceptions.Unauthenticated(message='token已过期')
except jwt.InvalidTokenError:
raise exceptions.Unauthenticated(message='无效token')
def _get_auth():
if not has_app_context():
raise RuntimeError(
'No application found. Either work inside a view function or push'
' an application context.'
)
if not hasattr(_app_ctx_stack.top, 'auth'):
# get and validate auth filed in request header
auth_header = request.headers.get('Authorization')
if not auth_header:
raise exceptions.InvalidArgument(message="请求头错误")
auth_attr = auth_header.split(' ')
if not auth_attr or auth_attr[0] != 'JWT' or len(auth_attr) != 2:
raise exceptions.InvalidArgument(message="token格式错误")
payload = decode_token(auth_attr[1])
# set auth data to app context
_app_ctx_stack.top.auth = _AuthObject(**payload)
return getattr(_app_ctx_stack.top, 'auth')
# global variable
current_auth = LocalProxy(lambda: _get_auth())
Flask 与Django
flask 一般在请求钩子中设置 或者 在调用时加入
jwt 使用装饰器
init.py
# -*- coding: utf8 -*-
"""
权限管理,JWT实现
"""
from .permission import auth_control, current_auth
__all__ = ['auth_control', 'current_auth']
# -*- coding: utf8 -*-
from typing import Optional
import datetime
import jwt
from werkzeug.local import LocalProxy
from flask import current_app, request, has_app_context, _app_ctx_stack
from . import exceptions
registered_claims = {'iss', 'sub', 'aud', 'exp', 'nbf', 'iat', 'jti'}
class _AuthObject:
def __init__(self, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val)
def __getattr__(self, item):
return None
def __repr__(self):
return str(self.__dict__)
def encode_token(iss: Optional[str] = None,
expire: Optional[datetime.timedelta] = None,
**kwargs):
"""
encode jwt token
:param iss:
:param expire: timedelta object, set the expire time of jwt
:param kwargs:
:return:
"""
try:
header = {'algorithm': 'HS256', 'type': 'JWT'}
payload = {
'iat': datetime.datetime.utcnow(),
'iss': iss or 'startask',
}
# update public claim names
payload.update(**kwargs)
# if set jwt expire time, update exp claim
if expire:
payload['exp'] = payload['iat'] + expire
# gen jwt token
token = jwt.encode(payload, current_app.config['SECRET_KEY'], headers=header)
return token
except Exception:
raise exceptions.Internal(message='无效token')
def decode_token(token, verify_exp: bool = False):
"""
decode jwt token
:param token:
:param verify_exp:
:return:
"""
try:
tmp = {}
payload = jwt.decode(token, current_app.config['SECRET_KEY'], options={'verify_exp': verify_exp})
# get the public claim names
for field in payload.keys():
if field in registered_claims:
continue
tmp[field] = payload[field]
return tmp
except jwt.ExpiredSignatureError:
raise exceptions.Unauthenticated(message='token已过期')
except jwt.InvalidTokenError:
raise exceptions.Unauthenticated(message='无效token')
def _get_auth():
if not has_app_context():
raise RuntimeError(
'No application found. Either work inside a view function or push'
' an application context.'
)
if not hasattr(_app_ctx_stack.top, 'auth'):
# get and validate auth filed in request header
auth_header = request.headers.get('Authorization')
if not auth_header:
raise exceptions.InvalidArgument(message="请求头错误")
auth_attr = auth_header.split(' ')
if not auth_attr or auth_attr[0] != 'JWT' or len(auth_attr) != 2:
raise exceptions.InvalidArgument(message="token格式错误")
payload = decode_token(auth_attr[1])
# set auth data to app context
_app_ctx_stack.top.auth = _AuthObject(**payload)
return getattr(_app_ctx_stack.top, 'auth')
# global variable
current_auth = LocalProxy(lambda: _get_auth())
直接调用 该方法
http://www.bjhee.com/flask-ad1.html
权限控制装饰器
# -*- coding: utf8 -*-
from functools import wraps
from typing import Optional, List, Tuple
from flask import current_app
from titan.user.enum import RoleEnum
from titan.factory import redis
from titan.cache import SignOutUserListKey
from stardust.exceptions import PermissionDenied, Unauthenticated
from stardust.auth import current_auth
def _access_control(auth_roles: Optional[List[RoleEnum]] = None) -> Tuple[str, RoleEnum]:
id_ = current_auth.id
user_role = RoleEnum(current_auth.role)
env = current_auth.env
if redis.sismember(SignOutUserListKey, id_):
raise PermissionDenied(message='请重新登录')
if not auth_roles:
auth_roles = [RoleEnum.DEFAULT]
if user_role != RoleEnum.ADMIN and user_role not in auth_roles:
raise Unauthenticated(message="无权访问")
if current_app.config['ENV'] != env:
raise Unauthenticated(message="环境有误,请重新登录")
return id_, user_role
def auth_control(roles: Optional[List[RoleEnum]] = None):
def wrapper(func):
@wraps(func)
def wrap_func(*args, **kwargs):
id_, user_role = _access_control(roles)
return func(id_, user_role, *args, **kwargs)
return wrap_func
return wrapper
@blueprint.route('/summary', methods=['GET'])
@auth_control()
def task_issue_summary(*args, **kwargs):
# 获取请求数据,数据校检
data_pb = api_request(issue_pb2.AnnotationIssueSummeryRequestProto)
required = {'task_id', 'container_id'}
variable_check(data_pb, required)
frame_indexes = controller.task_issue_summary(current_auth.id, data_pb.task_id, data_pb.container_id,
data_pb.sampling_id)
resp_dict = dict(frame_indexes=frame_indexes)
return api_response(issue_pb2.AnnotationIssueSummaryResponseProto, resp_dict)
Go语言实现
package main
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
//自定义一个字符串
var jwtkey = []byte("www.topgoer.com")
var str string
type Claims struct {
UserId uint
jwt.StandardClaims
}
func main() {
r := gin.Default()
r.GET("/set", setting)
r.GET("/get", getting)
//监听端口默认为8080
r.Run(":8080")
}
//颁发token
func setting(ctx *gin.Context) {
expireTime := time.Now().Add(7 * 24 * time.Hour)
claims := &Claims{
UserId: 2,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(), //过期时间
IssuedAt: time.Now().Unix(),
Issuer: "127.0.0.1", // 签名颁发者
Subject: "user token", //签名主题
},
}
fmt.Println(claims)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
fmt.Println(token)
tokenString, err := token.SignedString(jwtkey)
fmt.Println(tokenString)
if err != nil {
fmt.Println(err)
}
str = tokenString
ctx.JSON(200, gin.H{"token": tokenString})
}
//解析token
func getting(ctx *gin.Context) {
tokenString := ctx.GetHeader("Authorization")
//vcalidate token formate
if tokenString == "" {
ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"})
ctx.Abort()
return
}
token, claims, err := ParseToken(tokenString)
if err != nil || !token.Valid {
ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"})
ctx.Abort()
return
}
fmt.Println(111)
fmt.Println(claims.UserId)
}
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
Claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, Claims, func(token *jwt.Token) (i interface{}, err error) {
return jwtkey, nil
})
fmt.Println(token, Claims)
return token, Claims, err
}
获取token
JWT 更新状态问题(白名单)
如何让用户无感知获取最新token
总结
优点
因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
它不需要在服务端保存会话信息, 所以它易于应用的扩展
安全相关
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议