0%

XLNet Generalized Autoregressive Pretraining for Language Understanding

Abstract

Bert基于去噪自编码器的预训练模型,性能优于自回归语言模型的预训练方法。而由于mask一部分输入,bert忽略了被mask位置之间的依赖关系。

XLNet基于此优缺点可以

  • 通过最大化所有可能的因式分解顺序的对数似然,学习双向语境信息
  • 用自回归本身的特点克服bert的缺点
  • 融合了自回归模型Transformer-XT的思路

作者从自回归(autoregressive)和自编码(autoencoding)两大范式分析了当前的预训练语言模型。

1 介绍

1.1 AutoRegression与AutoEncoding两大阵营

XLNet是一个类似BERT的模型,XLNet是一种通用的自回归预训练方法。

自回归(AutoRegression)语言模型

AR语言模型是一种使用上下文词来预测下一个词的模型,但是上下文单词被限制在两个方向,前向和后向。(例如:GPT,GPT-2)

img-前向后向

AR语言模型擅长生成式NLP任务,生成上下文时总是前向的。

但是AR只能使用前向上下文或后向上下文,这意味着它不能同时使用前向和后向上下文。

自编码(AutoEncoding)语言模型

BERT被归类为自编码器语言模型。AE旨在从损坏的输入中重建原始数据。

损坏的输入意味着我们在预训练阶段用[MASK]替换原始词,目标是预测得到原始句子。
img-双向

AE语言模型的优势是,它可以从前向和后向的方向看到上下文。

AE语言模型的缺点是

  • [MASK]这种人为的符号在调优时在真实数据中并不存在,会导致预训练-调优的差异(pretrain-finetune discrepancy)
  • [MASK]假设mask词在给定未mask词的情况下彼此独立(例:住房危机已经变成银行业危机。 其忽略了 银行业 与 危机 间的关系)。

1.2 两大阵营需要新的XLNet

XLNet提出了一种让AR语言模型从双向上下文中学习的新方法,以避免[MASK]方法在AE语言模型中带来的缺点。

XLNet是一种泛化自回归(AR)方法,既集合了AR和AE方法的优势,又避免了二者的缺陷。

  • XLNet不使用传统AR模型中固定的前向或后向因式分解顺序,而是最大化所有可能因式分解顺序的期望对数似然,由于对因式分解顺序的排列操作,每个位置的语境都包含来自左侧和右侧的token。因此,每个位置都能学习来自所有位置的语境信息,即捕获双向语境。
  • 作为一个泛化自回归(AR)模型,XLNet不依赖残缺数据。不会有BERT的预训练-微调差异。自回归目标提供一种自然的方式,来利用乘法法则对预测token的联合概率执行因式分解(factorize),这消除了BERT中的独立性假设。
  • XLNet将Transformer-XL的分割循环机制和相对编码范式整合到预训练中。

2 Proposed Solution

2.1 在自回归模型中引入双向语言模型基本思想

XLNet仍然遵循两阶段的过程(pretrain fine-tune)。其主要希望改动第一阶段的过程,即不像BERT那样带[MASK]的Denoising-autoencoder的模式,而是采用AR语言模型的模式。

简单来说就是,输入句子X仍然使自左向右的输入,看到Ti单词的上文Context-before来预测Ti这个单词,又希望Context-before例不仅看到上文单词又能看到后面的Context-after里的下文单词。这样一来BERT预训练阶段引入的[MASK]符号就不需要了。

基本思想:在预训练阶段引入Permutation LM的训练目标。

举例:比如包含单词Ti的当前输入的句子X,由顺序的几个单词构成x1, x2, x3, x4四个单词顺序组成。假设x3为要预测的单词Ti,其所在的位置为Position 3,要想让它能够在上文Context-before中,也就是Pos 1或Pos 2的位置看到Pos 4的单词x4。可以这么做,假设固定住x3的位置,也就是仍在x3,之后随机排列组合句子中的4个单词,在所有可能里训责一部分作为模型的预训练的输入X。比如随机排列组合后x4, x2, x3, x1这样一个排列组合作为模型的输入X。于是x3就能同时看到上文x2以及下文x4的内容了。

img-permutation-language-model

