第三方组件

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

1. 前言

登陆不仅仅使用用户名登陆,还会涉及到很多登陆方法。例如:

  • 手机短信登陆
  • 验证码
  • OTP动态口令
  • 二维码扫描登陆

下面介绍常用的设计方法与组建。

2. 设计方法

2.1 二维码登陆

参考文档

2.2.1 实现原理

  • 服务器生成一个 UUID
    • UUID 与用户没有关联。
    • 这个 UUID 有失效时间。
  • 服务生成一个二维码图片
    • 图片里面包含扫描完毕二维码后的连接。
  • 服务器接受二维码扫描认证请求
    • 前提:扫描的这个手机已经登陆了
      • 将用户编号与二维码请求发送给服务器
    • 服务器判断是这个用户,并且将二维码状态设置成已经登陆
  • PC 段轮询后,发现二维码可以登陆,然后登陆成功
    • 登陆成功后要删除那个二维码。

3. j256/two-factor-auth

二次认证组件:j256/two-factor-auth

2FA使用了基于时间的一次性密码(TOTP)算法,可以结合Google Authenticator 移动应用程序、可认证的移动端或者浏览器一起使用。

Spring 的例子用到了这个组件,所以花了点时间研究一下这个组件的功能。

3.1 使用流程

下面是从官网中翻译的,其中【使用方式 1】不是太明白,感觉直接将密钥用二维码传递,会不会泄漏机密呀?

使用方式 1

  • 使用 generateBase32Secret() 产生一个 base-32 密钥. 例如: "NY4A5CPJZ46LXZCP"
  • 将这个密钥保存到数据库中,并与用户做关联。
  • 使用 qrImageUrl(...) 可以返回一个二维码 URL 地址,这里有一个GoogleAPIs的例子。如果 Google 不好用,可以选择国内的或者自己做一个。

  • 用户使用该图像将密钥加载到其验证器应用程序中

使用方式 2

  1. 用户从【认证程序】得到一个数字,并把这个数字输入到 Web 的表单中。
  2. Web 服务器从数据库中读取与用户账户的密钥。
  3. 服务将用户输入的数组与generateCurrentNumberString(密钥)做对比。
  4. 如果相同,就表示认证通过。

阮一峰-双因素认证(2FA)教程

上面的文章写的很明白。

第一步,用户开启双因素认证后,服务器生成一个密钥。

第二步:服务器提示用户扫描二维码(或者使用其他方式),把密钥保存到用户的手机。也就是说,服务器和用户的手机,现在都有了同一把密钥。

第三步,用户登录时,手机客户端使用这个密钥和当前时间戳,生成一个哈希,有效期默认为 30 秒。用户在有效期内,把这个哈希提交给服务器。

3.2 可用函数

基于 Base32

函数名说明
generateBase32Secret生成密钥
generateCurrentNumberString根据密钥生成数字字符串
generateNumber根据密钥生成数字,可以指定的参数:
1:密钥
2:当前的时间(可选)
3:间隔时间,默认 30 秒(可选)
4:OTP的 digits(可选)
validateCurrentNumber验证当前的数值可以指定的参数:
1:密钥
2:数值
3:当前的时间

基于 Hex

函数名说明
generateHexSecret生成密钥
generateCurrentNumberStringHex根据密钥生成数字字符串
generateNumberHex根据密钥生成数字,可以指定的参数:
1:密钥
2:当前的时间(可选)
3:间隔时间,默认 30 秒(可选)
4:OTP的 digits(可选)
validateCurrentNumberHex验证当前的数值可以指定的参数:
1:密钥
2:数值
3:当前的时间

其他辅助

函数名说明
qrImageUrl得到二维码的 Url
https://chart.googleapis.com/chart
国内无法访问
generateOtpAuthUrl得到 otpauth 的 Url
otpauth://totp/
国内无法访问

4. Nimbus

是这个公司https://connect2id.com/提供的开源组件,在Spring中被广泛使用,用来生成或校验jwt Token。

参考网址:

5. 加密工具

5.1 ssh-keygen 命令

