前言

学习新技术的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/'
settings.py
"""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()),
]
urls.py
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('订单列表')
views.py

 

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('订单列表')

 

posted on 2020-02-20 08:58  Martin8866  阅读(313)  评论(0编辑  收藏  举报