看上去仍然是个自回归的从左到右的模型,但是其实通过对句子中单词排列组合,把一部分Ti下文的单词排到Ti上文位置中,于是就看到了上文和下文,但是形式上看上去仍然是从左到右在预测后一个单词。

2.2 具体实现

尽管基本思想里提到把句子X的单词排列组合后,再随机抽取例子作为输入,但实际上不可以,因为Fine-tune阶段不可能去排列组合原始输入。所以必须让预训练阶段的输入部分看上去仍然是x1, x2, x3, x4这个顺序,但是可以在Transformer部分做些工作,来达成目标。

XLNet采取了Attention掩码机制,输入的X没有任何变化,单词Ti还是第i个单词,前面有1到i-1个单词。但是Transformer内部,通过Attention掩码从X的输入单词里,也就是Ti的Context-before和Context-after中随机选择i-1个单词,放到Ti的Context-before上,把其他单词的输入通过Attention掩码隐藏掉。(当然这个所谓放到Ti的上文位置,只是一种形象的说法,其实在内部,就是通过Attention Mask,把其它没有被选到的单词Mask掉,不让它们在预测单词Ti的时候发生作用,如此而已。看着就类似于把这些被选中的单词放到了上文Context_before的位置了)。

XLNet是用双流自注意力模型实现的。一个是内容流自注意力,其实就是标准的Transformer的计算过程,主要引入了Query流自注意力。其实就是用来替代[MASK]标记的,XLNet因为要抛弃[MASK]又不能看到x3的输入,于是Query流就直接忽略掉x3的输入,只保留这个位置信息,用参数w来代表位置的embedding编码。其实XLNet只是抛弃了[MASK]这个占位符号,内部还是引入Query流来忽略掉被MASK的这个单词。和BERT比只是实现方式不同而已

img-two-stream-attention

如图所示,输入序列仍是x1, x2, x3, x4,通过不同的掩码矩阵,让当前单词Xi只能看到被排列组合后的顺序x3->x2->x4->x1中自己前面的单词。

2.3 XLNet与BERT

通过改造BERT预训练的过程,其实可以模拟XLNet的PLM过程。

第一种思路:Bert目前的做法是,给定输入句子X,随机Mask掉15%的单词,然后要求利用剩下的85%的单词去预测任意一个被Mask掉的单词。对于输入句子,随机选择X中的任意i个单词,只用这i个单词去预测被Mask的单词。这个过程理论上可以在Transformer内采用Attention Mask来实现,这样BERT的预训练模型就和XLNet基本等价了。

第二种思路:假设仍使用BERT目前的Mask机制,将Mask 15%改为,每次一个句子只Mask掉一个单词,利用剩下的单词来预测被Mask掉的单词。因为XLNet在实现的时候,为了提升效率,其实是选择每个句子最后末尾的1/K个单词被预测,假设K=7,意味着一个句子X,只有末尾的1/7的单词会被预测。这样两者基本是等价的。

XLNet维持了自回归(AR)语言模型的从左向右的模式,这个BERT做不到。明显的好处是,对于生成类的任务,能够在维持表面从左向右的生成过程前提下,模型里隐含了上下文的信息。XLNet貌似应该对于生成类型的NLP任务,会比BERT又明显优势。此外,XLNet还引入了Transformer XL机制,所以对于长文档输入类型的NLP任务,也会比BERT有明显优势。

3 总结

Permutation Language Model是XLNet的主要理论创新,它开启了自回归(AR)语言模型如何引入下文的一个思路

XLNet也相当于是BERT, GPT2.0, Transformer XL的综合体。

  • 通过Permutation LM 预训练目标,吸收了BERT的双向语言模型;
  • 吸收了GPT2.0使用更多更高质量的预训练数据;
  • 吸收了Transformer XL的主要思想。

XLNet其实本质上还是ELMO/GPT/BERT这一系列两阶段模型的进一步延伸。在自回归(AR)语言模型方向引入双向语言模型打开了新思路。

未来预期

  • Transformer天然对长文档任务处理有弱点,所以XLNet对于长文档NLP任务相比BERT应该有直接且比较明显的性能提升;
  • 对于生成类的NLP任务,XLNet的预训练模式符合下游任务序列生成结果。

