目录

JWT认证(5星)

token发展史

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。

构成和工作原理

JWT的构成

JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我们称它为头部(header),第二部分我们称其为荷载(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

header(头部)

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

{
'typ': 'JWT',
'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

payload(荷载)

荷载就是存放类似用户信息,过期时间,签发时间...

{
"userid": "1",
"name": "John Doe",
"exp": 1214356
}

然后将其进行base64加密,得到JWT的第二部分。

eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9

signature(签证)

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64解密后加密算法加密后的)
  • payload (base64解密后加密算法加密后的)
  • secret(密钥=加盐)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。

文档网站:http://getblimp.github.io/django-rest-framework-jwt/

补充base64编码解码

import base64
import json payload = {
"userid": "1",
"name": "John Doe",
"exp": 1214356
}
json_payload = json.dumps(payload)
# 编码
res = base64.b64encode(json_payload.encode('utf8')) print(res)
# 解码
res2 = json.loads(base64.b64decode(res))
print(res2) # b'eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9'
# {'userid': '1', 'name': 'John Doe', 'exp': 1214356}

本质原理

jwt认证算法:签发与校验

1)jwt分三段式:头.体.签名 (head.payload.sgin)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的(base64反解出的是hash加密后的密文)
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间
{
"user_id": 1,
...
}
6)签名中的内容是安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码(对整个字典进行md5加密)
{
"head": "头的加密字符串",
"payload": "体的加密字符串",
"secret": "安全码"
}

签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token

1)用基本信息存储json字典,采用base64算法加密得到 头字符串
2)用关键信息存储json字典,采用base64算法加密得到 体字符串
3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串 账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台

校验:根据客户端带token的请求 反解出 user 对象

1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的
3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户

drf项目的jwt认证开发流程(重点)

1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中

2)校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户

注:登录接口需要做 认证 + 权限 两个局部禁用

drf-jwt安装和简单使用(2星)

安装

pip3 install djangorestframework-jwt

简单使用

签发

# 1 创建超级用户
python3 manage.py createsuperuser
# 解释下为什么要创建超级用户:因为djangorestframework-jwt认证是基于django的auth里的user表作关联的,所以验证的数据也必须源自于这张表
# 2 配置路由urls.py
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path('login/', obtain_jwt_token),
]
# 3 postman测试
向后端接口发送post请求,携带用户名密码,即可看到生成的token

认证

from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated class BookAPIView(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookModelSerializer
# 必须用这个认证类
authentication_classes = [JSONWebTokenAuthentication, ]
# 还要配合这个权限
permission_classes = [IsAuthenticated, ]

在postman里

JWT使用auth表签发token,自定制返回格式(3星)

配置setting.py

JWT_AUTH ={
# token的过期时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
# 如果不自定义,返回的格式是固定的,只有token字段
# 这里把下面自定制的函数注册进来
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}

自定制的py文件内

def jwt_response_payload_handler(token, user=None, request=None):
return {
'code': 1000,
'msg': '登陆成功',
'username': user.username,
'token': token
}

这时登陆时返回的格式就变成了:

djangorestframework-jwt模块源码分析(2星)

签发token

ObtainJSONWebToken.as_view()--->ObtainJSONWebToken---->post方法
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(): # 验证用户登录和签发token,都在序列化类的validate方法中完成的
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
# 返回了咱们自定指的格式
'''
{
'code':100,
'msg':'登录成功',
'username':user.username,
'token': token,
} '''
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # 全局钩子函数
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
} if all(credentials.values()):
# 根据用户名密码去auth的user表校验,是否存在
user = authenticate(**credentials) if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
# 生成payload
payload = jwt_payload_handler(user) return {
'token': jwt_encode_handler(payload), # 通过payload生成token
'user': user
}
else:
# 不在抛异常,前端就看到信息了
raise serializers.ValidationError(msg)
else:
raise serializers.ValidationError(msg)

认证

JWT使用自定义User表,手动签发token,自定义认证类(5星)

签发token

重点在于

1.通过用户输入的用户名和密码去数据库中查出该用户

2.获取到的用户信息生成荷载(payload),jwt模块提供了

3.通过荷载来生成toekn,jwt模块提供了

