前言
学习新技术的3部曲
Session+Cookie认证方式、传统的Token认证方式有哪些不足之处呢?
在前后端分离+后端分布式集群、微服务架构中
客户端不是浏览器而是App没有办法存储Cookie的情况下,我们如果进行用户身份认证呢?
一、传统认证方式
1.Session认证
如果客户端是浏览器,那么每1次访问服务端时会自动携带上cookie信息,并在cookie中包含上Session_id;
流程不再赘述;
2.静态Token认证
Cookie+Session机制仅能使用在B/S架构中,而Tonken是对Cookie+Session机制的完善;
如果客户端不是浏览器而是app,是没有办法存储Cookie的;
或者
随着架构的扩展,前后端分离,中间还有Nginx等中间代理层,后端分布式集群部署;
在以上架构中,客户端的Cookie就难以进行跨域携带,即便客户端的Cookie最终达了服务端,也需要共享分布式集群的Session;
以上得出Cookie+Session这种认证机制,不适用于大型网站架构中,于是Token出现了;
客户端第一次请求服务端时,服务端给客户端发放1个Token;
此后的每1次HTTP请求,自客户端开始,每经过1道关卡,都携带上这个Token,最终完成客户端和服务端的数据交互;
1.服务端发放Token
用户登录 服务端放回给客户端1个Token,并将Token保存在服务端
2.客户端请求之前携带Token
用户再次访问时 需要携带Token,
3.服务器验证Token
服务端获取Token后,进行校验。
""" Django settings for jwt_demo project. Generated by 'django-admin startproject' using Django 1.11.4. For more information on this file, see https://docs.djangoproject.com/en/1.11/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.11/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '2o=cm-!m%j**0&@9bgjq(zj!@ifw$5^o(4w@psst65l$1=2vmf' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'api.apps.ApiConfig', 'rest_framework'#加载rest_framework ] 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', ] ROOT_URLCONF = 'jwt_demo.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates'),], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'jwt_demo.wsgi.application' # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } # Password validation # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = '/static/'
"""jwt_demo URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.11/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url from django.contrib import admin from api import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^api/login/$', views.Login_View.as_view()), url(r'^api/order/$', views.Order_View.as_view()), ]
from django.shortcuts import render import uuid from rest_framework.views import APIView from rest_framework.response import Response from api import models class Login_View(APIView): '''用户登录 ''' def post(self,request,*args,**kwargs): print(21222) user=request.data.get('username') pwd = request.data.get('password') user_object=models.UserInfo.objects.filter(username=user).first() if not user_object: return Response({'code':1000,'error':'用户名/密码错误'}) random_string = str(uuid.uuid4()) user_object.token=random_string user_object.save() return Response({'code': 1001, 'data': random_string}) class Order_View(APIView): def get(self, request, *args, **kwargs): token=request.query_params.get('token') if not token: return Response({'code':2000,'error':'登录成功之后才能访问'}) user_obj=models.UserInfo.objects.filter(token=token).first() if not user_obj: return Response({'code': 2000, 'error':'token'}) return Response('订单列表')
3.JWT认证
K8S+微服务架构的部署方式逐渐兴起,使用JWK技术可以开发无状态应用,这样可以方便当前微服务的横向扩展,(K8S的Deployment);
JWT+Redis正在逐渐替代Cookie+Session用户认证机制;
虽然静态Token适用于大型网站架构中,跨域访问时也方便携带,但这个令牌(Token)是固定不变的;
容易在客户端请求服务器的途中,被人截获和篡改,于是又出现了JSON Web Token;
1.服务端生成Tonken响应给客户端,但是不保存Token;
2.客户端保存Token;
3.客户端请求之前携带Token;
4.服务端使用算法验证Token;
二、JsonWebToken结构
JWT一般是这样1个字符串,分为3个部分,以"."隔开;
这个字符串的3个部分分别由3个Json对象加密之后生成;
1.Header
1个Json对象经base64加密之后的字符串;
2.PayLoad
1个Json对象经base64加密之后的字符串;
3.Signature
Signature = Header+PayLoad拼接之后,再由Hash256加密+加盐 生成
三、JWT认证流程
- 1.用户在前端输入用户名密码,通过HTTP请求到达后端服务器。
- 2.后端服务器校验密码通过之后,生成动态Token,响应给前端。
- 3.前端把ACCESS_TOKEN存储在Vuex中并设置过期时间,Storage.set(ACCESS_TOKEN, token, 7 * 24 * 60 * 60 * 1000)。
- 4.前端每次向后端发送请求时,都会在request的header中携带Token,
- 5.路由守卫中(permission.js)都会检查Vuex中的ACCESS_TOKEN是否失效?避免Token过期还携带到后端服务器。
- 6.前端的Token失效就跳转至登录页面,让用户去重新登录,重新获取ACCESS_TOKEN,最终形成认证流程闭环。
1.服务端生成Token
如果用户登录成功,服务端使用jwt创建1个token,并返回用户。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2.第2段:HEADER
内部保存算法和token类型
让该json转换成字符串,然后进行base64url 加密然后把加密后的字符串+替换为_。(base64算法可以反解)
{ "alg": "HS256", "typ": "JWT" }
生成: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
2.第2段:PAYLOAD:
用户自定义的值
让该json转换成字符串,然后进行base64url 加密然后把加密后的字符串+替换为_。(base64算法可以反解)
{ "id": "1234567890", "name": "zhanggen", "iat": 1516239022 #超时时间 }
3.第3段:VERIFY SIGNATURE
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
a.对第1、2端密文进行拼接。
b.对第1、2部分密文进行hash256加密,并加盐。
c.对hash256加密之后的密码,再次进行base256加密。
4.服务端验证Token
用户再次访问服务端,需要携带token 服务端对token进行校验。
{ "id": "1234567890", "name": "zhanggen", "iat": 1516239022 #超时时间 }
- 获取token
- 通过.对web token进行切割,划分为3段
- 对第二段进行base64url解密获取 payload信息,检测web token是否超时?
- 然后通base64url加密解密后的 第二段数据,把第1、2段密文进行拼接
- 对第1、2部分密文 进行hash256加密,并加盐。再次得到第3段数据
- 让新生成的第3段 和从用户那里分割出来的第3段, 进行密文对比。检查 web token是否有效或者中途被修改过?
- 最后通过验证
5.总结
web token的核心加密算法就是把token分3段,前2段可以解密,第3段不可以解密。第3段 = 前2段的拼接(hash256加密+加盐)生成。
在这里我们在后端进行加密、解密用到的盐是至关重要的。
四、JWT应用
jwt已经通过 第三方包的方式集成到Python。使用非常简单。
1.安装pyjwt模块
D:\jwt_demo>pip install pyjwt -i http://pypi.douban.com/simple --trusted-host pypi.douban.com Collecting pyjwt Downloading http://pypi.doubanio.com/packages/87/8b/6a9f14b5f781697e51259d81657e6048fd31a113229cf346880bb7545565/PyJWT-1.7.1-py2.py3-none-any.whl Installing collected packages: pyjwt Successfully installed pyjwt-1.7.1 D:\jwt_demo>
2.基于Django 的 DRF使用
from django.shortcuts import render import uuid import datetime from jwt import exceptions as jwt_exceptions import jwt from rest_framework.views import APIView from rest_framework.response import Response from api import models class Login_View(APIView): '''用户登录 ''' def post(self,request,*args,**kwargs): print(21222) user=request.data.get('username') pwd = request.data.get('password') user_object=models.UserInfo.objects.filter(username=user).first() if not user_object: return Response({'code':1000,'error':'用户名/密码错误'}) random_string = str(uuid.uuid4()) user_object.token=random_string user_object.save() return Response({'code': 1001, 'data': random_string}) class Order_View(APIView): def get(self, request, *args, **kwargs): token=request.query_params.get('token') if not token: return Response({'code':2000,'error':'登录成功之后才能访问'}) user_obj=models.UserInfo.objects.filter(token=token).first() if not user_obj: return Response({'code': 2000, 'error':'token'}) return Response('订单列表') import uuid import datetime from jwt import exceptions as jwt_exceptions import jwt from rest_framework.views import APIView from rest_framework.response import Response from api import models salt = 'dsfhkjhiejgnvjcxhwwwwwwwwwww' class JwtLogin_View(APIView): '''基于Jwt用户登录 ''' def post(self,request,*args,**kwargs): user=request.data.get('username') pwd = request.data.get('password') user_object=models.UserInfo.objects.filter(username=user).first() if not user_object: return Response({'code':1000,'error':'用户名/密码错误'}) #构造header头部 headers={ "typ": "JWT", "alg": "HS256", } #构造payload payload={ "user_id": user_object.pk, "user_name": user_object.username, "exp": datetime.datetime.utcnow() +datetime.timedelta(minutes=1) #超时时间1分钟 } #生成 web token key=要加的盐 一定要保密啊!! web_token=jwt.encode(headers=headers,payload=payload,algorithm='HS256',key=salt).decode('utf-8') return Response({'code': 1001, 'data': web_token}) class JwtOrder_View(APIView): def get(self, request, *args, **kwargs): #获取token token=request.query_params.get('token') verified_payload=None msg=None try: # 解析token,得到第3段,True等于校验 #注意啦!!加密、解密用得都是同1个盐!!!!千万不能泄露 verified_payload=jwt.decode(token,salt,True)## except jwt_exceptions.ExpiredSignature: msg='Token已经超时' except jwt.DecodeError: msg='Token认证失败' except jwt.InvalidTokenError: msg='非法的Token' if not verified_payload: return Response({'code':1003,'error':msg}) #获取第二段 用户自定义的信息 print(verified_payload['user_id'],verified_payload['user_name']) return Response('订单列表')