Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT就一段字符串,由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
头部信息:header
jwt的头部承载两部分信息:
完整的头部就像下面这样的JSON:
{
'typ': 'JWT', # 认证类型类型
'alg': 'HS256' # 加密算法
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷:payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明 (建议但不强制使用) :
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub":"zhangsan",
"name": "test",
"id": 1
}
然后将其进行base64加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
签证信息:signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
图解:
jwt的优势:
1.天生适用于SSO单点登录
2.服务器端只需要存储秘钥,不需要分别存放用户的jwt信息,降低了服务器的存储压力
3.比cookie更加安全可靠
4.还可以用于提供数据加密传输的功能
jwt的缺点:
1.因为jwt是存放在客户端的,所以一旦签发以后,服务端是无法控制的或提前回收
2.增加了客户端的数据传输加密的功能
jwt官网地址:https://jpadilla.github.io/django-rest-framework-jwt/
安装配置可以按照官网文档中的来:
pip install djangorestframework-jwt
settings.py中配置如下:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
# 使用JWT认证 下面的是session认证
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
import datetime
JWT_AUTH = {
# 过期时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
源码:
User = get_user_model()
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
class JSONWebTokenSerializer(Serializer):
"""
Serializer class used to validate a username and password.
'username' is identified by the custom UserModel.USERNAME_FIELD.
Returns a JSON Web Token that can be used to authenticate later calls.
"""
def __init__(self, *args, **kwargs):
"""
Dynamically add the USERNAME_FIELD to self.fields.
"""
super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
self.fields[self.username_field] = serializers.CharField()
self.fields['password'] = PasswordField(write_only=True)
@property
def username_field(self):
return get_username_field()
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
if all(credentials.values()):
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
大概意思:
在用户注册或登录成功后,在序列化器中返回用户信息以后同时返回token即可。
返回一个JSON Web令牌,可用于对以后的调用进行身份验证。
路由信息(子路由中):
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path(r'authorizations/', obtain_jwt_token, name='authorizations'),
]
路由信息(主路由信息中):
urlpatterns = [
...
path('users/', include("users.urls")),
# include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点
]
前端代码:
<script>
export default {
name: 'Login',
data() {
return {
login_type: 0,
username: "",
password: "",
remember:false,
}
},
methods:{
loginHand(){
//登录处理
this.$axios.post(this.$settings.HOST+'/user/login/',{
username:this.username, //连带请求一起发送的参数
password:this.password,
}).then(response=>{
console.log(response.data.token);
// 判断用户是否勾选了'记住密码'
if(this.remember){
// 记住密码
localStorage.clear(); //删除以前有的信息
localStorage.user_token = response.data.token;
localStorage.user_id = response.data.id;
localStorage.user_name = response.data.username;
}else {
// 没有记住密码
localStorage.clear(); //删除以前有的信息
sessionStorage.user_token = response.data.token;
sessionStorage.user_id = response.data.id;
sessionStorage.user_name = response.data.username;
}
let self = this;
this.$alert('欢迎回来!','路飞学城',{
callback(){
// 页面跳转
// this.$router.push("/"); //使用vue跳转页面到指定地址
self.$router.go(-1); //使用vue跳转页面返回上一页
}
})
}).catch(error=>{
if(error.response.status === 400){
//登录失败
this.$message.error('登录失败,账号或密码错误');
}else {
// 其他未知错误
this.$message.error('未知错误!!')
}
})
}
},
};
</script>
在上面源码中可以看到,最终是将用户信息和生成的token返回给用户,如果想返回多个键值对怎么办,那么就可以自定义响应数据,在自定义响应数据之前先要熟悉一下响应数据是由哪个方法进行返回的,执行流程是什么样的。
可以看到生成jwt的源码就在下面这张图中:
可以在应用下面创建一个util文件用来自定义响应数据:
def jwt_response_payload_handler(token, user=None, request=None):
"""
自定义jwt认证成功返回数据
token: jwt认证信息
user:根据客户提交的信息查询出来的用户模型对象
username:本次客户端提交的请求对象
"""
return {
'token': token,
'id': user.id,
'username': user.username
}
自定义响应数据写好之后需要在settings.py文件配置一下:
# 自定义响应数据格式
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
刚刚是用用户名密码登录的,但是很多网站有手机号登录,邮箱登录等登录方式,那么我们想要让用户可以以用户名灯枯,也可以手机号登录,那么对于authenticate方法而言,username参数即表示用户名或者手机号。
看一下DRF的认证流程是怎么走的,需要在哪一步进行重写。
看一下数据校验这个方法里面是什么:
分别看一下_get_backends和authenticate这两个方法:
先看_get_backends:
authenticate方法:
父类中果然有:
认证流程梳理:
1.拿到用户模型User = get_user_model()
2.将用户名密码取出来credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
3.调用authenticate方法将用户名密码传递过去
4.使用_get_backends获取认证信息类(setting.py中配置的)
5.使用jwt认证类下面的authenticate方法校验数据
修改Django认证系统的认证后端需要继承django.contrib.auth.backends.ModelBackend,并重写authenticate方法。
# 多条件登录
from django.contrib.auth.backends import ModelBackend
from luffyapi.apps.users.models import User
from django.db.models import Q
def get_user_by_account(username):
return User.objects.filter(Q(username=username)|Q(mobile=username)|Q(email=username)).first()
class UsernameMobileAuthBackend(ModelBackend):
# 重写认证类里面的认证方法,实现多条件登录
def authenticate(self, request, username=None, password=None, **kwargs):
user = get_user_by_account(username)
# 如果用户名不是空并且密码正确
if user is not None and user.check_password(password):
return user