4.把含有token串的字典返回给前端

from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER class UserAPIView(ViewSet):
@action(methods=['POST', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陆成功'}
username = request.data.get('username')
password = request.data.get('password')
user = UserInfo.objects.filter(username=username, password=password).first()
if user:
# 获取荷载 直接用jwt模块提供的,缺什么导什么
payload = jwt_payload_handler(user)
# 获取token串 直接用jwt模块提供的,缺什么导什么
token = jwt_encode_handler(payload)
back_dic['token'] = token
back_dic['username'] = username
else:
back_dic['code'] = 101
back_dic['msg'] = '用户名或密码错误'
return Response(back_dic)

自定义认证类

因为认证类要重写authenticate方法,所以重点就是在authenticate方法中写下面逻辑:

1.取出客户端传入的token(后端自己规定),看是携带在请求头中,还是在请求地址中

2.验证token中的签名(jwt模块提供了)

3.通过payload得到当前登陆的用户对象(jwt模块提供了)

4.返回user对象和token(或者是其他参数)

import jwt
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
from .models import UserInfo jwt_decode_handler = api_settings.JWT_DECODE_HANDLER class MyAuthentication(BaseAuthentication):
def authenticate(self, request):
# 获取前端传的token串
jwt_value = request.META.get('HTTP_TOKEN')
if not jwt_value:
raise AuthenticationFailed('未携带token')
try:
# 获取荷载 直接用jwt模块提供的,缺什么导什么
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
msg = 'token已过期'
raise AuthenticationFailed(msg)
except jwt.DecodeError:
msg = 'token被篡改'
raise AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise AuthenticationFailed('未知错误')
# 获取用户对象 用自定义的User表获取对象
user = UserInfo.objects.filter(pk=payload['user_id']).first()
# 上面的方法每次认证都要查数据库,下面有两种方法做优化,减少数据库压力
# 这种是实例化得到user对象,没有取数据库查表,提高了性能,只是不能跨表查询
user=User(id=payload.get('user_id'),username=payload.get('username'))
# 直接组织成字典,因为我们后续主要用的是用户id,视图类中按字典取值就行了
user={'id':payload.get('user_id'),'username':payload.get('username')}
# 把对象和token返回
return user, jwt_value

登陆逻辑写在序列化类里(以后这种常写)

在views.py中

class UserAPIView(ViewSet):
@action(methods=['POST', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陆成功'}
# 调用序列化类传入参数获得序列化类的对象(所有的逻辑都在全局钩子函数里实现的)
# 可以把context={'request':request}传入,那么在序列化类中就可以获取request对象
ser = UserInfoSerializer(data=request.data)
# 如果验证通过说明已经走完字段校验及钩子函数
if ser.is_valid():
# 通过在钩子函数中对context字典中放的数据获取用户名和token并返回
username = ser.context['username']
token = ser.context['token']
back_dic['username'] = username
back_dic['token'] = token
else:
# 返回错误信息
back_dic['code'] = 101
back_dic['msg'] = ser.errors
return Response(back_dic)

在序列化类.py中

from .models import Book, UserInfo
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields = ['id', 'username', 'password']
# 字段本身的校验
username = serializers.CharField(max_length=10, min_length=3)
password = serializers.CharField(max_length=10, min_length=3)
# 全局钩子函数(核心)
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
user = UserInfo.objects.filter(username=username, password=password).first()
if not user:
raise ValidationError('用户名或密码错误')
# 获取荷载 直接用jwt模块提供的,缺什么导什么
payload = jwt_payload_handler(user)
# 获取token 直接用jwt模块提供的,缺什么导什么
token = jwt_encode_handler(payload)
# context字典是与视图函数沟通的桥梁,这里放,那里取,那里放,这里取
self.context['username'] = username
self.context['token'] = token
return attrs

补充: context字典是视图类与序列化类沟通的桥梁

在views.py中

class UserAPIView(ViewSet):
@action(methods=['POST', ], detail=False)
def login(self, request):
# 可以把context={'request':request}传入,那么在序列化类中就可以获取request对象
ser = UserInfoSerializer(data=request.data, context={'request':request})
if ser.is_valid():
...

在序列化类.py中

# 如果视图函数中传了reqeust,也可以取出
def validate(self, attrs):
request = self.context['request']
print(request.method)
...

多功能登陆

逻辑:

1.获取用户提交的用户名和密码

2.因为用户名可能是手机、邮箱、用户名,所以用正则进行判断

3.校验成功后签发token

普通版

from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
import re class UserAPIView(ViewSet):
@action(methods=['post', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陆成功'}
username = request.data.get('username')
password = request.data.get('password')
# 用正则判断到底是哪种登陆方式
re_phone = re.compile('^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$')
re_email = re.compile('^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$')
if re_phone.search(username):
user = UserInfo.objects.filter(phone=username, password=password).first()
elif re_email.search(username):
user = UserInfo.objects.filter(email=username, password=password).first()
else:
user = UserInfo.objects.filter(username=username, password=password).first()
if not user:
back_dic['code'] = 101
back_dic['msg'] = '用户名或密码错误'
else:
# 获取荷载 直接用jwt模块提供的,缺什么导什么
payload = jwt_payload_handler(user)
# 获取token 直接用jwt模块提供的,缺什么导什么
token = jwt_encode_handler(payload)
back_dic['username'] = user.username
back_dic['token'] = token
return Response(back_dic)

进阶版(逻辑写在序列化类)

在views.py中

from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
import re class UserAPIView(ViewSet):
@action(methods=['post', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陆成功'}
# 调用序列化类传入参数获得序列化类的对象(所有的逻辑都在全局钩子函数里实现的)
# 可以把context={'request':request}传入,那么在序列化类中就可以获取request对象
ser = UserInfoSerializer(data=request.data)
# 如果验证通过说明已经走完字段校验及钩子函数
if ser.is_valid():
# 通过在钩子函数中对context字典中放的数据获取用户名和token并返回
username = ser.context['username']
token = ser.context['token']
back_dic['username'] = username
back_dic['token'] = token
else:
back_dic['code'] = 101
back_dic['msg'] = ser.errors
return Response(back_dic)

在序列化类.py中

from .models import Book, UserInfo
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings
import re jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields = ['id', 'username', 'phone', 'email', 'password'] username = serializers.CharField() # 这里不重写会遇到username本身的校验从而导致验证邮箱和手机会报错
password = serializers.CharField(max_length=10, min_length=3) # 获取user对象的函数(拆分有助于扩展)
def _get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
# 用正则判断到底是哪种登陆方式
re_phone = re.compile('^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$')
re_email = re.compile('^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$')
if re_phone.search(username):
user = UserInfo.objects.filter(phone=username, password=password).first()
elif re_email.search(username):
user = UserInfo.objects.filter(email=username, password=password).first()
else:
user = UserInfo.objects.filter(username=username, password=password).first()
if not user:
raise ValidationError('用户名或密码错误')
return user def validate(self, attrs):
user = self._get_user(attrs)
# 获取荷载 直接用jwt模块提供的,缺什么导什么
payload = jwt_payload_handler(user)
# 获取token 直接用jwt模块提供的,缺什么导什么
token = jwt_encode_handler(payload)
# context字典是与视图函数沟通的桥梁,这里放,那里取,那里放,这里取
self.context['username'] = user.username
self.context['token'] = token
return attrs

drf的JWT认证的更多相关文章

  1. drf框架 - JWT认证插件

    JWT认证 JWT认证方式与其他认证方式对比: 优点 1) 服务器不要存储token,token交给每一个客户端自己存储,服务器压力小 2)服务器存储的是 签发和校验token 两段算法,签发认证的效 ...

  2. DRF之JWT认证

    一.JWT认证 JWT构成 JWT分为三段式:头.体.签名(head.payload.sgin) 头和体是可逆加密的,让服务器可以反解析出user对象,签名是不可逆加密,保证整个token的安全性的. ...

  3. drf框架中jwt认证,以及自定义jwt认证

    0909自我总结 drf框架中jwt 一.模块的安装 官方:http://getblimp.github.io/django-rest-framework-jwt/ 他是个第三方的开源项目 安装:pi ...

  4. drf组件之jwt认证

    drf组件之jwt认证模块 一.认证规则 全称:json web token 解释:加密字符串的原始数据是json,后台产生,通过web传输给前台存储 格式:三段式 - 头.载荷.签名 - 头和载荷才 ...

  5. DRF框架(七) ——三大认证组件之频率组件、jwt认证

    drf频率组件源码 1.APIView的dispatch方法的  self.initial(request,*args,**kwargs)  点进去 2.self.check_throttles(re ...

  6. DRF的JWT用户认证

    目录 DRF的JWT用户认证 JWT的认证规则 JWT的格式 JWT认证的流程 JWT模块的导入为 JWT的使用 DRF的JWT用户认证 从根本上来说,JWT是一种开放的标准(RFC 7519), 全 ...

  7. drf认证组件、权限组件、jwt认证、签发、jwt框架使用

    目录 一.注册接口 urls.py views.py serializers.py 二.登录接口 三.用户中心接口(权限校验) urls.py views.py serializers.py 四.图书 ...

  8. drf认证组件(介绍)、权限组件(介绍)、jwt认证、签发、jwt框架使用

    目录 一.注册接口 urls.py views.py serializers.py 二.登录接口 三.用户中心接口(权限校验) urls.py views.py serializers.py 四.图书 ...

  9. 9) drf JWT 认证 签发与校验token 多方式登陆 自定义认证规则反爬 admin密文显示

    一 .认证方法比较 1.认证规则图 django 前后端不分离 csrf认证 drf 前后端分离 禁用csrf 2. 认证规则演变图 数据库session认证:低效 缓存认证:高效 jwt认证:高效 ...

随机推荐

  1. LGP2155题解

    lg最优解来写题解啦( 题目大意: 多测: \[\sum_{i=1}^{n!}[\gcd(i,m!)=1] \] 根据 \(\gcd\) 的结论,我们可以得到答案其实是: \[\frac {n!} { ...

  2. ArcMap操作随记(12)

    1.[取色器]工具 [自定义]|[自定义模式]|[命令] 2.批量修改符号 [符号系统]→右键,[所有符号的属性] 3.将地图元素转换为图形 转换工具 4.好看的地图边框 [布局视图]→数据框上右键→ ...

  3. Play商店显示需要进行身份认证。您需要登录自己的Google帐户

    前段时间把一加6系统从H2OS换到OxygenOS,Play商店死活不能登录,网络配置等问题已经排除,重装Google全家桶也没有解决问题,最后找到原因. 解决办法:在应用列表中找到Google Pl ...

  4. CodeGym-17~20

    读文章 0.如果是基本数据类型的话,在数组中就存储特定的值:如果是对象的话,在数组中就是存储对象的引用. 1.数组本身就是对象 再读文章 0.Arrays.sort(array); Arrays.to ...

  5. 分布式 PostgreSQL 集群(Citus),分布式表中的分布列选择最佳实践

    确定应用程序类型 在 Citus 集群上运行高效查询要求数据在机器之间正确分布.这因应用程序类型及其查询模式而异. 大致上有两种应用程序在 Citus 上运行良好.数据建模的第一步是确定哪些应用程序类 ...

  6. JavaWeb 10_Filter过滤器

    一.什么是Filter? 1.Filter 过滤器它是JavaWeb的三大组件之一-.三大组件分别是: Servlet 程序.Listener 监听器.Filter 过滤器2.Filter 过滤器它是 ...

  7. [SPDK/NVMe存储技术分析]001 - SPDK/NVMe概述

    1. NVMe概述 NVMe是一个针对基于PCIe的固态硬盘的高性能的.可扩展的主机控制器接口. NVMe的显著特征是提供多个队列来处理I/O命令.单个NVMe设备支持多达64K个I/O 队列,每个I ...

  8. 查找goog13的ip

    C:\Users\Deen>ping 172.217.24.14 Pinging 172.217.24.14 with 32 bytes of data: Reply from 172.217. ...

  9. SpringBoot和SpringCloud?

    SpringBoot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务而SpringCloud专注于解决各个微服务之间的协调与配置,服务之间 ...

  10. spring-boot-learning-RabbitMQ

    为什么需要MQ??? 异步处理: 场景: 用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式` 用户注册-发送注册邮箱-发送注册短信都完成之后才结束返回给客户端 邮 ...