Rreference

https://zhuanlan.zhihu.com/p/70257427

https://blog.csdn.net/weixin_37947156/article/details/93035607

// AR AE LM 实现步骤?
// TODO Transformer 较于 RNN?
// TODO encoding decoding?
// TODO denoising-autoencoder?

// TODO Transformer-XL 分割循环机制 相对编码范式??
// TODO BERT 预训练-微调两阶段模式(pretrain fine-tune) 抛弃mask使两阶段保持一致? fine-tune阶段?
// TODO AR模型前向后向因式分解顺序?

Java中继承与实现的思考

继承(extend): 继承传达的意思是is-a, 比如Cat is an Animal.

实现(Implement): 实现传达的意思是able一种能力, 比如Cat is swimmable.

extend

比较灵活可以继承抽象类,也可以继承实体类。
抽象类可以定义非抽象方法与抽象方法,如果有抽象方法则子类必须继承,如果子类没有重写非抽象方法则子类的实例使用的是父类的方法。

多态
如果实例化一个子类赋给一个父类(例: Animal cat = new Cat()), 则只能够使用父类中的field与method,如果method被子类重写则会调用子类的method。

implement

比较严格,若实现则必须实现所有方法。
其中的field都是static final修饰的,且只能被实现的子类获取。method不能有body,但是如果static修饰则可以有body(怎么用?)。

多态
如果实例化一个子类赋给一个接口(例: Swimmable cat = new Cat()),则只能够使用该接口的field并且无法获得field。
如果一个子类继承了一个实现了该接口的父类则可以重写方法,如果没有重写则(Swimmable cat = new Cat())中cat获取的方法则是父类中实现的方法。

Docker 部署 Lineloss

1 拉取redhat镜像

docker pull yjjy0921/redhat7.2

2 进入镜像换yum源

  • rpm安装wget(docker cp host.path cotainer:container.path)
  • wget下载yum相关包
  • rpm安装下载的相关包

报错:
python-urlgrabber >= 3.10-8 is needed by yum-3.4.3-167.el7.centos.noarch
rpm >= 0:4.11.3-22 is needed by yum-3.4.3-167.el7.centos.noarch

查询存在的python-urlgrabber包
rpm -q python-urlgrabber~~

删除python-urlgrabber包
rpm -e python-urlgrabber

更新rpm包
rpm -Uvh rpm-4.11.3-43.el7.x86_64.rpm --nodeps

3 创建aliyun.repo

cd /etc/yum.repo.d/
vim aliyun.repo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[base]
name=CentOS-$releasever - Base
baseurl=http://mirrors.aliyun.com/centos/7.8.2003/os/$basearch/
gpgcheck=1
gpgkey=http://mirrors.aliyun.com/centos/7.8.2003/os/x86_64/RPM-GPG-KEY-CentOS-7


#released updates
[updates]
name=CentOS-$releasever - Updates
baseurl=http://mirrors.aliyun.com/centos/7.8.2003/updates/$basearch/
gpgcheck=1
gpgkey=http://mirrors.aliyun.com/centos/7.8.2003/os/x86_64/RPM-GPG-KEY-CentOS-7


[extras]
name=CentOS-$releasever - Extras
baseurl=http://mirrors.aliyun.com/centos/7.8.2003/extras//$basearch/
gpgcheck=1
gpgkey=http://mirrors.aliyun.com/centos/7.8.2003/os/x86_64/RPM-GPG-KEY-CentOS-7

[centosplus]
name=CentOS-$releasever - Plus
baseurl=http://mirrors.aliyun.com/centos/7.8.2003/centosplus//$basearch/
gpgcheck=1
enabled=0

清除redhat.repo
mv redhat.repo redhat.temp

检验yum配置成功
yum update -y --skip-broken

4 安装mariadb

安装
yum install -y mysql

注意
docker无法使用systemctl指令使mysql后台运行

查看容器配置文件
docker inspect containerID

修改已经运行的容器配置文件
/usr/lib/docker...

运行镜像时需要
docker run --privileged --name=lineloss -d -it edlison/lineloss /usr/sbin/init

--privileged赋予额外权限
/usr/sbin/init将cmd设置为这个 dbus服务就会运行

