什么是单点登录:
单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。
如何解决单点登录:
1.session+cookie+redis
2.设置登录服务器
3.JWT
JWT:
JWT长什么样?
JWT是由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
header
j wt的头部承载两部分信息:
-
声明类型,这里是jwt
-
声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
-
标准中注册的声明
-
公共的声明
-
私有的声明
标准中注册的声明 (建议但不强制使用) :
-
iss: jwt签发者
-
sub: jwt所面向的用户
-
aud: 接收jwt的一方
-
exp: jwt的过期时间,这个过期时间必须要大于签发时间
-
nbf: 定义在什么时间之前,该jwt都是不可用的.
-
iat: jwt的签发时间
-
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
-
header (base64后的)
-
payload (base64后的)
-
secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用 .
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
如何应用
一般是在请求头里加入Authorization
,并加上Bearer
标注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:
总结
优点
-
因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
-
因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
-
便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
-
它不需要在服务端保存会话信息, 所以它易于应用的扩展
安全相关
-
不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
-
保护好secret私钥,该私钥非常重要。
-
如果可以,请使用https协议
JWT使用:
安装配置
pip install djangorestframework-jwt
配置setting
配置全局路由
from django.contrib import admin from django.urls import path,re_path,include urlpatterns = [ path('admin/', admin.site.urls), re_path(r'users/',include(('users.urls','users'),namespace='users')) ]
配置局部路由
#! /usr/bin/env python # -*- coding: utf-8 -*- from django.urls import path,re_path,include from users import views from rest_framework_jwt.views import obtain_jwt_token # 验证密码后返回token urlpatterns = [ path('v1/register/', views.RegisterView.as_view(), name='register'), # 注册用户 path('v1/login/', obtain_jwt_token,name='login'), # 用户登录后返回token path('v1/list/', views.UserList.as_view(), name='register'), # 测试需要携带token才能访问 ]
重写User表
from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): username = models.CharField(max_length=64, unique=True) password = models.CharField(max_length=255) phone = models.CharField(max_length=64) token = models.CharField(max_length=255)
设置序列化器
#! /usr/bin/env python # -*- coding: utf-8 -*- from rest_framework_jwt.settings import api_settings from rest_framework import serializers from users.models import User class UserSerializer(serializers.Serializer): username = serializers.CharField() password = serializers.CharField() phone = serializers.CharField() token = serializers.CharField(read_only=True) def create(self, data): user = User.objects.create(**data) user.set_password(data.get('password')) user.save() # 补充生成记录登录状态的token jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) user.token = token return user
代码实现
from django.shortcuts import render import json from rest_framework.views import APIView from rest_framework.views import Response from rest_framework.permissions import IsAuthenticated from rest_framework_jwt.authentication import JSONWebTokenAuthentication from users.serializers import UserSerializer # 用户注册 class RegisterView(APIView): def post(self, request, *args, **kwargs): serializer = UserSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=201) return Response(serializer.errors, status=400) # 重新用户登录返回函数 def jwt_response_payload_handler(token, user=None, request=None): ''' :param token: jwt生成的token值 :param user: User对象 :param request: 请求 ''' return { 'token': token, 'user': user.username, 'userid': user.id } # 测试必须携带token才能访问接口 class UserList(APIView): permission_classes = [IsAuthenticated] # 接口中加权限 authentication_classes = [JSONWebTokenAuthentication] def get(self,request, *args, **kwargs): print(request.META.get('HTTP_AUTHORIZATION', None)) return Response({'name':'zhangsan'}) def post(self,request, *args, **kwargs): return Response({'name':'zhangsan'})
区别和优缺点:
基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。
jwt的优点:
-
可扩展性好
应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。
-
无状态
jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。
jwt的缺点:
-
安全性
由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。
-
性能
jwt太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用jwt的http请求比使用session的开销大得多。
-
一次性