ssh-keygen命令 用于为“ssh”生成、管理和转换认证密钥,它支持RSADSA两种认证密。SSH 密钥默认保留在 ~/.ssh 目录中。 如果没有 ~/.ssh 目录,ssh-keygen命令会使用正确的权限创建一个。

命令语法

ssh-keygen [选项]

命令选项

  • -b:指定密钥长度;
  • -e:读取 openssh 的私钥或者公钥文件;
  • -C:添加注释;
  • -f:指定用来保存密钥的文件名;
  • -i:读取未加密的 ssh-v2 兼容的私钥/公钥文件,然后在标准输出设备上显示 openssh 兼容的私钥/公钥;
  • -l:显示公钥文件的指纹数据;
  • -N:提供一个新密语;
  • -P:提供(旧)密语;
  • -q:静默模式;
  • -t:指定要创建的密钥类型。

示例

以下 ssh-keygen 命令默认在 ~/.ssh 目录中生成 4096 位 SSH RSA 公钥和私钥文件。 如果当前位置存在 SSH 密钥对,这些文件将被覆盖。

ssh-keygen -m PEM -t rsa -b 4096

使用ssh-kengen会在~/.ssh/目录下生成两个文件,不指定文件名和密钥类型的时候,默认生成的两个文件是:

  • id_rsa 第一个是私钥文件
  • id_rsa.pub 第二个是公钥文件

指定秘钥文件路径

ssh-keygen -t rsa -C 'rumenz@qq.com' -f ~/.ssh/github_id_rsa

5.2 OpenSSL

5.2.1 pkcs1 与 pkcs8 格式

java 使用的是 pkcs8 格式,所以要用 java 读取私钥文件,就必须转换成 PKCS8 格式。 公钥没关系,因为公钥都是 pkcs8 格式。

不管是 pkcs1 或 pkcs8 格式的私钥,导出的公钥都一样。

也有人推荐:LibresSSL。

https://www.openssl.org/

# 版本号
openssl version
# 生成私钥
openssl genrsa -out app_private_key.pem 2048
# 私钥转为PKCS8格式
openssl pkcs8 -topk8 -inform PEM -in app_private_key.pem -outform PEM -nocrypt -out app_private_key_pkcs8.pem
# 生成公钥
openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem

5.2.2 加密/解密/签名/验签

  1. 数据加密 数据揭秘的作用是防止信息泄露。 一般来说使用 RSA 的加密过程如下: (1)A(一般是服务器端,PKI 系统)生成一对公私钥对,私钥 A 自己保存,不对外公开,公钥则公开 x,通常是服务器通过某种下发证书的方式给到指定客户端; (2)B(通常为客户端)收到 A 的公钥后(一般是从证书中提取),使用 A 给的公钥对信息加密; (3)若 A 收到 B 通过公钥加密的信息,则可以使用私钥对数据进行解密; 因为私钥只有 A 拥有,也就是说 A 才能解密数据,那么及时在 A 将公钥下发的过程,或是 B 数据传输的过程,被黑客拦截,但黑客无法对数据进行解密,因此达到了防信息泄露的目的。 这个过程存在的风险在于,如果公钥被黑客截获后,黑客可以对数据进行篡改后或伪造数据,再利用公钥进行加密。A 端收到数据后能正常解密,但并不知道数据已经被篡改了或该数据是伪造的。

