前后端登录时密码的加密
概述
在一次 http 请求中,前后端如何安全地传输密码呢?服务器端如何安全存储密码呢?
一、明文传输、存储

1、大致步骤
用户:提供用户名和密码。
前端:密码明文传输。
后端:用户注册时,明文存储密码;
用户登录时,①读取数据库中密码,②与接收的密码进行比较以验证登录。
2、优缺点
没任何安全性可言,裸奔密码。请求被抓包,密码即暴露;数据库泄露,密码即暴露。
二、单向哈希后传输、存储

1、大致步骤
用户:提供用户名和密码。
前端:密码经单向哈希后生成指纹再发送给后端服务器。
后端:用户注册时,存储密码指纹;
用户登录时,①读取数据库中密码指纹,②与接收的密码指纹进行比较以验证登录。
2、优缺点
相比明文传输、存储,这个方式有了最基础安全防范。但密码的单向哈希,且存储时未加盐。密码极易被暴力穷举检索出来,如彩虹表攻击。现在主流的彩虹表都是100G以上了。
三、加盐存储

1、大致步骤
用户:提供用户名和密码。
前端:密码经单向哈希后生成指纹再发送给后端服务器。
后端:用户注册时,①生成随机盐,加入到接收的密码指纹中,经单向哈希后生成身份标识码;②存储盐值、身份标识码;
用户登录时,①读取数据库中盐值,加入到接收的密码指纹中,经单向哈希后生成身份标识码;②与库中身份标识码进行比较以验证登录。
2、优缺点
密码加盐可以有效地防止彩虹表攻击。即使攻击者知道了盐的内容和加盐的位置,加盐仍然大大增加了利用彩虹表攻击的难度。
这种方式的优点是,即使数据库泄露,密码仍然具有一定的安全性。
缺点:多次请求,传输的密码指纹总是固定的,并没有“加盐”保护。由于 http 请求的特性,不能直接对密码“加盐”,需要引入非对称加密【此处不引入 https,否则本篇讨论无意义】。
3.代码实现
前端:
import CryptoJS from 'crypto-js'; export default { encryptPassword(username, password){ //password 以utf-8编码,经SHA 256散列计算,返回散列值的十六进制表示 const hash = CryptoJS.SHA256(password).toString(); return {username:username,password:hash}; } }
后端:.net C# 验证登录:
using Microsoft.AspNetCore.Mvc; using System.Security.Cryptography; using System.Text; [Route("api/[controller]")] [ApiController] public class AuthenController : ControllerBase { //... [HttpPost] [Route("login")] public ResponseResult<string> Login([FromBody] LoginModel loginModel) { //... if (string.IsNullOrEmpty(user.Password) || string.IsNullOrEmpty(user.Salt)) { return false; } //读取前端传来的密码 byte[] password = Convert.FromHexString(loginModel.Password); //加盐 byte[] salt = Convert.FromBase64String(user.Salt); byte[] combined = new byte[password.Length + salt.Length]; Buffer.BlockCopy(password, 0, combined, 0, password.Length); Buffer.BlockCopy(salt, 0, combined, password.Length, salt.Length); var targetPassword = SHA256.HashData(combined); string passwordFromInput = Convert.ToBase64String(targetPassword); //验证登录 bool valid = string.Compare(passwordFromInput, user.Password, true) == 0; //... } } public class LoginModel { public string Username { get; set; } = null!; public string Password { get; set; } = null!; }
注册账号:(使用默认密码123)
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Security.Cryptography; using System.Text; [ApiController] [Route("api/[controller]")] public class UserController : ControllerBase { [HttpPost("[action]")] public ResponseResult<User> AddUser([FromBody] UserInfo userinfo) { //... string password = PasswordHelper.Encrypt("123", out string saltValue); User user = new() { Id = Guid.NewGuid().ToString(), UserName = userinfo.UserName, RealName = userinfo.RealName, //... Password = password, Salt = saltValue }; _appContext.Users.Add(user); _appContext.SaveChanges(); return new ResponseResult<User>(); } } public class PasswordHelper { public static string Encrypt(string plaintext, out string saltValue) { //plaintext="123" var bPlaintext = Encoding.UTF8.GetBytes(plaintext); var password = SHA256.HashData(bPlaintext); //生成随机盐 var salt = GetRandomSalt(); saltValue = Convert.ToBase64String(salt);//编码为base64 //加盐 byte[] combined = new byte[password.Length + salt.Length]; Buffer.BlockCopy(password, 0, combined, 0, password.Length); Buffer.BlockCopy(salt, 0, combined, password.Length, salt.Length); var targetPassword = SHA256.HashData(combined); return Convert.ToBase64String(targetPassword); } /// <summary> /// 获取随机盐值 /// </summary> public static byte[] GetRandomSalt() { return RandomNumberGenerator.GetBytes(16); } }
代码说明:
-
前端对密码明文,使用utf-8编码,哈希加密后生成密码指纹,以十六进制字符串表示;
-
后端注册账号时,从生成随机盐开始,进行密码指纹加盐,再哈希计算生成
身份标识码,将身份标识码转为base64字符串存入数据库中(盐值亦是)。 -
后端验证登录时,从以十六进制表示的字符串开始,生成 byte[],从而得到前端传来的密码指纹。
前后端共同点:(1)密码明文的编码方式:在注册、登录时,或设置默认密码时,前后端对密码明文都使用utf-8编码; (2)出入数据库时的编码方式:盐值、加盐后身份标识码都是以 base64 编码字符串【写入或读出】数据库。 (3)加盐的方式。
四、非对称加密后传输后,加盐存储——基础版

1、大致步骤
用户:提供用户名和密码。
前端:注册账号或登录时,①生成随机值;②密码经单向哈希后生成指纹,与生成的随机值一起,经公钥加密后,生成密文,③将密文发送给后端服务器。
后端:
-
用户注册时,①使用私钥解密密文,丢掉随机值,得到密码指纹;②生成随机盐,加入到密码指纹中,经单向哈希后生成
身份标识码;③存储盐值、身份标识码; -
用户登录时,①使用私钥解密密文,丢掉随机值,得到密码指纹;②读取数据库中盐值,加入到密码指纹中,经单向哈希后生成
身份标识码;③与库中身份标识码进行比较以验证登录。
2、优缺点
引入随机值,每次传输的密文不同。但RSA加密对于其加密的明文的长度有限制。同时,非对称加解密需要一些开销。另外,还需要维护公钥的安全等。
五、非对称加密后传输后,加盐存储——改进版

1、大致步骤
用户:提供用户名和密码。
前端:注册账号或登录时,①随机生成对称加密密钥;②密码经单向哈希后生成指纹,进行对称加密,生成密文1;③随机生成的对称加密密钥经公钥加密后,生成密文2;④将密文1、密文2一起发送给后端服务器。
后端:
-
用户注册时,①使用私钥解密密文2,得到对称加密密钥;②使用对称密钥解密密文,得到密码指纹;③生成随机盐,加入到哈希值中,经单向哈希后生成
身份标识码;④存储盐值、身份标识码; -
用户登录时,①使用私钥解密密文2,得到对称加密密钥;②使用对称密钥解密密文,得到密码指纹;③读取数据库中盐值,加入到哈希值中,经单向哈希后生成
身份标识码;④与库中身份标识码进行比较以验证登录。
2、优缺点
此方式虽然仍需要维护公钥,但传输的数据将不再有限制(即使是https,也需要付出维护公钥的成本)。
缺点是:开销进一步加大。
3.题外话
可将要传输的“密码指纹”换成任何你想要的数据,以达到安全传输数据的目的。
(1)与https 的差异
此方式为https的雏形,与https相比,区别在于:①此方式并不验证公钥,②此方式是应用层加密,https在传输层加密,③此方式仅对发送至服务器方向进行加密保护,登录成功后返回令牌(token)的加密未讨论,④传输内容没有防篡改。
而 https 通过2次请求完成对公钥的验证以及对“对称加密的密钥”的约定,之后才发送数据,且继续使用密钥:
(1)第一次请求:服务器返回含公钥的证书;客户端验证证书(与本地的证书进行比较),决定是否发起第二次请求;
(2)第二次请求:客户端使用公钥加密“对称加密的密钥”,再发送给服务器,以约定之后使用的“对称加密的密钥”。
——后续的请求与返回,使用约定好的对称加密的密钥来加密要传输的数据【将继续使用对称加密】
(2)安全性
即使是改进版,仍然存在安全漏洞,此方式并不能代替https。当前讨论内容,未涉及以下攻击的防止:
-
用户输入密码时,被恶意脚本截取;
-
http 明文请求和响应;
-
密码指纹与密码等价,被截取,等同于密码被截取,攻击者不需要知道密码明文;
-
前后端传输没有防篡改机制;
-
没有防重放攻击;
-
没有防止中间人攻击;
-
若数据库泄露,
身份标识码可被撞库,而泄露密码指纹; -
单次 SHA256 撞库成本极低;
-
AES 密钥密钥长度与RSA公钥、私钥长度不规范,易被暴力破解;
-
加密模式不恰当,也可能导致被攻击;
-
随机生成密钥或盐值时,随机性不足,伪随机等;
……
六、总结
对用户名和密码的验证,是其他登录方式的基础。
七、其他
浙公网安备 33010602011771号