写点什么

深入浅出 - 如何安全的传输密码

作者:梁歪歪 ♚
  • 2022 年 6 月 06 日
  • 本文字数:6645 字

    阅读完需:约 22 分钟

深入浅出-如何安全的传输密码

我们在平常的业务开发当中,经常遇到用户注册登录模块需要将用户密码传输到服务端的情况,当然密码类的信息在安全重要性这块不言而喻。如果,在客户端,网络或者服务端泄露了密码则会产生非常严重的后果,如下图:一个简单的用户登录表单,当我们通过 chrome 的 network 查看请求时发现我们用户的密码暴露无遗,那我们的密码将很不安全,此篇博客,记录解决这个问题的方法。


一、数据加密

想要安全的传输,最重要的也是唯一的解决方案就是加密,就好比以前苏联和德国世界大战,双方互相借助通信力量拦截情报,加密情报,反解密情报等。安全通信的本质其实就在于你的加密是否足够强大。通用意义上的加密技术分为哈希散列,对称加密和非对称加密三种。

二、哈希散列

哈希散列并不能算是严格意义上的加密技术,因此我把它称为通用意义上的加密技术,哈希散列的通用定义是:通过一些不可逆的哈希算法将原本的明文内容转化为散列后的密文内容。 由于散列算法几乎不可逆,因此攻击者几乎无法通过密文猜测到对应的明文内容。通过这种机制做到加密控制,典型的算法由我们之前耳熟能详也就是课程内用到的 md5 加密算法。


package cn.liangyy.util;
import org.apache.log4j.Logger;import org.springframework.util.CollectionUtils;
import java.security.MessageDigest;
public class MD5Util { private static final Logger logger = Logger.getLogger(MD5Util.class); public final static String MD5(String s) { char hexDigits[]={'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; try { byte[] btInput = s.getBytes(); // 获得MD5摘要算法的 MessageDigest 对象 MessageDigest mdInst = MessageDigest.getInstance("MD5"); // 使用指定的字节更新摘要 mdInst.update(btInput); // 获得密文 byte[] md = mdInst.digest(); // 把密文转换成十六进制的字符串形式 int j = md.length; char str[] = new char[j * 2]; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md[i]; str[k++] = hexDigits[byte0 >>> 4 & 0xf]; str[k++] = hexDigits[byte0 & 0xf]; } return new String(str); } catch (Exception e) { logger.error("",e); return null; } }}
复制代码


以上这种加密算法存在着一个非常大的隐患,就是对于弱口令的攻击特别容易被破解,比如我对应的用户明文密码为:


明文密码=admin


使用 md5 加密后的密文


密文密码:21232f297a57a5a743894a0e4a801fc3


很明显,只要是该21232f297a57a5a743894a0e4a801fc3字符串,那么它的明文就是admin,只要攻击者设定一些常用的弱口令密文表,则可以通过暴力破解去解读密文密码了。

三、对称加密

最典型的对称加密方式就是 AES,DES 之类的,通信双方约定一个一样的对称加密密钥,在传输前客户端使用密钥加密好后传输给服务端,服务端通过密钥解密后拿到明文,由于密钥的不同导致即便是弱口令攻击者也根本无法做对应的暴力破解手段。看似貌似非常完美,但有个致命的问题在于这个密钥存储在哪里才安全,服务端由于都是受到防火墙并且多道密码的保护,一般很少会被人攻破,但客户端,尤其是运行在浏览器上的 html,js 等文件确是直接以可运行的明文代码的方式跑在用户浏览器上的,直接在客户端加密就等于把自己的加密算法和保护密钥全直接送给攻击者,因此这类的加密方式只适用于服务端与服务端之间的交互。

四、非对称加密

非对称加密属于处理这类问题的王道,RSA 算法为非对称加密的标杆性算法,通过数学方式生成“公钥”,“私钥”对,整个服务通信过程如下:


  • 客户端在连接建立之初就获取到服务端的公钥

  • 客户端要传输数据前使用公钥加密传输内容