systemctl start mariadb 启动服务
systemctl enable mariadb 设置开机启动
systemctl restart mariadb 重新启动
systemctl stop mariadb.service 停止MariaDB

初始化数据库
mysql_secure_installation

设置密码
mysqladmin -u root password 'newpassword'

导入数据
create database lineloss;
use lineloss;
source /home/xs_project/sql/lineloss.sql

5 Java环境

安装Java
yum install java

6 Python环境

安装python3
yum install python3

安装依赖
pip3 install Flask
pip3 install flask_sqlalchemy
pip3 install pymysql
pip3 install numpy
pip3 install sklearn
pip3 install jieba
pip3 install pandas
pip3 install statsmodels

7 运行测试

运行镜像
docker run --privileged --name=lineloss -d -p 18084:18084 -it edlison/lineloss /usr/sbin/init

Java
cd /home/xs_project/java
java -jar RuoYi.jar

Python
cd /home/xs_project/python/LineLoss
python3 service.py

8 Dockerfile

只是构建镜像???不便于执行镜像内程序???

9 其他

复制文件

容器内新建文件夹
mkdir /home/xs_project/java
mkdir /home/xs_project/python
mkdir /home/xs_project/sql

将文件复制到容器内
cp RuoYi.jar containerID:/home/xs_project/java
cp LineLoss.zip containerID:/home/xs_project/python
cp lineloss.sql containerID:/home/xs_project/sql

容器提交成镜像

docker commit -a "userid" -m "comment" containerID imageName:tag

镜像上传仓库

docker push userName/imageName

注意

tag命令修改为规范的镜像名
docker tag lineloss:latest edlison/lineloss

push到用户仓库
docker push edlison/lineloss

镜像导出导入

导出镜像
docker save -o lineloss.tar edlison/lineloss
docker save edlison/lineloss>/home/lineloss.tar

导入镜像
docker load<lineloss.tar

Attention Is All You Need

Abstract

我们提出一种全新的简洁的网络架构–Transfomer,它完全基于Attention机制,舍弃了循环与卷积。

1 Introduction

Transformer的并行性很好的解决了RNN模型的序列化输入输出。

2 Background

3 Model Architecture

大部分序列神经网络模型都具有encoder-decoder结构。

3.1 Encoder and Decoder Stacks

encoder由6个相同的层构成,每一层有两个子层。第一个是一个多Attention机制,第二个是一个简单的全连接feed-forward网络。两个子层中间部署一个参差网络,后接一个正则化。

decoder也是由6个相同的层构成。除了两个子层外中间还加入了一个第三子层Multi-head Attention.

3.2 Attention

Attention的核心内容是为输入向量的每个单词(512d)学习一个权重。在self-attention中,每个单词有3个不同的向量,Query,Key,Value,长度均是64. 他们通过3个不同的权值矩阵由嵌入向量X乘以三个不同的权值矩阵WQ,WK,WV(512 * 64)得到。

Attention计算步骤:

  • 将输入单词转换成词向量
  • 根据词向量得到q, k, v三个向量
  • 为每个向量计算一个score: score = q * kT
  • 为了梯度的稳定,Transformer使用了score归一化,即除以根号dk
  • 对score过一遍softmax函数
  • softmax点乘value得到每个输入向量的评分v
  • 想家之后得到最终的输出结果z=∑v

3.3 Multi-Head Attention

Multi-Head Attention相当于h个不同的self-attention的集成。

Multi-Head输出分三步

  • 将X分别输入到8个self-attention中得到8个加权后的特征矩阵Z
  • 将8个Z按列拼接成一个大的特征矩阵
  • 将特征矩阵经过一层fc后得到输出Z

3.4 损失层

解码器解码后,解码的特征向量通过一层激活函数为softmax的全连接层之后得到反应每个单词概率的输出向量。此时通过CTC等损失函数训练模型。

4 位置编码

Transfomer无法捕捉循序序列的能力。论文在编码词向量时引入了位置编码的特征,位置编码会在词向量中加入单词的位置信息。

在上式中, [公式] 表示单词的位置, [公式] 表示单词的维度。关于位置编码的实现可在Google开源的算法中get_timing_signal_1d()函数找到对应的代码。