2.数据签名 数据签名的作用是防止数据被篡改(或者说篡改了能被发现) RSA 数据签名的过程如下: (1)B(一般是客户端)生成一对公私钥,私钥 B 自己保留(一般要求安全存储),不对外公开,公钥公开,通常随数据一块发送给对端; (2)B 使用自己的私钥对源数据的摘要加密(因为摘要是唯一且不可逆的,摘要值不变,说明数据也没变,就形成了签名,B 将源数据和签名一起发送给 A; (3)A 收到数据和签名后,使用同样的摘要算法进行摘要计算,并使用 B 给的公钥对公钥对签名值解密,若解密后的摘要值与 A 端计算的摘要值一致,则说明数据没被篡改; 这个过程中,如果黑客截获了数据和 B 发给 A 的公钥,以及数据签名,但由于只有使用私钥才能对数据进行签名,因此黑客没办法伪造数据和其签名值,因此能防止信息被篡改。但因为数据本身是明文,因此信息会被泄露。

那么没有既防止信息泄露又防止数据被篡改的方法呢,答案就是同时使用加密和签名,但通常使用非对称算法进行加密,资源消耗会远大于对称算法进行加密。

Ubuntu 系统使用 openssl 进行 RSA 加密/解密/签名/验签

  • 生成私钥:
openssl genrsa -out rsa_private_key.pem 2048

参数:genrsa 生成密钥

  • -out 输出到文件 rsa_private_key.pem 文件名 2048 长度
  • 从私钥中提取公钥
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

参数 rsa

  • -in 从文件中读入 rsa_private_key.pem
  • -pubout 输出公钥
  • -out 到文件 rsa_public_key.pem 文件名
  • 使用公钥加密:
openssl rsautl -encrypt -in readme.txt -inkey rsa_public_key.pem -pubin -out hello.en

参数 rsautl 加解密

  • -encrypt 加密
  • -in 从文件输入 readme.txt 文件名
  • -inkey 输入的密钥 rsa_public_key.pem 上一步生成的公钥
  • -pubin 表名输入是公钥文件
  • -out 输出到文件 hello.en 输出文件名
  • 使用私钥解密:
openssl rsautl -decrypt -in hello.en -inkey rsa_private_key.pem -out hello.de

参数 rsautl 加解密

  • -decrypt 解密
  • -in 从文件输入 hello.en 上一步生成的加密文件
  • -inkey 输入的密钥 rsa_private_key.pem 上一步生成的私钥
  • -out 输出到文件 hello.de 输出的文件名
  • 使用私钥签名:
openssl rsautl -sign -inkey rsa_private_key.pem -in readme.txt -out hello.sign

参数 rsautl 加解密

  • -sign 签名
  • -inkey rsa_private_key.pem 使用私钥
  • -in readme.txt 要签名的文件
  • -out hello.sign 签名
  • 使用公钥验证:
openssl rsautl -verify -inkey rsa_public_key.pem -pubin -in hello.sign -out hello.unsign

参数 rsautl 加解密

  • -verify 验证
  • -inkey rsa_public_key.pem 使用公钥
  • -pubin 表名输入是公钥文件
  • -in hello.sign 要验证的内容
  • -out hello.unsign

5.2.3 带有密码的私钥

有时候,为了安全,需要对私钥进行加密

  • 生成私钥: 使用了-aes256 加密
openssl genrsa -aes256 -passout pass:123456 -out rsa_aes_private.key 2048
  • -aes256 加密的方法
  • -passout pass:123456 输出密码到加密算法中
  • 若生成公钥,需要提供密码
openssl rsa -in rsa_aes_private.key -passin pass:123456 -pubout -out rsa_public.key

这个私钥是带有密码的,所以要输入参数

  • -passin pass:123456
  • 使用公钥加密:
openssl rsautl -encrypt -in readme.txt -inkey rsa_public.key -pubin -out hello.en

由于公钥没有加密,所以这里不需要密码

  • 使用私钥解密:
openssl rsautl -decrypt -in hello.en -inkey rsa_aes_private.key -passin pass:123456 -out hello.de

这个私钥是带有密码的,所以要输入参数

  • -passin pass:123456
  • 私钥转非加密
openssl rsa -in rsa_aes_private.key -passin pass:123456 -out rsa_private.key
  • 私钥转加密
openssl rsa -in rsa_private.key -aes256 -passout pass:111111 -out rsa_aes_private.key
  • 查看私钥明细
openssl rsa -in rsa_private.key -noout -text

输出的内容比较多,这里不显示

  • 使用-pubin 参数可查看公钥明细
openssl rsa -in rsa_public.key -pubin -noout -text

输出内容是

  • RSA Public-Key: (2048 bit)
    Modulus:
    00:d0:b2:68:23:b9:16:a6:b4:11:d1:ad:7d:ba:c7:
    26:73:6e:ef:13:69:68:d3:43:09:c3:f1:3e:9d:4e:
    9e:d4:91:aa:5e:a3:da:df:ed:d7:1e:83:e6:87:64:
    32:96:24:5d:74:ab:66:22:96:12:f5:30:c4:60:f0:
    88:33:a8:dc:10:45:9c:d3:76:44:60:d6:83:9b:8a:
    5b:7b:c0:1a:dc:f5:2e:6e:07:bc:3a:6d:f2:26:4b:
    1f:91:92:8d:e2:cd:f4:30:46:e0:28:08:94:ab:bc:
    3a:87:c4:dd:de:af:bf:c1:d5:58:f1:1c:a7:9f:f3:
    12:0d:aa:c8:66:09:f3:70:ed:4b:ef:35:52:50:48:
    de:70:9a:cc:cb:66:64:ad:cb:30:16:d2:23:67:e7:
    82:7d:44:d1:a4:f7:a5:ef:00:5a:a1:52:35:03:e7:
    97:0c:02:f6:0b:f4:ae:38:39:68:51:d3:30:6e:0f:
    95:70:07:c8:14:84:e6:99:76:59:99:69:2d:8d:b9:
    80:c5:e6:01:b3:8d:5e:5c:47:d7:35:a1:c2:dc:1d:
    c5:b1:6f:65:ff:0a:0d:87:f7:cd:ca:9d:75:6e:25:
    7e:f5:96:03:03:30:35:7e:86:c8:c2:f4:55:06:16:
    93:66:0e:1e:d8:60:48:ad:d2:c0:4f:e3:16:45:23:
    96:cd
    Exponent: 65537 (0x10001)

5.2.4 CA 自签名证书

可以参考openssl 自签名证书

简便做法

1.生成 CA 证书的私钥 key(ca.key)。以下输入了为这个 key 值设置了密码,且密码使用 aes128 加密保存:

openssl genrsa -aes128 -out ca.key 2048
# 这个key文件就是私钥文件,可以查看文件内容:
cat ca.key

2.生成公钥

openssl rsa -in ca.key -pubout -out ca-public.key
#查看这个公钥:
cat ca-public.key

3.生成 CA 的自签名证书 ca.crt

openssl req -new -x509 -days 365 -key ca.key -out ca.crt

查看自签名证书信息(颁发者):

openssl x509 -in ca.crt -subject -issuer -noout
nginx 步骤

上面的做法,省去 csr 步骤。

nginx 官方文档中有关自签名证书的生成步骤:

cd /usr/local/nginx/conf
openssl genrsa -des3 -out server.key 1024
# 生成请求证书
openssl req -new -key server.key -out server.csr
cp server.key server.key.org
# 去掉了密码
openssl rsa -in server.key.org -out server.key
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

Configure the new certificate into nginx.conf:

server {
server_name YOUR_DOMAINNAME_HERE;
listen 443;
ssl on;
ssl_certificate /usr/local/nginx/conf/server.crt;
ssl_certificate_key /usr/local/nginx/conf/server.key;
}

Restart Nginx.

5.2.5 CA 特殊写法

  • 一个简化的说明:生成 RSA 私钥和自签名证书:
openssl req -newkey rsa:2048 -nodes -keyout rsa_private.key -x509 -days 36500 -out cert.crt

req 是证书请求的子命令

  • -newkey rsa:2048
  • -nodes 表示私钥不加密,若不带参数将提示输入密码;
  • -keyout private_key.pem 表示生成私钥(PKCS8 格式)
  • -x509 表示输出证书
  • -days36500 为有效期

系统会出现提示信息,可以输入:

Generating a RSA private key
....................+++++
.+++++
writing new private key to 'rsa_private.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:BeiJing
Locality Name (eg, city) []:HaiDian
Organization Name (eg, company) [Internet Widgits Pty Ltd]:pku.edu.cn
Organizational Unit Name (eg, section) []:Student
Common Name (e.g. server FQDN or YOUR name) []:pku.edu.cn
Email Address []:fanhl@pku.org.cn

此时就生成了:未加密码的私钥rsa_private.key和 CA 自签名证书cert.crt