做项目时需要用到jwt认证,简单记录一下~
1、JWT认证简易流程
2、安装jwt认证包
- terminal窗口下载包
pip install djangorestframework-jwt
- 在settings.py中添加代码如下:若无指定权限类则默认执行的权限
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
#'rest_framework.authentication.SessionAuthentication',
#'rest_framework.authentication.BasicAuthentication',
),
}
import datetime
JWT_AUTH = {
# JWT_EXPIRATION_DELTA 指明token的有效期
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
3、用户登录
- 用户登录时不需要认证和权限,所以视图层authentication_classes和permission_classes为空
# views.py
class LoginView(ViewSet):
'''
登录视图,用户名与密码匹配返回token
'''
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
# 实例化得到一个序列化类的对象
ser = LoginSerializer(data=request.data)
if ser.is_valid(raise_exception=True):
token = ser.context.get('token')
# 如果通过,表示登录成功,返回手动签发的token
# 如果失败,抛异常,就不用管了
username = ser.context.get('username')
id = ser.context.get('id')
return APIResponse(token=token, id=id, username=username)
- 登录序列化器里签发token
JWT 包含三部分内容:header、payload 和 signature,其中 header 和 payload 部分为 JSON 格式,而 signature 则是对前两部分进行签名得到的。
from rest_framework_jwt.settings import api_settings
from django.contrib.auth import get_user_model
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
User = get_user_model() # 获取用户模型
class LoginSerializer(serializers.ModelSerializer):
'''登录序列化器'''
# 设置自定义的反序列化字段usr,pwd
username = serializers.CharField(write_only=True) # 重写 username , 否则会它会认为你想存数据
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'password']
extra_kwargs = {
'password': {'write_only': True}
}
# 全局钩子
def validate(self, attrs):
# username mobile 都可能是登录账户
username = attrs.get('username')
password = attrs.get('password')
if re.match('^1[0-9]\d{9}$', username): # 手机号正则
user = User.objects.filter(mobile=username).first()
else: # 用户名登录
user = User.objects.filter(username=username).first()
if user and user.check_password(password): # 如果登录成功,生成token
payload = jwt_payload_handler(user) # 通过user拿到payload
token = jwt_encode_handler(payload) # 通过payload拿到token
print('生成token:' + token)
# 视图类和序列化类之间通过context这个字典来传递数据
self.context['token'] = token
self.context['username'] = user.username
self.context['id'] = user.id
return attrs
else:
raise ValidationError("账户或密码错误")
- 前端拿到token后可以存到浏览器中
httptool是我创建的axios实例,后面有代码
methods: {
// 登录方法
loginhander() {
// axios实例
httptool.post('/login/', {
"username": this.username,
"password": this.password,
}).then(response => {
console.log(response) //http响应头
console.log(response.data) //http响应的请求体数据
// 根据用户是否勾选了记住密码来保存用户认证信息
if (this.remember) {
// 记住登录
sessionStorage.clear(); // 清除所有sessionStorage数据
// localStorage的生命周期是永久的,关闭页面或浏览器之后localStorage中的数据也不会消失。除非主动删除,否则数据永远不会消失
localStorage.token = response.data.token; // 存储token
localStorage.id = response.data.id;
localStorage.username = response.data.username;
} else {
// 未记住登录
localStorage.clear();
// sessionStorage的生命周期仅在当前会话下有效,关闭浏览器窗口后就会被销毁
sessionStorage.token = response.data.token;
sessionStorage.id = response.data.id;
sessionStorage.username = response.data.username;
}
// this.$router.go(-1);
// 在登录成功后返回前一个页面
window.history.back();
}).catch(error => {
console.log(error) // error可以是本地错误信息,也可以是服务端的错误信息
console.log(error.response) // 接收来自服务端的响应错误,如果服务端没有错误,则没有response属性
// 登录错误提示
this.$alert(error.response.data.msg, '警告');
})
},
4、发送请求时加上jwt认证头
- 我在前端用的是axios发送请求,每次发送请求对请求做一个拦截,加入请求头和token
- jwt认证要加入Bearer头,格式一般是Bearer[空格]token,后端根据空格切割拿到token(在后面自定义认证类会说)
// aip.js
const httptool = axios.create({
baseURL: 'http://api.forum.com:8000', // 服务器后端地址
timeout: 1000,
})
//请求拦截器
httptool.interceptors.request.use(config => {
let token = window.localStorage.getItem('token') || window.sessionStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
return config;
}, error => {
console.log(error);
return Promise.reject(error)
})
- 此时只要用httptool发送请求就会拦截加入认证头,现在假如有个获取某用户创建的所有帖子的请求
methods: {
/*获取发布的帖子*/
getMyPublish() {
httptool.get(`news/user/${this.userId}/`).then(resp => {
if (resp) {
this.articleList = resp.data.news_list
}
})
},
}
5、自定义认证类
-
前端发送获取某用户创建的所有帖子的请求,请求头中包含token,现在就要对token进行获取并认证。首先要定义一个认证类
-
在你的app下创建authentication.py
# authentication.py
import jwt
from rest_framework import exceptions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.settings import api_settings
from forum.models import User
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class JsonAuthentication(JSONWebTokenAuthentication):
def authenticate_credentials(self, payload):
username = payload['username']
if not username:
msg = 'Invalid payload.'
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get(username=username)
except Exception:
msg = 'Invalid signature.'
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = 'User account is disabled.'
raise exceptions.AuthenticationFailed(msg)
return user
def authenticate(self, request):
# 若前端无token则获取切割后的列表索引1会报索引错误,则用try包围
try:
jwt_value = request.META.get('HTTP_AUTHORIZATION', '').split(' ')[1]
except IndexError:
jwt_value = None
print(jwt_value)
# # 验证签名,验证是否过期
try:
payload = jwt_decode_handler(jwt_value) # 得到载荷
# 取当前用户,拿到user对象,每登录一个人就要去数据库查一次
# 这个也要重写,因为它找的是auth的user表,我们是去自己表中查
user = self.authenticate_credentials(payload)
# 效率更高一写,不需要查数据库了
# user=LbUserInfo(id=payload['user_id'],username=payload['username']) # user={'id':payload['user_id'],'username':payload['username']}
except jwt.ExpiredSignature:
msg = 'token过期'
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = '签名错误'
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed('错误')
return (user, jwt_value)
当进入认证类时,authenticate方法获取token,拿到载荷,若token过期或签名错误则报错,再调用authenticate_credentials方法检索数据库是否有该用户,返回用户和token
- 自定义完认证类,再回到刚才的获取用户发布的帖子请求
# urls.py
path("news/user/<int:userId>/", views.NewsByUser.as_view({'get': 'get_by_userId'})),
# views.py
from rest_framework.decorators import authentication_classes, permission_classes
from 你的app.authentication import JsonAuthentication
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class NewsByUser(ViewSet):
'''需要权限的帖子视图'''
authentication_classes = [JsonAuthentication]
permission_classes = [IsAuthenticatedOrReadOnly] # 按需使用
def get_by_userId(self, request, userId):
'''查询用户id创建的帖子'''
# 业务代码,获取请求数据返回给前端
news = xx
return
- JsonAuthentication是刚才自定义的认证类,在进入NewsByUser方法前,会先进行认证,进入authentication_classes列表中的认证类进行认证,成功后再进入permission_classes中的权限类判断是否有权限。认证和权限都通过后才执行业务方法
- IsAuthenticatedOrReadOnly是DRF自带的权限类:游客只读
- 如果需要像登录时那样不进行认证和权限判断,则使之为空,否则默认执行settings.py中设置的默认权限类
authentication_classes = []
permission_classes = []
6、报错401时前端响应,跳转登录界面
// api.js
httptool.interceptors.response.use(
response => {
// 对响应数据做处理
return response;
},
error => {
if (error.response && error.response.status === 401) {
Message({
type: 'warning',
message: '尚未登录,请登录!',
offset: 54
})
router.push('/login');
} else {
return Promise.reject(error);
}
}
);