作者这么设计的原因是考虑到在NLP任务重,除了单词的绝对位置,单词的相对位置也非常重要。根据公式 [公式] 以及[公式] ,这表明位置 [公式] 的位置向量可以表示为位置 [公式] 的特征向量的线性变化,这为模型捕捉单词之间的相对位置关系提供了非常大的便利。

问题

位置编码对词袋模型等具有普适性?
encoder/decoder通过self-attention层所得到的向量即理解为加密/解密?
得到最后的Z之后怎么用?

Cookie, Session, Token, JWT

Authentication(认证)

认证当前的用户的身份

互联网中的认证

  • 用户名密码登陆
  • 邮箱发送登陆链接
  • 手机号接受验证码

Authorization(授权)

用户权限

实现授权的方式

  • Cookie
  • Session
  • Token
  • OAuth

Credentials(凭证)

实现认证和授权的前提是需要一种媒介(证书)来标记访问者的身份。

在互联网应用中,一般网站(如掘金)会有两种模式,游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,一旦想要点赞/收藏/分享文章,就需要登录或者注册账号。当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能。

Cookie

  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。
  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。

Session

  • Session是另一种记录服务器和客户端会话状态的机制。
  • Session是基于Cookie实现的,Session存储在服务器端,SessionId会被存储到客户端的Cookie中。
  • Session认证流程
    • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的Session。
    • 请求返回时将此Session对应的唯一标识信息SessionId返回给浏览器。
    • 浏览器接受到服务器返回的SessionId信息后,会将此信息存入到Cookie中,同时Cookie记录此SessionId属于哪个域名。
    • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在Cookie信息,如果存在自动将Cookie信息信息也发送到服务端,服务端会从Cookie中获取SessionId,再根据SessionId查找到对应的Session信息,如果没有找到说明用户没有登陆或登陆失效,如果找到Session证明用户已经登陆可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

Cookie和Session的区别

  • 安全性:Session比Cookie安全,Session是存储在服务端的,Cookie是存储在客户端的。
  • 存取值的类型不同:Cookie只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session可以存任意数据类型。
  • 有效期不同:Cookie可设置为长时间按保持,比如我们经常使用的默认登陆功能,Session一半失效时间较短,客户端关闭或Session超时都会失效。
  • 存储大小不同:单个Cookie保存的数据不能超过4K,Session可存储数据远高于Cookie,但是当访问量过多,会占用过多的服务器资源。

Token(令牌)

Access Token

  • 访问资源接口(API)时所需要的资源凭证

  • 简单Token的组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)

  • 特点

    • 服务端无状态化、可扩展性好。
    • 支持移动端设备
    • 安全
    • 支持跨程序调用
  • Token的身份验证流程
    accessToken

    1. 客户端使用用户名跟密码请求登陆
    2. 服务端收到请求,去验证用户名与密码
    3. 验证成功后,服务端会签发一个Token并把这个Token发送到客户端
    4. 客户端收到Token后,会把它存储起来(Cookie, LocalStorage)
    5. 客户端每次向服务端请求资源的时候需要带着服务端签发的Token
    6. 服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据
  • 每一次请求都需要携带Token,需要把Token放到HTTP的Header里

  • 基于Token的用户认证是一种服务端无状态的认证方法,服务端不用存放Token数据。用解析Token的实践换取Session的存储空间,从而减轻服务器的压力,减少频繁的查询数据库

  • Token完全由应用管理,所以可以避开同源策略

Refresh Token

  • Refresh Token是专用于刷新Access Token的Token。如果没有Refresh Token,也可以刷新Access Token,但每次刷新都要用户输入登陆用户名和和密码。有了Refresh Token,客户端直接用Refresh Token去更新Access Token。

refreshToken

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
  • Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

Token和Session的区别

  • Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
  • Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
  • 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。

JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
  • 是一种认证授权机制
  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
  • 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。

原理

jwt

  • JWT认证流程
    • 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
    • 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
    • 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样Authorization: Bearer <token>
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
  • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
  • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

方法一

  • 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。
    1
    2
    3
    GET /calendar/v1/events
    Host: api.example.com
    Authorization: Bearer <token>
    • 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制
    • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。
    • 由于 JWT 是自包含的,因此减少了需要查询数据库的需要
    • JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。
    • 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

