你好,我是朱晔。
今天,我们从安全角度来聊聊用户名、密码、身份证等敏感信息,应该怎么保存和传输。同时,你还可以进一步复习加密算法中的散列、对称加密和非对称加密算法,以及HTTPS等相关知识。
最敏感的数据恐怕就是用户的密码了。黑客一旦窃取了用户密码,或许就可以登录进用户的账号,消耗其资产、发布不良信息等;更可怕的是,有些用户至始至终都是使用一套密码,密码一旦泄露,就可以被黑客用来登录全网。
为了防止密码泄露,最重要的原则是不要保存用户密码。你可能会觉得很好笑,不保存用户密码,之后用户登录的时候怎么验证?其实,我指的是不保存原始密码,这样即使拖库也不会泄露用户密码。
我经常会听到大家说,不要明文保存用户密码,应该把密码通过MD5加密后保存。这的确是一个正确的方向,但这个说法并不准确。
首先,MD5其实不是真正的加密算法。所谓加密算法,是可以使用密钥把明文加密为密文,随后还可以使用密钥解密出明文,是双向的。
而MD5是散列、哈希算法或者摘要算法。不管多长的数据,使用MD5运算后得到的都是固定长度的摘要信息或指纹信息,无法再解密为原始数据。所以,MD5是单向的。最重要的是,仅仅使用MD5对密码进行摘要,并不安全。
比如,使用如下代码在保持用户信息时,对密码进行了MD5计算:
UserData userData = new UserData();userData.setId(1L);userData.setName(name);//密码字段使用MD5哈希后保存userData.setPassword(DigestUtils.md5Hex(password));return userRepository.save(userData);
通过输出,可以看到密码是32位的MD5:
"password": "325a2cc052914ceeb8c19016c091d2ac"
到某MD5破解网站上输入这个MD5,不到1秒就得到了原始密码:
其实你可以想一下,虽然MD5不可解密,但是我们可以构建一个超大的数据库,把所有20位以内的数字和字母组合的密码全部计算一遍MD5存进去,需要解密的时候搜索一下MD5就可以得到原始值了。这就是字典表。
目前,有些MD5解密网站使用的是彩虹表,是一种使用时间空间平衡的技术,即可以使用更大的空间来降低破解时间,也可以使用更长的破解时间来换取更小的空间。
此外,你可能会觉得多次MD5比较安全,其实并不是这样。比如,如下代码使用两次MD5进行摘要:
userData.setPassword(DigestUtils.md5Hex(DigestUtils.md5Hex( password)));
得到下面的MD5:
"password": "ebbca84993fe002bac3a54e90d677d09"
也可以破解出密码,并且破解网站还告知我们这是两次MD5算法:
所以直接保存MD5后的密码是不安全的。一些同学可能会说,还需要加盐。是的,但是加盐如果不当,还是非常不安全,比较重要的有两点。
第一,不能在代码中写死盐,且盐需要有一定的长度,比如这样:
userData.setPassword(DigestUtils.md5Hex("salt" + password));
得到了如下MD5:
"password": "58b1d63ed8492f609993895d6ba6b93a"
对于这样一串MD5,虽然破解网站上找不到原始密码,但是黑客可以自己注册一个账号,使用一个简单的密码,比如1:
"password": "55f312f84e7785aa1efa552acbf251db"
然后,再去破解网站试一下这个MD5,就可以得到原始密码是salt,也就知道了盐值是salt:
其实,知道盐是什么没什么关系,关键的是我们是在代码里写死了盐,并且盐很短、所有用户都是这个盐。这么做有三个问题:
所以,最好是每一个密码都有独立的盐,并且盐要长一点,比如超过20位。
第二,**虽然说每个人的盐最好不同,但我也不建议将一部分用户数据作为盐。**比如,使用用户名作为盐:
userData.setPassword(DigestUtils.md5Hex(name + password));
如果世界上所有的系统都是按照这个方案来保存密码,那么root、admin这样的用户使用再复杂的密码也总有一天会被破解,因为黑客们完全可以针对这些常用用户名来做彩虹表。所以,盐最好是随机的值,并且是全球唯一的,意味着全球不可能有现成的彩虹表给你用。
正确的做法是,使用全球唯一的、和用户无关的、足够长的随机值作为盐。比如,可以使用UUID作为盐,把盐一起保存到数据库中:
userData.setSalt(UUID.randomUUID().toString());userData.setPassword(DigestUtils.md5Hex(userData.getSalt() + password));
并且每次用户修改密码的时候都重新计算盐,重新保存新的密码。你可能会问,盐保存在数据库中,那被拖库了不是就可以看到了吗?难道不应该加密保存吗?
在我看来,盐没有必要加密保存。盐的作用是,防止通过彩虹表快速实现密码“解密”,如果用户的盐都是唯一的,那么生成一次彩虹表只可能拿到一个用户的密码,这样黑客的动力会小很多。
更好的做法是,不要使用像MD5这样快速的摘要算法,而是使用慢一点的算法。比如Spring Security已经废弃了MessageDigestPasswordEncoder,推荐使用BCryptPasswordEncoder,也就是BCrypt来进行密码哈希。BCrypt是为保存密码设计的算法,相比MD5要慢很多。
写段代码来测试一下MD5,以及使用不同代价因子的BCrypt,看看哈希一次密码的耗时。
private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();@GetMapping("performance")public void performance() {StopWatch stopWatch = new StopWatch();String password = "Abcd1234";stopWatch.start("MD5");//MD5DigestUtils.md5Hex(password);stopWatch.stop();stopWatch.start("BCrypt(10)");//代价因子为10的BCryptString hash1 = BCrypt.gensalt(10);BCrypt.hashpw(password, hash1);System.out.println(hash1);stopWatch.stop();stopWatch.start("BCrypt(12)");//代价因子为12的BCryptString hash2 = BCrypt.gensalt(12);BCrypt.hashpw(password, hash2);System.out.println(hash2);stopWatch.stop();stopWatch.start("BCrypt(14)");//代价因子为14的BCryptString hash3 = BCrypt.gensalt(14);BCrypt.hashpw(password, hash3);System.out.println(hash3);stopWatch.stop();log.info("{}", stopWatch.prettyPrint());}
可以看到,MD5只需要0.8毫秒,而三次BCrypt哈希(代价因子分别设置为10、12和14)耗时分别是82毫秒、312毫秒和1.2秒:
也就是说,如果制作8位密码长度的MD5彩虹表需要5个月,那么对于BCrypt来说,可能就需要几十年,大部分黑客应该都没有这个耐心。
我们写一段代码观察下,BCryptPasswordEncoder生成的密码哈希的规律:
@GetMapping("better")public UserData better(@RequestParam(value = "name", defaultValue = "zhuye") String name, @RequestParam(value = "password", defaultValue = "Abcd1234") String password) {UserData userData = new UserData();userData.setId(1L);userData.setName(name);//保存哈希后的密码userData.setPassword(passwordEncoder.encode(password));userRepository.save(userData);//判断密码是否匹配log.info("match ? {}", passwordEncoder.matches(password, userData.getPassword()));return userData;}
我们可以发现三点规律。
第一,我们调用encode、matches方法进行哈希、做密码比对的时候,不需要传入盐。BCrypt把盐作为了算法的一部分,强制我们遵循安全保存密码的最佳实践。
第二,生成的盐和哈希后的密码拼在了一起:$
是字段分隔符,其中第一个$
后的2a代表算法版本,第二个$
后的10是代价因子(默认是10,代表2的10次方次哈希),第三个$
后的22个字符是盐,再后面是摘要。所以说,我们不需要使用单独的数据库字段来保存盐。
"password": "$2a$10$wPWdQwfQO2lMxqSIb6iCROXv7lKnQq5XdMO96iCYCj7boK9pk6QPC"//格式为:$<ver>$<cost>$<salt><digest>
第三,代价因子的值越大,BCrypt哈希的耗时越久。因此,对于代价因子的值,更建议的实践是,根据用户的忍耐程度和硬件,设置一个尽可能大的值。
最后,我们需要注意的是,虽然黑客已经很难通过彩虹表来破解密码了,但是仍然有可能暴力破解密码,也就是对于同一个用户名使用常见的密码逐一尝试登录。因此,除了做好密码哈希保存的工作外,我们还要建设一套完善的安全防御机制,在感知到暴力破解危害的时候,开启短信验证、图形验证码、账号暂时锁定等防御机制来抵御暴力破解。
我们把姓名和身份证,叫做二要素。
现在互联网非常发达,很多服务都可以在网上办理,很多网站仅仅依靠二要素来确认你是谁。所以,二要素是比较敏感的数据,如果在数据库中明文保存,那么数据库被攻破后,黑客就可能拿到大量的二要素信息。如果这些二要素被用来申请贷款等,后果不堪设想。
之前我们提到的单向散列算法,显然不适合用来加密保存二要素,因为数据无法解密。这个时候,我们需要选择真正的加密算法。可供选择的算法,包括对称加密和非对称加密算法两类。
对称加密算法,是使用相同的密钥进行加密和解密。使用对称加密算法来加密双方的通信的话,双方需要先约定一个密钥,加密方才能加密,接收方才能解密。如果密钥在发送的时候被窃取,那么加密就是白忙一场。因此,这种加密方式的特点是,加密速度比较快,但是密钥传输分发有泄露风险。
非对称加密算法,或者叫公钥密码算法。公钥密码是由一对密钥对构成的,使用公钥或者说加密密钥来加密,使用私钥或者说解密密钥来解密,公钥可以任意公开,私钥不能公开。使用非对称加密的话,通信双方可以仅分享公钥用于加密,加密后的数据没有私钥无法解密。因此,这种加密方式的特点是,加密速度比较慢,但是解决了密钥的配送分发安全问题。
但是,对于保存敏感信息的场景来说,加密和解密都是我们的服务端程序,不太需要考虑密钥的分发安全性,也就是说使用非对称加密算法没有太大的意义。在这里,我们使用对称加密算法来加密数据。
接下来,我就重点与你说说对称加密算法。对称加密常用的加密算法,有DES、3DES和AES。
虽然,现在仍有许多老项目使用了DES算法,但我不推荐使用。在1999年的DES挑战赛3中,DES密码破解耗时不到一天,而现在DES密码破解更快,使用DES来加密数据非常不安全。因此,在业务代码中要避免使用DES加密。
而3DES算法,是使用不同的密钥进行三次DES串联调用,虽然解决了DES不够安全的问题,但是比AES慢,也不太推荐。
AES是当前公认的比较安全,兼顾性能的对称加密算法。不过严格来说,AES并不是实际的算法名称,而是算法标准。2000年,NIST选拔出Rijndael算法作为AES的标准。
AES有一个重要的特点就是分组加密体制,一次只能处理128位的明文,然后生成128位的密文。如果要加密很长的明文,那么就需要迭代处理,而迭代方式就叫做模式。网上很多使用AES来加密的代码,使用的是最简单的ECB模式(也叫电子密码本模式),其基本结构如下:
可以看到,这种结构有两个风险:明文和密文是一一对应的,如果明文中有重复的分组,那么密文中可以观察到重复,掌握密文的规律;因为每一个分组是独立加密和解密的 ,如果密文分组的顺序,也可以反过来操纵明文,那么就可以实现不解密密文的情况下,来修改明文。
我们写一段代码来测试下。在下面的代码中,我们使用ECB模式测试:
private static final String KEY = "secretkey1234567"; //密钥//测试ECB模式@GetMapping("ecb")public void ecb() throws Exception {Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");test(cipher, null);}//获取加密秘钥帮助方法private static SecretKeySpec setKey(String secret) {return new SecretKeySpec(secret.getBytes(), "AES");}//测试逻辑private static void test(Cipher cipher, AlgorithmParameterSpec parameterSpec) throws Exception {//初始化Ciphercipher.init(Cipher.ENCRYPT_MODE, setKey(KEY), parameterSpec);//加密测试文本System.out.println("一次:" + Hex.encodeHexString(cipher.doFinal("abcdefghijklmnop".getBytes())));//加密重复一次的测试文本System.out.println("两次:" + Hex.encodeHexString(cipher.doFinal("abcdefghijklmnopabcdefghijklmnop".getBytes())));//下面测试是否可以通过操纵密文来操纵明文//发送方账号byte[] sender = "1000000000012345".getBytes();//接收方账号byte[] receiver = "1000000000034567".getBytes();//转账金额byte[] money = "0000000010000000".getBytes();//加密发送方账号System.out.println("发送方账号:" + Hex.encodeHexString(cipher.doFinal(sender)));//加密接收方账号System.out.println("接收方账号:" + Hex.encodeHexString(cipher.doFinal(receiver)));//加密金额System.out.println("金额:" + Hex.encodeHexString(cipher.doFinal(money)));//加密完整的转账信息byte[] result = cipher.doFinal(ByteUtils.concatAll(sender, receiver, money));System.out.println("完整数据:" + Hex.encodeHexString(result));//用于操纵密文的临时字节数组byte[] hack = new byte[result.length];//把密文前两段交换System.arraycopy(result, 16, hack, 0, 16);System.arraycopy(result, 0, hack, 16, 16);System.arraycopy(result, 32, hack, 32, 16);cipher.init(Cipher.DECRYPT_MODE, setKey(KEY), parameterSpec);//尝试解密System.out.println("原始明文:" + new String(ByteUtils.concatAll(sender, receiver, money)));System.out.println("操纵密文:" + new String(cipher.doFinal(hack)));}
输出如下:
可以看到:
所以说,ECB模式虽然简单,但是不安全,不推荐使用。我们再看一下另一种常用的加密模式,CBC模式。
CBC模式,在解密或解密之前引入了XOR运算,第一个分组使用外部提供的初始化向量IV,从第二个分组开始使用前一个分组的数据,这样即使明文是一样的,加密后的密文也是不同的,并且分组的顺序不能任意调换。这就解决了ECB模式的缺陷:
我们把之前的代码修改为CBC模式,再次进行测试:
private static final String initVector = "abcdefghijklmnop"; //初始化向量@GetMapping("cbc")public void cbc() throws Exception {Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));test(cipher, iv);}
可以看到,相同的明文字符串复制一遍得到的密文并不是重复两个密文分组,并且调换密文分组的顺序无法操纵明文:
其实,除了ECB模式和CBC模式外,AES算法还有CFB、OFB、CTR模式,你可以参考这里了解它们的区别。《实用密码学》一书比较推荐的是CBC和CTR模式。还需要注意的是,ECB和CBC模式还需要设置合适的填充模式,才能处理超过一个分组的数据。
对于敏感数据保存,除了选择AES+合适模式进行加密外,我还推荐以下几个实践:
接下来,我们按照这个策略完成相关代码实现。
第一步,对于用户姓名和身份证,我们分别保存三个信息,脱敏后的明文、密文和加密ID。加密服务加密后返回密文和加密ID,随后使用加密ID来请求加密服务进行解密:
@Data@Entitypublic class UserData {@Idprivate Long id;private String idcard;//脱敏的身份证private Long idcardCipherId;//身份证加密IDprivate String idcardCipherText;//身份证密文private String name;//脱敏的姓名private Long nameCipherId;//姓名加密IDprivate String nameCipherText;//姓名密文}
第二步,加密服务数据表保存加密ID、初始化向量和密钥。加密服务表中没有密文,实现了密文和密钥分离保存:
@Data@Entitypublic class CipherData {@Id@GeneratedValue(strategy = AUTO)private Long id;private String iv;//初始化向量private String secureKey;//密钥}
第三步,加密服务使用GCM模式( Galois/Counter Mode)的AES-256对称加密算法,也就是AES-256-GCM。
这是一种AEAD(Authenticated Encryption with Associated Data)认证加密算法,除了能实现普通加密算法提供的保密性之外,还能实现可认证性和密文完整性,是目前最推荐的AES模式。
使用类似GCM的AEAD算法进行加解密,除了需要提供初始化向量和密钥之外,还可以提供一个AAD(附加认证数据,additional authenticated data),用于验证未包含在明文中的附加信息,解密时不使用加密时的AAD将解密失败。其实,GCM模式的内部使用的就是CTR模式,只不过还使用了GMAC签名算法,对密文进行签名实现完整性校验。
接下来,我们实现基于AES-256-GCM的加密服务,包含下面的主要逻辑:
这段逻辑的实现代码比较长,我加了详细注释方便你仔细阅读:
@Servicepublic class CipherService {//密钥长度public static final int AES_KEY_SIZE = 256;//初始化向量长度public static final int GCM_IV_LENGTH = 12;//GCM身份认证Tag长度public static final int GCM_TAG_LENGTH = 16;@Autowiredprivate CipherRepository cipherRepository;//内部加密方法public static byte[] doEncrypt(byte[] plaintext, SecretKey key, byte[] iv, byte[] aad) throws Exception {//加密算法Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");//Key规范SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES");//GCM参数规范GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);//加密模式cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);//设置aadif (aad != null)cipher.updateAAD(aad);//加密byte[] cipherText = cipher.doFinal(plaintext);return cipherText;}//内部解密方法public static String doDecrypt(byte[] cipherText, SecretKey key, byte[] iv, byte[] aad) throws Exception {//加密算法Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");//Key规范SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES");//GCM参数规范GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);//解密模式cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);//设置aadif (aad != null)cipher.updateAAD(aad);//解密byte[] decryptedText = cipher.doFinal(cipherText);return new String(decryptedText);}//加密入口public CipherResult encrypt(String data, String aad) throws Exception {//加密结果CipherResult encryptResult = new CipherResult();//密钥生成器KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");//生成密钥keyGenerator.init(AES_KEY_SIZE);SecretKey key = keyGenerator.generateKey();//IV数据byte[] iv = new byte[GCM_IV_LENGTH];//随机生成IVSecureRandom random = new SecureRandom();random.nextBytes(iv);//处理aadbyte[] aaddata = null;if (!StringUtils.isEmpty(aad))aaddata = aad.getBytes();//获得密文encryptResult.setCipherText(Base64.getEncoder().encodeToString(doEncrypt(data.getBytes(), key, iv, aaddata)));//加密上下文数据CipherData cipherData = new CipherData();//保存IVcipherData.setIv(Base64.getEncoder().encodeToString(iv));//保存密钥cipherData.setSecureKey(Base64.getEncoder().encodeToString(key.getEncoded()));cipherRepository.save(cipherData);//返回本地加密IDencryptResult.setId(cipherData.getId());return encryptResult;}//解密入口public String decrypt(long cipherId, String cipherText, String aad) throws Exception {//使用加密ID找到加密上下文数据CipherData cipherData = cipherRepository.findById(cipherId).orElseThrow(() -> new IllegalArgumentException("invlaid cipherId"));//加载密钥byte[] decodedKey = Base64.getDecoder().decode(cipherData.getSecureKey());//初始化密钥SecretKey originalKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");//加载IVbyte[] decodedIv = Base64.getDecoder().decode(cipherData.getIv());//处理aadbyte[] aaddata = null;if (!StringUtils.isEmpty(aad))aaddata = aad.getBytes();//解密return doDecrypt(Base64.getDecoder().decode(cipherText.getBytes()), originalKey, decodedIv, aaddata);}}
第四步,分别实现加密和解密接口用于测试。
我们可以让用户选择,如果需要保护二要素的话,就自己输入一个查询密码作为AAD。系统需要读取用户敏感信息的时候,还需要用户提供这个密码,否则无法解密。这样一来,即使黑客拿到了用户数据库的密文、加密服务的密钥和IV,也会因为缺少AAD无法解密:
@Autowiredprivate CipherService cipherService;//加密@GetMapping("right")public UserData right(@RequestParam(value = "name", defaultValue = "朱晔") String name,@RequestParam(value = "idcard", defaultValue = "300000000000001234") String idCard,@RequestParam(value = "aad", required = false)String aad) throws Exception {UserData userData = new UserData();userData.setId(1L);//脱敏姓名userData.setName(chineseName(name));//脱敏身份证userData.setIdcard(idCard(idCard));//加密姓名CipherResult cipherResultName = cipherService.encrypt(name,aad);userData.setNameCipherId(cipherResultName.getId());userData.setNameCipherText(cipherResultName.getCipherText());//加密身份证CipherResult cipherResultIdCard = cipherService.encrypt(idCard,aad);userData.setIdcardCipherId(cipherResultIdCard.getId());userData.setIdcardCipherText(cipherResultIdCard.getCipherText());return userRepository.save(userData);}//解密@GetMapping("read")public void read(@RequestParam(value = "aad", required = false)String aad) throws Exception {//查询用户信息UserData userData = userRepository.findById(1L).get();//使用AAD来解密姓名和身份证log.info("name : {} idcard : {}",cipherService.decrypt(userData.getNameCipherId(), userData.getNameCipherText(),aad),cipherService.decrypt(userData.getIdcardCipherId(), userData.getIdcardCipherText(),aad));}//脱敏身份证private static String idCard(String idCard) {String num = StringUtils.right(idCard, 4);return StringUtils.leftPad(num, StringUtils.length(idCard), "*");}//脱敏姓名public static String chineseName(String chineseName) {String name = StringUtils.left(chineseName, 1);return StringUtils.rightPad(name, StringUtils.length(chineseName), "*");
访问加密接口获得如下结果,可以看到数据库表中只有脱敏数据和密文:
{"id":1,"name":"朱*","idcard":"**************1234","idcardCipherId":26346,"idcardCipherText":"t/wIh1XTj00wJP1Lt3aGzSvn9GcqQWEwthN58KKU4KZ4Tw==","nameCipherId":26347,"nameCipherText":"+gHrk1mWmveBMVUo+CYon8Zjj9QAtw=="}
访问解密接口,可以看到解密成功了:
[21:46:00.079] [http-nio-45678-exec-6] [INFO ] [o.g.t.c.s.s.StoreIdCardController:102 ] - name : 朱晔 idcard : 300000000000001234
如果AAD输入不对,会得到如下异常:
javax.crypto.AEADBadTagException: Tag mismatch!at com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:578)at com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1116)at com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053)at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)at javax.crypto.Cipher.doFinal(Cipher.java:2164)
经过这样的设计,二要素就比较安全了。黑客要查询用户二要素的话,需要同时拿到密文、IV+密钥、AAD。而这三者可能由三方掌管,要全部拿到比较困难。
我们知道,HTTP协议传输数据使用的是明文。那在传输敏感信息的场景下,如果客户端和服务端中间有一个黑客作为中间人拦截请求,就可以窃听到这些数据,还可以修改客户端传过来的数据。这就是很大的安全隐患。
为解决这个安全隐患,有了HTTPS协议。HTTPS=SSL/TLS+HTTP,通过使用一系列加密算法来确保信息安全传输,以实现数据传输的机密性、完整性和权威性。
可以看出,理解HTTPS的流程,将有助于我们理解各种加密算法的区别,以及证书的意义。此外,SSL/TLS还是混合加密系统的一个典范,如果你需要自己开发应用层数据加密系统,也可以参考它的流程。
那么,我们就来看看HTTPS TLS 1.2连接(RSA握手)的整个过程吧。
作为准备工作,网站管理员需要申请并安装CA证书到服务端。CA证书中包含非对称加密的公钥、网站域名等信息,密钥是服务端自己保存的,不会在任何地方公开。
建立HTTPS连接的过程,首先是TCP握手,然后是TLS握手的一系列工作,包括:
接下来,客户端和服务端的所有通信都是加密通信,并且数据通过签名确保无法篡改。你可能会问,客户端怎么验证CA证书呢?
其实,CA证书是一个证书链,你可以看一下上图的左边部分:
这,就验证了用户证书的合法性,然后再校验其有效期、域名等信息进一步验证有效性。
总结一下,TLS通过巧妙的流程和算法搭配解决了传输安全问题:使用对称加密加密数据,使用非对称加密算法确保密钥无法被中间人解密;使用CA证书链认证,确保中间人无法伪造自己的证书和公钥。
如果网站涉及敏感数据的传输,必须使用HTTPS协议。作为用户,如果你看到网站不是HTTPS的或者看到无效证书警告,也不应该继续使用这个网站,以免敏感信息被泄露。
今天,我们一起学习了如何保存和传输敏感数据。我来带你回顾一下重点内容。
对于数据保存,你需要记住两点:
对于数据传输,则务必通过SSL/TLS进行传输。对于用于客户端到服务端传输数据的HTTP,我们需要使用基于SSL/TLS的HTTPS。对于一些走TCP的RPC服务,同样可以使用SSL/TLS来确保传输安全。
最后,我要提醒你的是,如果不确定应该如何实现加解密方案或流程,可以咨询公司内部的安全专家,或是参考业界各大云厂商的方案,切勿自己想当然地去设计流程,甚至创造加密算法。
今天用到的代码,我都放在了GitHub上,你可以点击这个链接查看。
关于各种加密算法,你还遇到过什么坑吗?你又是如何保存敏感数据的呢?我是朱晔,欢迎在评论区与我留言分享你的想法,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。