一、jwt介绍
1、什么是jwt
JWT(JSON Web Token)是一种用于在网络应用中传递信息的开放标准(RFC 7519)。它通过在用户和服务器之间传递的信息生成具有一定结构的令牌,这些令牌可以袐用于身份验证和信息传递。它是一种前后端登陆认证的方案,区别于之前的 cookie,session。
2、JWT结构
一个JWT令牌由三部分组成,这三部分分别是:
- Header(头部):包含了令牌的类型(即JWT)和所使用的加密算法类型。
- Payload(荷载):包含了要传输的信息,如用户ID、角色等,以及其他自定义的数据。
- Signature(签名):由头部、负载和密钥一起加密生成的签名,用于验证令牌的完整性和真实性。
这三部分通过.
符号连接在一起,形成了一个完整的JWT令牌。
(1)Header
- 报头通常由两部分组成: Token的类型(即 JWT)和所使用的签名算法(如 HMAC SHA256或 RSA)。
- 例如:
{"alg": "HS256", "typ": "JWT"}
- 例如:
- 最终这个 JSON 将由base64进行加密(该加密是可以对称解密的),用于构成 JWT 的第一部分,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
就是base64加密后的结果。
(2)Payload
- 令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分。
- 简单来说,Payload 中包含了用户所需要的信息,如用户id、用户名、权限等。(由于Base64可以被解码,所以不要放用户的敏感信息,如密码!)
- 例如:
{ "sub": "1234567890", "name": "John Doe", "admin": true}
- iss:JWT 的签发者/发行人
- sub:该 JWT 面向的用户
- aud:接收方
- exp:JWT 的过期时间,必须大于签发时间
- nbf:JWT 的生效时间,在这个时间之前该 token 都是无效的
- iat:JWT 的签发时间
- jti:JWT 唯一身份标识,主要用来作为一次性 toekn,从而避免重放攻击
(3) Signature
-
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及提供的密钥,使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。(header、payload 被篡改时,由于签名基于header和payload生成,所以将无法通过验证)
-
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
-
所以要创建Signature,您必须获取编码的标头(header)、编码的有效载荷(payload)、secret、标头中指定的算法,并对其进行签名。
(4)补充 摘要算法
摘要算法(Hash Algorithm)是一种将任意长度的数据转换为固定长度摘要(哈希值)的算法。这个哈希值通常是一个固定长度的字节序列,通常用于唯一标识输入数据。摘要算法的主要特点包括:
-
固定长度输出:无论输入数据的长度如何,摘要算法都会生成一个固定长度的哈希值。
-
唯一性:对于不同的输入数据,摘要算法应该生成不同的哈希值。这意味着即使输入数据仅有微小的变化,生成的哈希值也会完全不同。
-
不可逆性:摘要算法是单向的,即从哈希值无法还原出原始数据。这意味着无法通过哈希值来获取原始数据的内容。
-
一致性:相同的输入数据应该始终生成相同的哈希值。
-
快速计算:摘要算法通常设计成能够在较短的时间内计算出哈希值。
常见的摘要算法包括MD5(Message-Digest Algorithm 5)、SHA-1(Secure Hash Algorithm 1)、SHA-256等。然而,由于MD5和SHA-1存在碰撞(collision)风险,即不同的输入可能生成相同的哈希值,因此在安全敏感的应用中,通常推荐使用更安全的算法,如SHA-256、SHA-384或SHA-512。
摘要算法在密码存储、数据完整性验证、数字签名、消息验证等领域有着广泛的应用。通过比较哈希值,可以验证数据在传输过程中是否被篡改,或者验证数据的完整性,确保数据的安全性和可靠性。
(5)小结
- 简单来说就是分为两个阶段:
- 签发阶段:通过头和荷载 使用 某种加密方式[HS256,md5,sha1]加密得到。
- 校验阶段:拿到token,取出第一和第二部分—> 通过之前同样的加密方式加密得到新签名—> 用新签名和第三段(传入的老签名)比较—> 如果一样—> 说明没有被篡改过—> 信任—> 如果不一样—> 说明是被篡改了,或模拟生成的—> 不能信任,返回错误。
3、JWT认证流程
- 首先,前端通过 Web 表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个 HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为 JWT Payload(荷载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在 localStorage 或 sessionStorage 上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将 JWT 放入 HTTP Header 中的 Authorization 位。(解决 XSS 和 XSRF 问题)
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
- 验证通过后,后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
二、base64使用
上面说到JWT里面用到了Base64编码,所以这部分我们来大致了解一下这种编码格式的使用。
要注意base64不是加密方案,只是编码方案,没有加密。
1、什么是base64?
Base64是一种用于将二进制数据编码为文本的方法,使得数据可以在网络上传输或存储时更易处理。Base64编码将二进制数据转换为由64个不同ASCII字符组成的文本字符串,这些字符包括大小写字母、数字和一些特殊符号。
编码原理为:Base64编码将每3个字节的数据转换为4个Base64字符。如果数据长度不是3的倍数,会在末尾添加一个或两个等号作为填充字符。
数据大小增加:Base64编码会使数据大小增加约1/3,因为每3个字节的数据会被编码为4个字符。
2、base64 有什么用?
-
Base64常用于在网络传输中传递二进制数据,如在网页中嵌入图片、在JSON数据中传输二进制数据等。
-
后端把图片使用base64编码,然后传给前端
-
这节内容讲的是在jwt中使用
3、案例使用
常用方法
方法 | 说明 |
---|---|
encode,decode | 专门用来编码和解码文件的,也可以对·StringIO里的数据做编解码 |
encodestring,decodestring | 用来编码和解码字符串 |
b64encode,b64decode | 用来编码和解码字符串 |
urlsafe_b64encode,urlsafe_b64decode | 用来对url进行base64编解码的 |
实例演示
import json
import base64
userinfo = {'name': 'xiao', 'age': 19}
userinfo_str = json.dumps(userinfo)
# base64编码
res = base64.b64encode(userinfo_str.encode(encoding='utf-8'))
print(res) # b'eyJuYW1lIjogInhpYW8iLCAiYWdlIjogMTl9'
# base64 解码
s = 'eyJuYW1lIjogInhpYW8iLCAiYWdlIjogMTl9'
res1 = base64.b64decode(s)
print(res1)
# b'{"name": "xiao", "age": 19}'
# bytes格式字符串--》json.loads--->字典
# 保存图片
s='....'
res=base64.b64decode(s.split(',')[-1])
with open('a.png','wb') as f:
f.write(res)
三、simple-jwt使用
1、为什么要会使用simple-jwt
因为在项目中经常会使用jwt认证方案,而且所有web框架都可能采用这种认证方案,非常常用。
2、simple-jwt 快速使用
(1)安装
pip install djangorestframework-simplejwt
(2)登录签发token
-
登录默认使用auth的user表,先创建个用户,保证我们有用户可以登录
-
路由配置
from rest_framework_simplejwt.views import token_obtain_pair
path('login/', token_obtain_pair),
- 访问
127.0.0.1:8000/app01/login/
,只要使用正确的用户名和密码就可以登录
(3)认证阶段
- 视图类
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
class UserTokenView(GenericViewSet, mixins.CreateModelMixin):
# 必须登陆后才能新增
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
四、simple-jwt配置文件
1、JWT默认配置
# 这些配置在rest_framework_simplejwt的settings文件里有
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Access Token的有效期
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token的有效期
# 对于大部分情况,设置以上两项就可以了,以下为默认配置项目,可根据需要进行调整
# 是否自动刷新Refresh Token
'ROTATE_REFRESH_TOKENS': False,
# 刷新Refresh Token时是否将旧Token加入黑名单,如果设置为False,则旧的刷新令牌仍然可以用于获取新的访问令牌。需要将'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中
'BLACKLIST_AFTER_ROTATION': False,
'ALGORITHM': 'HS256', # 加密算法
'SIGNING_KEY': settings.SECRET_KEY, # 签名密匙,这里使用Django的SECRET_KEY
# 如为True,则在每次使用访问令牌进行身份验证时,更新用户最后登录时间
"UPDATE_LAST_LOGIN": False,
# 用于验证JWT签名的密钥返回的内容。可以是字符串形式的密钥,也可以是一个字典。
"VERIFYING_KEY": "",
"AUDIENCE": None,# JWT中的"Audience"声明,用于指定该JWT的预期接收者。
"ISSUER": None, # JWT中的"Issuer"声明,用于指定该JWT的发行者。
"JSON_ENCODER": None, # 用于序列化JWT负载的JSON编码器。默认为Django的JSON编码器。
"JWK_URL": None, # 包含公钥的URL,用于验证JWT签名。
"LEEWAY": 0, # 允许的时钟偏差量,以秒为单位。用于在验证JWT的过期时间和生效时间时考虑时钟偏差。
# 用于指定JWT在HTTP请求头中使用的身份验证方案。默认为"Bearer"
"AUTH_HEADER_TYPES": ("Bearer",),
# 包含JWT的HTTP请求头的名称。默认为"HTTP_AUTHORIZATION"
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
# 用户模型中用作用户ID的字段。默认为"id"。
"USER_ID_FIELD": "id",
# JWT负载中包含用户ID的声明。默认为"user_id"。
"USER_ID_CLAIM": "user_id",
# 用于指定用户身份验证规则的函数或方法。默认使用Django的默认身份验证方法进行身份验证。
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
# 用于指定可以使用的令牌类。默认为"rest_framework_simplejwt.tokens.AccessToken"。
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
# JWT负载中包含令牌类型的声明。默认为"token_type"。
"TOKEN_TYPE_CLAIM": "token_type",
# 用于指定可以使用的用户模型类。默认为"rest_framework_simplejwt.models.TokenUser"。
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
# JWT负载中包含JWT ID的声明。默认为"jti"。
"JTI_CLAIM": "jti",
# 在使用滑动令牌时,JWT负载中包含刷新令牌过期时间的声明。默认为"refresh_exp"。
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
# 滑动令牌的生命周期。默认为5分钟。
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
# 滑动令牌可以用于刷新的时间段。默认为1天。
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
# 用于生成access和刷refresh的序列化器。
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
# 用于刷新访问令牌的序列化器。默认
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
# 用于验证令牌的序列化器。
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
# 用于列出或撤销已失效JWT的序列化器。
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
# 用于生成滑动令牌的序列化器。
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
# 用于刷新滑动令牌的序列化器。
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
2、项目中配置
我们知道在项目中的配置是优先于默认配置的,所以我们可以自己定义这些默认配置,修改参数供我们使用
例如:
from datetime import timedelta
SIMPLE_JWT ={
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Access Token的有效期
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token的有效期
"AUTH_HEADER_TYPES": ("TOKEN",),
}
五、 定制登陆返回格式
如果直接使用simple-jwt的情况下,登录接口是不用我们自己写的,但是返回的格式跟我们的要求有点差距,于是我们可以自己定制返回格式来达到我们的预期。
1、定制成功返回格式
(1)序列化类书写
- 在jwt的默认配置中,下面的序列化类是用于生成access和刷新refresh的序列化器。这也就导致返回的格式只有access和refresh的编码,没有状态码和其他的东西
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
- 所以我们自己写个序列化类,继承TokenObtainPairSerializer,重写validata方法,用来定制序列化的字段
class CommonTokenObtainSerializer(TokenObtainPairSerializer):
# 重写全局钩子---》用原来的用来校验--》返回什么字典,前端登录成功看到的就是什么
def validate(self, attrs):
# 校验还用原来的--》校验用户名和密码
dic = super().validate(attrs) # 校验通过:把校验通过的user放到self.user 中
# 父类的父类中的validate方法:self.user = authenticate(**authenticate_kwargs)
data = {'code': 100,
'msg': '登录成功',
'username': self.user.username,
'refresh': dic.get('refresh'),
'access': dic.get('access')
}
return data
(2)配置文件配置
SIMPLE_JWT ={
'TOKEN_OBTAIN_SERIALIZER':'app01.serializer.CommonTokenObtainSerializer',
'TOKEN_OBTAIN_SERIALIZER':'应用名.序列化类py文件名.序列化类名'
}
(3)最终返回格式
- 前端看到的是
{
"code": 100,
"msg": "登录成功",
"username": "xiao",
"refresh": "...",
"access": "..."
}
2、定制payload格式
- 在我们想要往荷载(payload)中存储数据的话,比如:
- 过期时间、用户id…
- 那我们该怎么去实现这个要求?
(1)序列化类书写
- 首先写个序列化类,继承TokenObtainPairSerializer–》重新validata方法
class CommonTokenObtainSerializer(TokenObtainPairSerializer):
# 重写get_token这个绑定给类的类方法
@classmethod
def get_token(cls, user):
# super()--->代指父类对象--》父类对象调用类的绑定方法
# 对象调用类的绑定方法--->会自动把对象的类传入
token = super().get_token(user)
token['name'] = user.username # 荷载中加入了username
return token
# 重写全局钩子---》用原来的用来校验--》返回什么字典,前端登录成功看到的就是什么
def validate(self, attrs):
# 校验还用原来的--》校验用户名和密码
dic = super().validate(attrs) # 校验通过:把校验通过的user放到self.user 中
# 父类的父类中的validate方法:self.user = authenticate(**authenticate_kwargs)
data = {'code': 100,
'msg': '登录成功',
'username': self.user.username,
'refresh': dic.get('refresh'),
'access': dic.get('access')
}
return data
(2)配置文件配置
SIMPLE_JWT ={
'TOKEN_OBTAIN_SERIALIZER':'app01.serializer.CommonTokenObtainSerializer'
}
(3)最终返回格式
- 虽然前端看到的是仍是这个格式,但是payload
{
"code": 100,
"msg": "登录成功",
"username": "lqz",
"refresh": "",
"access": ""
}
3、简单回顾 绑定方法
(1)函数和方法的区别
- 函数就是原本有几个值就要传几个值
- 方法分为对象方法和类方法,他们可以自动传值
(2)类中的三种方法
- 绑定给对象的方法,没用装饰器装饰的
- 对象调用—> person.speak()—> 可以自动把对象传入
- 类调用对象的绑定方法—> Person.speak(person),可以调用—> 但是这个方法就变成了普通函数,有几个值就要传几个值,第一个参数是类的对象
- 绑定给类的方法,使用classmethod装饰的
- 类来调用—> Person.xx()—> 可以自动把类传入
- 对象也可以调用类的绑定方法: person.xx()—> 会自动把person对象的类传入
- 静态方法,使用staticmethod装饰器的,跟函数一样
- 对象可以调用:有几个值就要传几个值,没有自动传参
- 类可以调用:有几个值就要传几个值,没有自动传参
(3)示例演示
class Person:
def speak(self):
print(f'对象的绑定方法,{self.name}说话了')
@classmethod
def xx(cls):
print(cls, '类的绑定方法,类和对象都可以调用-如果对象调用,会自动把对象类传进来')
@staticmethod
def yy():
print('普通静态方法,谁都可以调用')
person=Person()
person.name='lqz'
# 对象调用对象的绑定方法
person.speak()
# 类调用类的绑定方法
Person.xx()
# 对象或类调用静态方法
person.yy()
Person.yy()
# 不正统:对象掉类的绑定方法--可以掉--》自动把对象的类传入
person.xx()
# 不正统:类调用对象的绑定方法---》就变成了普通函数,有几个值就要传几个值
Person.speak(person)
六、案例展示
1、多方式登录
(1)视图类
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from .serializer import LoginJWTSerializer
class UserView(GenericViewSet):
serializer_class = UserInfoSerializer
@action(methods=['post'], detail=False)
def login(self, request, *args, **kwargs):
# 正常逻辑:取出手机号 / 用户名 / 邮箱 + 密码 - -》去数据校验 - -》校验通过 -->签发token - -》返回给前端
# 高级逻辑:使用序列化类做上述逻辑,并将token返回给前端
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(raise_exception=True): # 执行 三层认证
# 校验通过:会把user,access和refresh都放到序列化类对象中--》返回给前端
# 现在在视图类中----》有个序列化类--》把视图类中变量给序列化类---》序列化类的变量给视图类--》借助于context给前端 [字典]
# context是视图类和序列化列之间沟通的桥梁
refresh_token = serializer.context.get('refresh')
access_token = serializer.context.get('access')
username = serializer.context.get('username')
return Response({'code': 100, 'msg': '登录成功',
'detail': {'username': username, 'refresh_token': refresh_token,
'access_token': access_token}})
else:
return Response({'code': 101, 'msg': '用户名或密码错误'})
(2)路由
from rest_framework_simplejwt.views import token_obtain_pair
router.register('users', UserJWTView, 'users')
urlpatterns = [
# 自己签发token
path('api/v1/', include(router.urls)), # 127.0.0.1:8000/app01/api/v1/users/login/--->post请求
# jwt默认签发token
path('login/', token_obtain_pair), # 127.0.0.1:8000/app01/login/--->post请求
]
(3)序列化类
from rest_framework import serializers
import re
from .models import UserInfo
from rest_framework.exceptions import ValidationError
class UserInfoSerializer(serializers.ModelSerializer):
# 因为username可能是手机号,邮箱,用户名,所以这里用CharField
username = serializers.CharField(max_length=32)
class Meta:
model = UserInfo
fields = ['username', 'password']
def _get_user(self, attrs):
# 校验用户
username = attrs.get('username')
password = attrs.get('password')
# 去数据库 查询用户---》username可能是手机号,邮箱,用户名--》查的字段不一样
if re.match(r'^1[3-9][0-9]{9}$', username):
user = UserInfo.objects.filter(mobile=username).first()
elif re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', username):
user = UserInfo.objects.filter(email=username).first()
else:
user = UserInfo.objects.filter(username=username).first()
print(user)
if user and user.check_password(password):
return user
return ValidationError('用户名或者密码错误')
def validate(self, attrs):
# 取出 手机号/用户名/邮箱+密码--》数据库校验--》校验通过签发 access和refresh,放到context中
user = self._get_user(attrs)
# 签发token--》通过user对象,签发token
refresh = RefreshToken.for_user(user)
self.context["refresh"] = str(refresh)
self.context["access"] = str(refresh.access_token)
self.context['username'] = user.username
return attrs # 必须返回attrs,否则会报错,因为源码中校验了是否为空,如果为空则会抛出异常
(4)总结
- 校验数据,放到序列化类的 validate中,而不放在视图类的方法中了
- 视图类和序列化类直接交互变量可以通过
context
---->serializer.context
user.check_password
必须是auth的user表,校验密码才会使用它- attrs必须返回值,返回空报错
- 视图类的方法校验失败的else中:也要return Response,否则会报错
- 如何签发token?
token = RefreshToken.for_user(user)
self.context['access'] = str(token.access_token)
self.context['refresh'] = str(token)
2、自定义用户表、手动签发和认证
这个案例不使用扩写auth的user表,而是我们自定义表,并且自己写登录签发token,自己写认证类(如果不是自己写的认证类,那么它会跑去auth的user表去验证,所以需要我们自己去写)
(1)创建模型表
class User(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=32)
user_type=models.IntegerField(choices=((1,'注册用户'),(2,'普通管理员'),(3,'超级管理员')),default=1)
(2)路由
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from app01.views import UserOurJWTView, PublishView
router = DefaultRouter()
router.register(r'users', UserOurJWTView, basename='users')
router.register(r'publish', PublishView, basename='publish')
# 127.0.0.1:8000/api/v1/users/login/ -- post请求
# 127.0.0.1:8000/api/v1/publish/ -- get请求
urlpatterns = [
path('api/v1/', include(router.urls)),
]
(3)视图类
class UserOurJWTView(GenericViewSet):
serializer_class = LoginOurJWTSerializer
@action(methods=['POST'],detail=False)
def login(self,request,*args,**kwargs):
serializer=self.get_serializer(data=request.data)
if serializer.is_valid():
refresh = serializer.context.get('refresh')
access = serializer.context.get('access')
return Response({'code': 100, 'msg': '登录成功', 'refresh': refresh, 'access': access})
else:
return Response({'code': 101, 'msg': serializer.errors})
(4)序列化类
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_simplejwt.tokens import RefreshToken
from .models import User
class LoginOurJWTSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
def _get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
user = User.objects.filter(username=username, password=password).first()
if user:
return user
else:
raise ValidationError('用户名或密码错误')
def validate(self, attrs):
user = self._get_user(attrs)
token = RefreshToken.for_user(user)
self.context['access'] = str(token.access_token)
self.context['refresh'] = str(token)
return attrs
(5)认证类
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .models import User
class JWTOurAuth(JWTAuthentication):
def authenticate(self, request):
# 取出用户携带的access---》放请求头中:Authorization
token = request.META.get('HTTP_AUTHORIZATION')
if token:
# 校验token--》validated_token 返回的就是可以信任的payload中的数据
validated_token = self.get_validated_token(token)
user_id = validated_token['user_id']
user = User.objects.filter(pk=user_id).first()
return user, token
else:
raise AuthenticationFailed('请携带登录信息')
(6)查询使用
# 登陆后才能访问
from .authentication import JWTOurAuth
class PublishView(GenericViewSet):
authentication_classes = [JWTOurAuth]
def list(self, request):
return Response('666')
七、权限介绍
1、引入
在前面的三大认证中有一个就是权限,我们自己写过权限类,就比如控制用户是否有权利访问我们的接口,但是之前我们都是直接定死了,普通用户,超级用户,管理员 这几种身份,给不同人设置不同权限,但是在web领域应用的可不止这些,所以我们仍要继续了解。
就比如都是互联网用户,在抖音上实现的acl控制:
- 游客用户只能查看视频,不可以评论
- 登陆用户可以查看和评论
- 当用户粉丝超过1000用户,这个账号又拥有开直播的权限了
而在公司里面,也有不同人对应不同的权限:
- 人事需要招员工
- 财务需要发工资
- 开发组需要开发代码
- 董事会又限制哪些人可以参加
- 开除员工的权限又可以给到哪些人
2、常见的权限控制方案
(1)基于角色的访问控制(Role-Based Access Control,RBAC)
- RBAC 是一种广泛应用的权限管理模型,将权限分配给角色,然后将角色分配给用户。
- 通过角色来管理权限可以简化权限管理和维护,因为权限的变化只需调整角色与权限的关系,而不需要逐个调整用户的权限。
- RBAC 包括三个主要组成部分:用户(User)、角色(Role)和权限(Permission)。
- 用户通过分配角色来获得相应的权限,这种模型适用于组织结构较为清晰的情况。
(2)基于属性的访问控制(Attribute-Based Access Control,ABAC)
- ABAC 是一种动态访问控制模型,根据用户的属性、资源的属性以及环境条件来决定访问权限。
- 这种模型可以更精细地控制访问权限,允许根据更多因素来做出访问控制决策。
- ABAC 可以根据用户的属性(如职务、部门)、资源的属性(如机密级别)、环境条件(如时间、位置)等因素来确定用户是否有权限访问资源。
(3)强制访问控制(Mandatory Access Control,MAC)
- MAC 是一种严格的访问控制模型,由系统管理员设定访问规则,用户无法修改。
- MAC 基于资源的安全级别和用户的授权级别来做出访问控制决策,通常用于处理对高度敏感信息的访问控制。
- 常见的 MAC 包括多级安全(MLS)和标签安全(Label Security),用于保护国家安全、军事机密等重要信息。
(4)自主访问控制(Discretionary Access Control,DAC)
- DAC 是一种灵活的访问控制模型,允许资源的所有者控制资源的访问权限。
- 资源的所有者有权决定谁可以访问资源以及访问级别,这种模型常见于文件系统等环境中。
- DAC 提供了更灵活的权限管理方式,但也可能导致权限管理混乱和安全风险。
(5)基于策略的访问控制(Policy-Based Access Control,PBAC)
- PBAC 是一种基于策略的访问控制模型,通过定义访问策略来控制访问权限。
- 策略可以基于角色、属性、条件等因素,允许管理员根据具体的访问需求定义灵活的访问控制策略。
- PBAC 可以结合其他访问控制模型,提供更细粒度的权限控制。
(6)访问控制列表(Access Control List,ACL)
- 用户 :用户表
- 权限 :权限表
- 用户和权限中间表:是多对多,一个用户有多个权限,一个权限可能对应多个用户
- 将用户直接与权限对接,每个用户有个权限列表
3、Django中的RBAC权限
(1)Django中的RBAC权限介绍
- 作为python写的项目,多半是公司内部的项目,因为可以快速实现,所以一般拿python写RBAC权限控制比较多
- 在Django中,auth+admin就是一套可以基于rbac权限控制的后台管理。基于admin+auth,我们可以快速开发出公司内部的项目(任务管理系统,任务调度系统,物资调配系统等等) ,这种我们就不需要做出前后端分离的项目,而是基于auth+admin就可以实现。
(2)Django中是如何设计RBAC控制的
通过6张表来控制的:
- auth_user # 用户表
- auth_group # 角色表,组表,部门表
- auth_permission # 权限表
- auth_user_group # 中间表:用户和角色多对多
- auth_grou_permission # 中间表:部门和权限
- auth+admin生成的表会多一个:为了更细粒度的区分权限
- 用户和权限多对多:auth_user_user_permission
(3)admin 权限演示
- django 的admin 自己有什么权限,是它自己增加的(权限就是对表的增删查改权限)
- 首先得是超级管理员登录,然后增加了个组[图书看-删-增组]:对图书有 查看,删除,增加权限
- 增加用户 :quan,属于–图书看-删-增组
- quan:
is_superuser:0
# 不是超级用户 - xiao用户是超级管理员,它有所有权限,即便选了组,也有所有权限
重点补充:如果自定义User表后,再另一个项目中采用原生User表,完成数据库迁移时,可能失败
1)卸载Django重新装
2)将django.contrib下面的admin、auth下的数据库迁移记录文件清空
4、基于Django实现自定义RBAC权限
- 不使用Django自带的auth+admin表,自定义表模型及权限认证
(1)项目中简单使用RBAC思想
需求:
- 给不同的用户分配不同的角色,不同的角色操作不同的功能。
表结构分析:
- 用户表(UserInfo):存储用户名密码,用户和角色是多对多关系,使用roles来创建第三张表
- 角色表(Role):存储角色名,角色和权限表是多对多关系,使用permissions来创建第三张表
- 权限表(Permission):存储url和title,用来控制用户可以访问的url路径
表结构架构图:
表模型实现:
# 用户表
class UserInfo(models.Model):
name = models.CharField(max_length=32)
password = models.CharField(max_length=16)
roles = models.ManyToManyField(to='Role')
def __str__(self):
return self.name
# 角色表
class Role(models.Model):
name = models.CharField(max_length=32)
Permissions = models.ManyToManyField(to='Permission')
def __str__(self):
return self.name
# 权限表
class Permission(models.Model):
url = models.CharField(max_length=48)
title = models.CharField(max_length=32)
def __str__(self):
return self.title
(2)权限认证
- 登录
class UserView(ViewSet):
@action(methods=['GET', 'POST'], detail=False)
def login(self, request):
if request.method == 'GET':
return JsonResponse({'code': 100, 'msg': '您还未登陆'})
else:
# 验证用户名和密码
uname = request.data.get('username')
pwd = request.data.get('password')
user_obj = models.UserInfo.objects.filter(name=uname, password=pwd).first()
if user_obj:
# 登录认证-放到session中
request.session['is_login'] = True
# 权限认证-放到session中
permissions_list = user_obj.roles.values('Permissions__url', 'Permissions__title')
request.session['permissions_list'] = list(permissions_list)
return JsonResponse({'code': 100, 'msg': '保存成功'})
else:
return JsonResponse({'code': 101, 'msg': '用户名或密码错误'})
- 使用中间件认证
import re
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class PermissionAuth(MiddlewareMixin):
def process_request(self, request):
# 当前请求的路径
current_path = request.path
# 白名单
white_list = 'http://127.0.0.1:8000/api/v1/ab/login/'
for white_path in white_list:
if re.match(white_path, current_path):
return None
# 登录认证
print(request.session.get('is_login'))
is_login = request.session.get('is_login')
if not is_login: # 不为真就重定向到登录页面
return JsonResponse({'code': 401, 'errmsg': '请先登录'})
# 权限认证
permission_path_list = request.session.get('permissions_list')
for permission_path in permission_path_list:
reg = '^%s$' % permission_path['Permissions__url']
if re.match(reg, current_path):
return None
# 循环路径进行一一匹配,匹配不到走else。
else:
return HttpResponse('没有权限操作此功能!!')
- 中间件注册
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# 注册中间件
'app01.rbac.mydd.PermissionAuth',
]
八、simpleUI使用
1、simple ui使用前期准备
我们都知道django–admin 后台页面很丑,那如何能够美化这个界面呢?
django–admin的优点是几行代码配置就可以撸出一个功能性强的管理后台,缺点就是不怎么美观,感觉拿不出手。在所有的Django后台美化插件中,SimpleUI处于第一阵营,非常符合国人的审美观。
(1)安装并加入INSTALLED_APPS
- 在使用之前记得生成迁移文件和创建超级用户
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser 需要你注册的账号和密码
- pip安装
pip install django-simpleui
- 修改
settings.py
, 将simpleui
加入到INSTALLED_APPS
里去,放在第一行,也就是django自带admin的前面。
INSTALLED_APPS = [
'simpleui', # 注意这里
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
...
]
- 测试是否安装成功
- 使用
python manage.py runserver
命令启动本地测试服务器, 访问/admin/
, 如果你能看到如下页面说明安装成功。
注意:如果你在生成环境中使用SimpleUI,还需要使用python manage.py collectstatic命令收集静态文件,否则样式无法正常显示。
这是因为在 Django 中,静态文件(如 CSS、JavaScript 文件、图像等)需要被收集到一个统一的目录中,以便 Web 服务器能够正确地提供这些静态文件给用户访问。
具体来说,SimpleUI 主题可能包含了一些自定义的 CSS 样式表、JavaScript 文件等静态文件,这些文件需要被收集到 Django 项目中的静态文件目录中,以确保在用户访问网站时能够正确加载这些静态文件,从而使页面样式能够正常显示。
因此,在使用 SimpleUI 主题或任何其他自定义主题时,确保在部署 Django 项目到生成环境之前,运行
python manage.py collectstatic
命令,将所有静态文件从各个应用程序和第三方库中收集到一个统一的静态文件目录中,以确保网站的样式能够正确加载和显示。
(2)常用配置
设置语言, 去Logo
# 更改默认语言为中文
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
# 去掉默认Logo或换成自己Logo链接
SIMPLEUI_LOGO = 'https://th.bing.com/th/id/R2411a2b340731d?rik=zmYce%2fLys72JVQ&pid=ImgRaw'
修改管理后台名字
- 修改管理后台的名称和标题要稍微复杂些,因为你不能直接在
settings.py
里进行配置。在任何一个app的目录下新建一个admin.py
, 添加如下代码即可修改(本例app名为app01)。这个设置属于Django的设置,不属于SimpleUI的设置。
# app01/admin.py
from django.contrib import admin
# list_display = ('pk','title','body','timestamp') #设置要显示的属性,pk为索引。
admin.site.site_header = '图书管理后台' # 设置header
admin.site.site_title = '图书管理后台' # 设置title
admin.site.index_title = '图书管理后台'
from .models import app01
admin.site.register(app01)
自定义或第三方APP名和模型名修改成中文
- 修改
app01/app.py
, 通过verbose_name
可以将app名改为中文,这里将app01
改成了任务管理
。
from django.apps import AppConfig
class App01sConfig(AppConfig):
name = 'app01'
verbose_name = '任务管理'
- 接着修改
tasks/models.py
, 以中文设置模型的verbose_name
, 如下所示:
在模型表中添加
class Meta:
verbose_name_plural = "用户信息"
可以改成自己想要的名字
- 实际Django开发中,我们还会用到第三方应用app和第三方app提供的模型,我们也可以通过打补丁的方式更改第三方app或模型以及模型字段的
verbose_name
或者label
,将其修改成中文,如下所示:
from third_package.models import ModelA
ModelA._meta.verbose_name = ''
ModelA._meta.verbose_name_plural = ''
ModelA._meta.get_field('first_name').verbose_name = '名字'
关闭右侧广告链接和使用分析
- 修改
settings.py
, 添加如下两行代码:
# 隐藏右侧SimpleUI广告链接和使用分析
SIMPLEUI_HOME_INFO = False
SIMPLEUI_ANALYSIS = False
SIMPLEUI_HOME_ACTION = False # 关闭最近动作
自定义首页
- SimpleUI默认首页由快捷链接和最近动作组成,我们可以将其隐藏,并将其链接到其它url。
- 继续修改
settings.py
, 添加如下代码:
#修改左侧菜单首页设置
SIMPLEUI_HOME_PAGE = 'https://blog.csdn.net/' #指向页面
SIMPLEUI_HOME_TITLE = 'CSDN欢迎你' #首页标题
SIMPLEUI_HOME_ICON = 'fa fa-code' #首页图标
#设置右上角home图标跳转链接,会以另外一个窗口打开
SIMPLEUI_INDEX = 'https://blog.csdn.net/qiujin000?spm=1010.2135.3001.5421'
#隐藏首页的快捷操作和最近动作
SIMPLEUI_HOME_QUICK = False
SIMPLEUI_HOME_ACTION = False
- 实际应用中后台首页通常是控制面板,需要用图表形式展示各种关键数据,这时就需要重写首页了。这里主要有两种实现方法。第一种是重写simpleui自带的home.html, 另一种自己编写一个控制面板的页面,然后设置首页指向它, 个人倾向于第二种, 因为它完全不涉及改动simpleui的源码。
自定义菜单
-
如果想建图个公司管理后台的话,可以根据这个题目更改菜单
-
app01/settings.py
SIMPLEUI_CONFIG = {
'system_keep': False, # 关闭系统菜单
'menu_display': ['行政管理', '研发管理','高层管理', '认证和授权'], # 只会显示这几个菜单,如果去掉那么都显示
'dynamic': True, # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时刷新展示菜单内容。一般建议关闭。
'menus': [{
'app': 'myblog',
'name': '行政管理', # 父级菜单,必须和上面的菜单一一对应
'icon': 'fas fa-x-ray',
'models': [{
'name': '人员信息', # 子菜单,需要的时候自己添加字典格式就好
'icon': 'fa fa-user',
'url': 'myblog/person/' # 你的网页比如/index
}, {
'name': '工作计划',
'icon': 'el-icon-video-camera-solid',
'url': 'myblog/job_plan/',
}]},
{
'app': 'myblog', # 和app名字对应
'name': '研发管理',
'icon': 'fab fa-app-store-ios',
'models': [{
'name': '人员信息',
'icon': 'fa fa-user',
'url': 'myblog/person1/'
}, {
'name': '工作计划',
'icon': 'el-icon-video-camera-solid',
'url': 'myblog/job_plan1/',
}]},
{
'app': 'myblog',
'name': '高层管理',
'icon': 'fa fa-th-list',
'models': [{
'name': '人员信息',
'icon': 'el-icon-message-solid',
'url': 'myblog/person2/'
}, {
'name': '工作计划',
'icon': 'el-icon-picture',
'url': 'myblog/job_plan2/'
}
]},
{
'app': 'auth',
'name': '认证和授权',
'icon': 'fas fa-shield-alt',
'models': [{
'name': '用户',
'icon': 'far fa-user',
'url': 'auth/user/'
}, {
'name': '组',
'icon': 'fas fa-users-cog',
'url': 'auth/group/',
}]
}]
}
自定义action按钮
# 增加自定义按钮
actions = ['custom_button']
def custom_button(self, request, queryset):
pass
# 显示的文本,与django admin一致
custom_button.short_description = '测试按钮'
# icon,参考element-ui icon与https://fontawesome.com
custom_button.icon = 'fas fa-audio-description'
# 指定element-ui的按钮类型,参考https://element.eleme.cn/#/zh-CN/component/button
custom_button.type = 'danger'
# 给按钮追加自定义的颜色
custom_button.style = 'color:black;'
# 链接按钮,设置之后直接访问该链接
# 3中打开方式
# action_type 0=当前页内打开,1=新tab打开,2=浏览器tab打开
# 设置了action_type,不设置url,页面内将报错
# 设置成链接类型的按钮后,custom_button方法将不会执行。
custom_button.action_type = 1
custom_button.action_url = 'http://www.baidu.com'
九、补充 对称加密和非对称加密
1、对称加密
-
对称加密指的就是加密和解密使用同一个秘钥,所以叫对称加密。 对称加密只有一个秘钥,作为私钥。
-
加密过程:
- 加密: 原文+密钥 = 密文
- 解密:密文-密钥 = 原文
-
常见的对称加密算法: DES,、AES、 3DES等
-
优点 - 算法简单,加解密容易,效率高,执行快。
-
缺点 - 相对来说不安全,只有一把钥匙,密文如果被拦截,且密钥被劫持,那么信息很容易被破译。
2、非对称加密
-
非对称加密指的是:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥。 公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。
-
常见的给对称加密: RSA、ECC
-
优点 - 安全,即使密文和公钥被拦截,但是由于无法获取到私钥,也就无法破译到密文。
-
缺点 - 加密算法复杂,安全性依赖算法和密钥, 且加密和解密效率很低。
-
需要注意的是,在许多应用中,对称和非对称加密会一起使用。这种混合系统的典型案例是安全套接字层(SSL)和传输层安全(TLS)加密协议,该协议被用于在因特网内提供安全通信。SSL协议现在被认为是不安全的,应该停止使用。相比之下,TLS协议目前被认为是安全的,并且已被主流的Web浏览器所广泛使用。
3、对称加密和非对称加密的区别
- 对称加密: 加密解密使用同一个密钥,被黑客拦截不安全
- 非对称加密:公钥加密,私钥解密。公钥可以公开给别人进行加密,私钥永远在自己手里,非常安全,黑客拦截也没用,因为私钥尚未公开。 著名的RSA加密算法就是用的非对称加密。
简单理解:
-
对称加密: A和B传输数据,使用同一个密钥,不安全
-
非对称加密: A和B传输数据, A具有自己的公私钥,B具有自己的公私钥。
- (公钥是在公网上公开的,任何人都能看见, 私钥自己保留),A拿着B的公钥+信息数据, 传递给B。 这个时候 , 只有B手里的密钥才能解开。假设C拦截了A传递的信息,他是解不开的, 因为C没有这个公钥对应的私钥。所以比较安全。