方法二

  • 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。

方法三

  • 通过URL传输。
    http://www.example.com/user?token=xxx

Token和JWT的区别

相同:

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上手保护的资源

区别:

  • Token: 服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

常见的前后端鉴权方式

  • Session-Cookie
  • Token(JWT, SSO)
  • OAuth2.0

问题

使用Cookie时需要考虑的问题

  • 因为存储在客户端,容易被客户端篡改,使用前需要验证合法性
  • 不要存储敏感数据,比如用户密码,账户余额
  • 使用 httpOnly 在一定程度上提高安全性
  • 尽量减少 cookie 的体积,能存储的数据量不能超过 4kb
  • 设置正确的 domain 和 path,减少数据传输
  • cookie 无法跨域
  • 一个浏览器针对一个网站最多存 20 个Cookie,浏览器一般只允许存放 300 个Cookie
  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用Session时需要考虑的问题

  • 将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session
  • 当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session 是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 session 的服务器,那么该服务器就无法拿到之前已经放入到 session 中的登录凭证之类的信息了。
  • 当多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie 跨域的处理。
  • sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办? 一般会把 sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现
  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用Token时需要考虑的问题

  • 如果你认为用数据库来存储 token 会导致查询时间太长,可以选择放在内存当中。比如 redis 很适合你对 token 查询的需求。
  • token 完全由应用管理,所以它可以避开同源策略
  • token 可以避免 CSRF 攻击(因为不需要 cookie 了)
  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用JWT时需要考虑的问题

  • 因为 JWT 并不依赖 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  • JWT 不加密的情况下,不能将秘密数据写入 JWT。
  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  • JWT 最大的优势是服务器不再需要存储 Session,使得服务器认证鉴权业务可以方便扩展。但这也是 JWT 最大的缺点:由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

使用加密算法时需要考虑的问题

  • 绝不要以明文存储密码
  • 永远使用 哈希算法 来处理密码,绝不要使用 Base64 或其他编码方式来存储密码,这和以明文存储密码是一样的,使用哈希,而不要使用编码。编码以及加密,都是双向的过程,而密码是保密的,应该只被它的所有者知道, 这个过程必须是单向的。哈希正是用于做这个的,从来没有解哈希这种说法, 但是编码就存在解码,加密就存在解密。
  • 绝不要使用弱哈希或已被破解的哈希算法,像 MD5 或 SHA1 ,只使用强密码哈希算法。
  • 绝不要以明文形式显示或发送密码,即使是对密码的所有者也应该这样。如果你需要 “忘记密码” 的功能,可以随机生成一个新的 一次性的(这点很重要)密码,然后把这个密码发送给用户。

只要关闭浏览器,Session就消失了
不对。对 session 来说,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。
然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间。

分布式下Session共享方案

  1. Session复制

任何一个服务器上的 session 发生改变(增删改),该节点会把这个 session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 session ,以此来保证 session 同步

优点:可容错,各个服务器间Session能够实时响应。

缺点:会对网络负荷造成一定压力,如果 session 量大的话可能会造成网络堵塞,拖慢服务器性能。

  1. 粘性Session/IP绑定策略

采用 Ngnix 中的 ip_hash 机制,将某个 ip的所有请求都定向到同一台服务器上,即将用户与服务器绑定。 用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 session 机制。

优点:简单,不需要对Session做任何个处理。

缺点:缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 session 信息都将失效。

  1. Session共享(常用)
  • 使用分布式缓存方案比如 Memcached 、Redis 来缓存 session,但是要求 Memcached 或 Redis 必须是集群
  • 把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis ,但是这种方案带来的好处也是很大的:
    • 实现了 session 共享;
    • 可以水平扩展(增加 Redis 服务器);
    • 服务器重启 session 不丢失(不过也要注意 session 在 Redis 中的刷新/失效机制);
    • 不仅可以跨服务器 session 共享,甚至可以跨平台(例如网页端和 APP 端)
  1. Session持久化

将 session 存储到数据库中,保证 session 的持久化

优点:服务器出现问题,session 不会丢失

缺点:如果网站的访问量很大,把 session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。