分享一套更安全的 API 用户登录 明文密码加密 设计方案 (适合用于非https的场景)
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;
}
}
}
又凌晨一点钟该休息了,改天有空再慢慢完善哈!
本文地址:https://blog.csdn.net/rcr676/article/details/109634926
上一篇: synchronized深入理解和探究
下一篇: Redis实现分布式锁的几种方法总结