  • 服务端获取加密后的数据并用自己的私钥解密获取传输内容*


由于公钥是公共的,所有地方都可以被拿到使用,私钥是绝密的,只能被服务端保密持有。依靠这种简单的方式就完成密钥的安全管理和通信。


package cn.liangyy.util;
import sun.misc.BASE64Decoder;import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;import java.security.*;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.HashMap;import java.util.Map;import java.util.Random;
public class SecureMethod { public static final String KEY_ALGORTHM="RSA"; //public static final String SIGNATURE_ALGORITHM="MD5withRSA"; public static final String SIGNATURE_ALGORITHM="SHA256WithRSA";

public static final String PUBLIC_KEY = "RSAPublicKey";//公钥 public static final String PRIVATE_KEY = "RSAPrivateKey";//私钥 public static final String KEY_SHA="SHA"; public static final String KEY_MD5="MD5";

/** * BASE64解密 * @param key * @return * @throws Exception */ public static byte[] decryptBASE64(String key) throws Exception{ return (new BASE64Decoder()).decodeBuffer(key); }
/** * BASE64加密 * @param key * @return * @throws Exception */ public static String encryptBASE64(byte[] key)throws Exception{ return (new BASE64Encoder()).encodeBuffer(key); }
/** * MD5加密 * @param data * @return * @throws Exception */ public static byte[] encryptMD5(byte[] data)throws Exception{ MessageDigest md5 = MessageDigest.getInstance(KEY_MD5); md5.update(data); return md5.digest(); } /** * SHA加密 * @param data * @return * @throws Exception */ public static byte[] encryptSHA(byte[] data)throws Exception{ MessageDigest sha = MessageDigest.getInstance(KEY_SHA); sha.update(data); return sha.digest(); }

/** * 初始化密钥 * @return * @throws Exception */ public static Map<String,Object> initRSAKey()throws Exception{ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORTHM); keyPairGenerator.initialize(512); KeyPair keyPair = keyPairGenerator.generateKeyPair();
//公钥 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); //私钥 RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String,Object> keyMap = new HashMap<String, Object>(2); keyMap.put(PUBLIC_KEY, publicKey); keyMap.put(PRIVATE_KEY, privateKey);
return keyMap; } /** * 取得公钥,并转化为String类型 * @param keyMap * @return * @throws Exception */ public static String getPublicKey(Map<String, Object> keyMap)throws Exception{ Key key = (Key) keyMap.get(PUBLIC_KEY); return encryptBASE64(key.getEncoded()); }
/** * 取得私钥,并转化为String类型 * @param keyMap * @return * @throws Exception */ public static String getPrivateKey(Map<String, Object> keyMap) throws Exception{ Key key = (Key) keyMap.get(PRIVATE_KEY); return encryptBASE64(key.getEncoded()); }
/** * 用公钥加密 * @param data 加密数据 * @param key 密钥 * @return * @throws Exception */ public static byte[] encryptByPublicKey(byte[] data,String key)throws Exception{ //对公钥解密 byte[] keyBytes = decryptBASE64(key); //取公钥 X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORTHM); Key publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
//对数据解密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data); }
/** * 用私钥解密 * @param data 加密数据 * @param key 密钥 * @return * @throws Exception */ public static byte[] decryptByPrivateKey(byte[] data,String key)throws Exception{ //对私钥解密 byte[] keyBytes = decryptBASE64(key);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORTHM); Key privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); //对数据解密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data); }
/** * 用私钥加密 * @param data 加密数据 * @param key 密钥 * @return * @throws Exception */ public static byte[] encryptByPrivateKey(byte[] data,String key)throws Exception{ //解密密钥 byte[] keyBytes = decryptBASE64(key); //取私钥 PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORTHM); Key privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
//对数据加密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(data); }
/** * 用公钥解密 * @param data 加密数据 * @param key 密钥 * @return * @throws Exception */ public static byte[] decryptByPublicKey(byte[] data,String key)throws Exception{ //对私钥解密 byte[] keyBytes = decryptBASE64(key); X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORTHM); Key publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
//对数据解密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, publicKey);
return cipher.doFinal(data); }

/** * 用私钥对信息生成数字签名 * @param data //加密数据 * @param privateKey //私钥 * @return * @throws Exception */ public static String sign(byte[] data,String privateKey)throws Exception{ //解密私钥 byte[] keyBytes = decryptBASE64(privateKey); //构造PKCS8EncodedKeySpec对象 PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes); //指定加密算法 KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORTHM); //取私钥匙对象 PrivateKey privateKey2 = keyFactory.generatePrivate(pkcs8EncodedKeySpec); //用私钥对信息生成数字签名 Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); signature.initSign(privateKey2); signature.update(data);
return encryptBASE64(signature.sign()); }
/** * 校验数字签名 * @param data 加密数据 * @param publicKey 公钥 * @param sign 数字签名 * @return * @throws Exception */ public static boolean verify(byte[] data,String publicKey,String sign)throws Exception{ //解密公钥 byte[] keyBytes = decryptBASE64(publicKey); //构造X509EncodedKeySpec对象 X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes); //指定加密算法 KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORTHM); //取公钥匙对象 PublicKey publicKey2 = keyFactory.generatePublic(x509EncodedKeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); signature.initVerify(publicKey2); signature.update(data); //验证签名是否正常 return signature.verify(decryptBASE64(sign));
}


public static String generateAccessKey(){ String accessKey = CommonMethod.generateGuid(); accessKey = accessKey.substring(0,16); StringBuilder stringBuilder = new StringBuilder(); Random random = new Random(); for(int i = 0; i < accessKey.length(); i++){ char c = accessKey.charAt(i); if(CommonMethod.isNumberChar(c)) { stringBuilder.append(c); continue; } if(random.nextBoolean()){ stringBuilder.append(String.valueOf(c).toUpperCase()); }else{ stringBuilder.append(String.valueOf(c).toLowerCase()); } } return stringBuilder.toString(); } public static String generateAccessSecret(){ String accessSecret = CommonMethod.generateGuid(); StringBuilder stringBuilder = new StringBuilder(); Random random = new Random(); for(int i = 0; i < accessSecret.length(); i++){ char c = accessSecret.charAt(i); if(CommonMethod.isNumberChar(c)) { stringBuilder.append(c); continue; } if(random.nextBoolean()){ stringBuilder.append(String.valueOf(c).toUpperCase()); }else{ stringBuilder.append(String.valueOf(c).toLowerCase()); } } return stringBuilder.toString(); }}
复制代码

五、实际情况

虽然看似非对称加密完美的解决通信的安全性问题,但实际的公钥私钥长度非常长,有 1024 位的,也有 2048 位的,单纯在通信时做加解密的时间在不太好的运算处理器上都高于传输时间了,因此实际投产的过程中若要对高并发的每个请求都做到这种加解密方式未免太过于难受了。因此便有了实际落地的方案



可以看到,最精华的落地方案就是依靠非对称加解密解决初期的对称密钥生成和交换的问题,保证对称密钥可以做到


  • 每次连接到断开连接前的会话独立,减少被拦截破解后的损失,因为是会话级别的

  • 一旦对称密钥安全交换完成后,后续对应会话的所有操作都是用对称加解密通信,提高通信两端的处理性能

六、深入浅出

回到如何安全的传输密码,实际在开发中,要理解清楚并实际实现对应的通信流程机制是比较难的。因此若要确保你的应用可以安全的传输密码,最好的选择是使用https协议。

发布于: 刚刚阅读数: 3
用户头像

梁歪歪 ♚

关注

时间带走了年少轻狂,无知沉淀了冷暖自知。 2021.07.22 加入

互联网行业摸爬滚打的Java后生一枚,希望在InfoQ能够充实自己的同时,结识更多大牛。

评论

发布
暂无评论
深入浅出-如何安全的传输密码_加密_梁歪歪 ♚_InfoQ写作社区