1)早期登录接口一般都是采用 账号+明文密码 直接发送到服务端做校验,数据库存储的是用户密码 md5 值;
此方法如果在没有用 https 的场景,很容易被抓包盗取用户密码;
2)另一种方法是用户密码在本地端使用 md5 转换后、再生成一个签名同时发送到服务端做校验;(常用于端对端的 API )
此方法的好处是用户密码不在网络中流通,无需担心被抓包盗取用户密码;
但有个重大的弊端就是、此时数据库中存储的 md5 转换后的用户密码就形同于明文存储了;
如果此时数据库中存储的 md5 密码被盗取后、那将可以被直接模拟登录所有用户的帐户;
3)那么我们就希望能设计一个 防止密码被抓包盗取、又能确保数据库 md5 密码被盗取后 也不会影响用户帐户安全的解决方案;
以下方案为个人研究后得出的总结,希望对大家有帮助吧!
为了方便大家快速理解,所以就直接采用中文命名啦!
【双混淆加密存储设计方案】
· 数据库 - 存储:
· DB密钥1 = md5("一层混淆A" + 明文密码) //主要用于实现离线生成签名密钥,不在网络中传输可以有效防止劫持者仿造签名密钥
· DB密钥2 = md5("二层混淆" + md5("一层混淆B" + 明文密码)) //主要用于校验用户端传来的加密密码,同时防止泄库导致密钥被盗取利用//签名密钥 = md5(md5("一层混淆A" + 明文密码) + md5("一层混淆B" + 明文密码))
· 用户端 - 登录:
· 生成 缓存密钥 = md5("一层混淆A" + 明文密码) //同等于<DB密钥1>,不在网络中流通
· 生成 传输密钥 = md5("一层混淆B" + 明文密码) //同等于<DB密钥2>的第一层
· 生成 签名密钥 = md5(缓存密钥 + 传输密钥) //也不在网络中流通· 服务端 - 接收:
· 参数1 = 用户账号
· 参数2 = 传输密钥· 生成<DB密钥2> = md5("二层混淆" + 参数2)
· 提取<DB密钥1> //通过<参数1>获取对应账户的<DB密钥1>
· 提取<DB密钥2> //通过<参数1>获取对应账户的<DB密钥2>> 验证生成的<DB密钥2>与提取的<DB密钥2>是否相同,如果相同则说明密码正确、这时才为用户创建访问令牌
· 生成<签名密钥> = md5(提取的<DB密钥1> + 参数2)
· 总结:
1)劫持者只能通过网络抓包提取到<传输密钥>,无法知道<明文密码>、也无法知道<签名密钥>
2)如果数据库存储的<DB密钥1>与<DB密钥2>泄漏、也不影响账户的安全
因为没有<明文密码>的协助就无法生成<传输密钥>、无法生成<传输密钥>就无法生成<签名密钥>
【附加 C# 代码模拟实现】
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;namespace ConsoleApplication1
{using static global::ConsoleApplication1.Helper;class Program{static void Main(){var result_11 = DataBase.Register(用户账号: "user1", 明文密码: "123456"); //注册成功!var result_12 = DataBase.Register(用户账号: "user2", 明文密码: "111222"); //注册成功!var result_13 = DataBase.Register(用户账号: "user3", 明文密码: "333444"); //注册成功!var user1 = new Client();{var result1 = user1.Login(用户账号: "user1", 明文密码: "123456"); //登录成功!var result2 = user1.TestRequest(); //请求成功!}var user2 = new Client();{var result1 = user2.Login(用户账号: "user2", 明文密码: "123456"); //登录失败,密码不正确。var result2 = user2.TestRequest(); //请先登录后再操作。}Console.ReadKey();}}/// <summary>/// 模拟 - 数据库/// </summary>class DataBase{private static DataTable _userTable = Task.Run(() =>{var dt = new DataTable();dt.Columns.Add("用户账号");dt.Columns.Add("DB密钥1");dt.Columns.Add("DB密钥2");return dt;}).Result;/// <summary>/// 查看用户表数据/// </summary>public static DataTable View { get { return DataBase._userTable; } }/// <summary>/// 用户注册/// </summary>/// <param name="用户账号"></param>/// <param name="明文密码"></param>/// <returns></returns>public static Result<string> Register(string 用户账号, string 明文密码){if (string.IsNullOrWhiteSpace(用户账号))return new Result<string>(message: "请定义一个合法的用户账号。", data: null);if (string.IsNullOrWhiteSpace(明文密码))return new Result<string>(message: "请定义一个合法的明文密码。", data: null);lock (DataBase._userTable){var count = DataBase._userTable.AsEnumerable().Where(x => x["用户账号"].ToString() == 用户账号).Count();if (count >= 1)return new Result<string>(message: $"账号“{用户账号}”已存在。", data: null);var DB密钥1 = md5("一层混淆A" + 明文密码);var DB密钥2 = md5("二层混淆" + md5("一层混淆B" + 明文密码));var dr = DataBase._userTable.NewRow();dr["用户账号"] = 用户账号;dr["DB密钥1"] = DB密钥1;dr["DB密钥2"] = DB密钥2;DataBase._userTable.Rows.Add(dr);}return new Result<string>(message: "注册成功!", data: null);}/// <summary>/// 获取指定的用户信息/// </summary>/// <param name="用户账号"></param>/// <returns></returns>public static User GetUser(string 用户账号){var dr = default(DataRow);lock(DataBase._userTable)dr = DataBase._userTable.AsEnumerable().Where(x => x["用户账号"].ToString() == 用户账号).FirstOrDefault();if (dr == null)return null;var user = new User(用户账号: dr["用户账号"].ToString(),DB密钥1: dr["DB密钥1"].ToString(),DB密钥2: dr["DB密钥2"].ToString());return user;}public class User{public string 用户账号 { get; }public string DB密钥1 { get; }public string DB密钥2 { get; }public User(string 用户账号, string DB密钥1, string DB密钥2){this.用户账号 = 用户账号;this.DB密钥1 = DB密钥1;this.DB密钥2 = DB密钥2;}}}/// <summary>/// 模拟 - 服务端/// </summary>class Service{/// <summary>/// 所有用户登录信息集合/// </summary>private static List<LoginInfo> _loginInfoCollect = new List<LoginInfo>();/// <summary>/// 查看服务端的所有用户登录信息/// </summary>public static List<LoginInfo> View { get { return Service._loginInfoCollect; } }/// <summary>/// 模拟登录接口/// </summary>/// <param name="用户账号"></param>/// <param name="传输密钥"></param>/// <returns>登录成功后将返回一个token</returns>public static Result<string> Login(string 用户账号, string 传输密钥){if (string.IsNullOrWhiteSpace(用户账号))return new Result<string>(message: "请输入您的账号。", data: null);if (string.IsNullOrWhiteSpace(传输密钥))return new Result<string>(message: "密码不可为空。", data: null);var user = DataBase.GetUser(用户账号);if (user == null)return new Result<string>(message: "指定的账号不存在。", data: null);var DB密钥2 = md5("二层混淆" + 传输密钥);if (DB密钥2 != user.DB密钥2)return new Result<string>(message: "输入的密码不正确。", data: null);var 签名密钥 = md5(user.DB密钥1 + 传输密钥);var loginInfo = new LoginInfo(登录时间: DateTime.Now,用户账号: 用户账号,签名密钥: 签名密钥,token: Guid.NewGuid().ToString("n"));Service._loginInfoCollect.Add(loginInfo);return new Result<string>(message: "登录成功!", data: loginInfo.Token);}/// <summary>/// 模拟一个测试接口/// </summary>/// <param name="token"></param>/// <returns></returns>public static Result<string> TestRequest(string token){var loginInfo = Service._loginInfoCollect.Where(x => x.Token == token).FirstOrDefault();if (loginInfo == null)return new Result<string>(message: "登录信息已过期。", data: null);return new Result<string>(message: "请求成功!", data: $"当前登录账号为“{loginInfo.用户账号}”。");}}/// <summary>/// 模拟 - 用户端/// </summary>class Client{private LoginInfo _loginInfo;/// <summary>/// 查看当前用户端登录信息/// </summary>public LoginInfo View { get { return this._loginInfo; } }/// <summary>/// 模拟客户端登录/// </summary>/// <param name="用户账号"></param>/// <param name="明文密码"></param>/// <returns>登录成功返还可用的 token</returns>public Result<string> Login(string 用户账号, string 明文密码){var 缓存密钥 = md5("一层混淆A" + 明文密码);var 传输密钥 = md5("一层混淆B" + 明文密码);var 签名密钥 = md5(缓存密钥 + 传输密钥);//模拟请求服务端登录var result = Service.Login(用户账号, 传输密钥);if (result.Message == "登录成功!"){var loginInfo = new LoginInfo(登录时间: DateTime.Now,用户账号: 用户账号,签名密钥: 签名密钥,token: result.Data);this._loginInfo = loginInfo;return new Result<string>(message: $"登录成功!", data: loginInfo.Token);}return new Result<string>(message: $"登录失败。", data: result.Message);}/// <summary>/// 模拟消息请求/// </summary>/// <returns></returns>public Result<string> TestRequest(){if (this._loginInfo == null)throw new Exception("请先登录后再操作。");//模拟请求服务端接口return Service.TestRequest(token: this._loginInfo.Token);}}struct Result<TData>{public string Message { get; }public TData Data { get; }public Result(string message, TData data){this.Message = message;this.Data = data;}}class LoginInfo{public DateTime 登录时间 { get; }public string 用户账号 { get; }public string 签名密钥 { get; }public string Token { get; }public LoginInfo(DateTime 登录时间, string 用户账号, string 签名密钥, string token){this.登录时间 = 登录时间;this.用户账号 = 用户账号;this.签名密钥 = 签名密钥;this.Token = token;}}static class Helper{public static string md5(string plainText){if (plainText == null)return plainText;var textBytes = Encoding.UTF8.GetBytes(plainText);var md5Bytes = MD5.Create().ComputeHash(textBytes);var cipherText = BitConverter.ToString(md5Bytes).Replace("-", ""); //默认为大写return cipherText;}}
}
又凌晨一点钟该休息了,改天有空再慢慢完善哈!