极客大学架构师训练营 系统安全架构 系统稳定高可用 PBKDF2加密算法 第11次作业

1. 导致系统不可用的原因有哪些?保障系统稳定高可用的方案有哪些?请分别列举并简述。

引起故障的原因

  • 硬件故障
  • 软件 bug
  • 系统发布
  • 并发压力
  • 网络攻击
  • 外部灾害

解决方案如下:

解耦

  • 高内聚、低耦合的组件设计原则
  • 面向对象基本设计原则
  • 面向对象设计模式
  • 领域驱动设计建模

隔离

  • 业务与子系统隔离
  • 微服务与中台架构
  • 生产者消费者隔离
  • 虚拟机与容器隔离

异步

  • 多线程编程
  • 反应式编程
  • 异步通信网络编程
  • 事件驱动异步架构

在这里插入图片描述

备份

  • 集群设计
  • 数据库复制:CAP原理
    在这里插入图片描述
    任何情况下都不能只用一台服务器提供服务,任何服务都要提供2个以上的服务器。
    都要考虑当一台服务器不可用的时候,其它服务器可以替代使用。架构师在系统架构设计的时候,都要考虑这种异常情况。

Failover (失效转移)

  • 数据库主主失效转移。
  • 负责均衡失效转移。

如何确认失效,需要转移?

设计无状态的服务。

幂等

应用调用服务失败后,会将调用请求重新发送到其它服务器,但是这个失败可能是虚假的失败。比如服务以及处理成功,但是因为网络故障应用没有收到响应,这时应用重新提交请求就导致服务重复调用,如果这个服务是一个转账操作,就会产生严重的后果。

服务重复调用有时候是无法避免的,必须保证服务重复调用和调用一次产生的结果相同,即服务具有幂等性。有些服务天然具有幂等性,比如将用户性别设置为女性,不管设置多少次,结果都一样。但是对于交易等操作,问题就会比较复杂,需要通过交易编号等信息进行服务调用有效性校验,只有有效的操作才继续执行。

事务补偿

  • 传统事务的 ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
  • 分布式事务的 BASE:基本可用(Basic Availability)、软状态(Soft-State)、最终一致性(Eventual Consistency)。

事务补偿:通过执行业务逻辑逆操作,使事务回滚到事务前状态。

重试

远程服务可能会由于线程阻塞、垃圾回收或者网络抖动,而无法及时返还响应,调用者可以通过重试的方式修复单词调用的故障。

  • 上游调用者超时时间要大于下游调用者超时时间之和。

在这里插入图片描述

熔断

当某个服务出现故障,响应延迟或者失败率增加,继续调用这个服务会导致调用者请求阻塞,资源消耗增加,进而出现服务级联失效,这种情况下使用断路器阻断对故障服务的调用。

  • 断路器三种状态:关闭,打开,半开。
  • Spring Cloud 断路器实现:Hystrix

在这里插入图片描述

限流

在高并发场景下,如果系统的访问量超过了系统的承受能力,可以通过限流对系统进行保护。限流是指对系统的用户请求进行流量限制,如果访问量超过了系统的最大处理能力,就会丢弃一部分的用户请求,保证整个系统可用,保证大部分用户是可以访问系统的。这样虽然有一部分用户的请求被丢弃,产生了部分不可用,但还是好过整个系统崩溃,所有的用户都不可用要好。

限流的几种方法:

  • 计数器算法(固定窗口,滑动窗口)
  • 令牌桶算法
  • 漏桶算法

降级

有一些系统功能是非核心的,但是它也给系统产生了非常大的压力,比如说在电商系统中又确认收货这个功能,即便我们不去确认收货,系统也会超时自动确认收货。

但是实际上确认收货这个操作是一个非常重的操作,因为它会对数据库产生很大的压力:它要进行更改订单状态,完成支付确认,并进行评价等一系列操作。如果在系统高并发的时候去完成这些操作,那么会对系统雪上加霜,使系统的处理能力更加恶化。

解决办法就是在系统高并发的时候,比如说像淘宝双11 的时候,当前可能整天系统都处于一种极限的高并发访问压力之下,这时候就可以将确认收货、评价这些非核心的功能关闭,将宝贵的系统资源留下来,给正在购物的人,让他们去完成交易。

异地多活

如果整个数据中心都不可用,比如说数据中心所在城市遭遇了地震,机房遭遇了火灾或者停电,这样的话,不管我们的设计和系统多么的高可用,系统依然是不可用的。

为了解决这个问题,同时也为了提高系统的处理能力和改善用户体验,很多大型互联网应用都采用了异地多活的多机房机构策略,也就是说数据中心分布在多个不同地点的机房里,这些机房都可以对外提供服务,用户可以连接任何一个机房进行访问,这样每个机房都可以提供完整的系统服务,即使某一个机房不可使用,机房也不会宕机,依然保持可用。

异地多活的难点是数据一致。

2. 请用你熟悉的编程语言写一个用户密码验证函数,Boolean checkPW(String 用户 ID,String 密码明文,String 密码密文)返回密码是否正确 boolean 值,密码加密算法使用你认为合适的加密算法。

在基于哈希加密的帐号系统中,用户注册和认证的大致流程如下。

  1. 用户创建自己的帐号。
  2. 密码经过哈希加密后存储在数据库中。密码一旦写入到磁盘,任何时候都不允许是明文形式。
  3. 当用户试图登录时,系统从数据库取出已经加密的密码,和经过哈希加密的用户输入的密码进行对比。
  4. 如果哈希值相同,用户将被授予访问权限。否则,告知用户他们输入的登陆凭据无效。
  5. 每当有人试图尝试登陆,就重复步骤 3 和 4。

在步骤 4 中,永远不要告诉用户输错的究竟是用户名还是密码。就像通用的提示那样,始终显示:“无效的用户名或密码。”就行了。这样可以防止攻击者在不知道密码的情况下枚举出有效的用户名。

应当注意的是,用来保护密码的哈希函数,和数据结构课学到的哈希函数是不同的。例如,实现哈希表的哈希函数设计目的是快速查找,而非安全性。只有加密哈希函数( cryptographic hash function)才可以用来进行密码哈希加密。像 SHA256 、 SHA512 、 RIPEMD 和 WHIRLPOOL 都是加密哈希函数。

人们很容易认为,Web 开发人员所做的就是:只需通过执行加密哈希函数就可以让用户密码得以安全。然而并不是这样。有很多方法可以从简单的哈希值中快速恢复出明文的密码。有几种易于实施的技术,使这些“破解”的效率大为降低。网上有这种专门破解 MD5 的网站,只需提交一个哈希值,不到一秒钟就能得到破解的结果。显然,单纯的对密码进行哈希加密远远达不到我们的安全要求。下一节将讨论一些用来破解简单密码哈希常用的手段。

有些黑客创建一个网站,用户注册的时候把密码的明文和密文都收集起来。把别的网站的密文密码拖库了以后,直接拿之前的对照表,就可以反向查询到明文。

解决方案:加盐,密码加某些特定的字符,或者用户id等。

PBKDF2(Password-Based Key Derivation Function Version 2 )算法,该算法原理大致相当于在HASH算法基础上增加随机盐,并进行多次HASH运算,随机盐使得彩虹表的建表难度大幅增加,而多次HASH也使得建表和破解的难度都大幅增加。使用PBKDF2算法时,HASH算法一般选用sha1或者sha256,随机盐的长度一般不能少于8字节,HASH次数至少也要1000次,这样安全性才足够高。
一次密码验证过程进行1000次HASH运算,对服务器来说可能只需要1ms,但对于破解者来说计算成本增加了1000倍,而至少8字节随机盐,更是把建表难度提升了N个数量级,使得大批量的破解密码几乎不可行,该算法也是美国国家标准与技术研究院推荐使用的算法。
在这里插入图片描述

package pwd;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;

public class PBKDF2 {

  public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException {
    String originalPassword = "password";
    String encryptedPassword = generateStorngPasswordHash(originalPassword);
    System.out.println(encryptedPassword);

    String originalPassword2 = "123456";
    System.out.println("origin pwd: " + originalPassword + "  ; result > " + checkPW("userId", originalPassword, encryptedPassword));
    System.out.println("origin pwd: " + originalPassword2 + "  ; result > " + checkPW("userId", originalPassword2, encryptedPassword));

    //Output:
    //1000:14cc179bf6c2b8acb3daaca45b9d2f75:2e81f5220266cdddd13f98c458ae6388abe563a63244d0e4c8d0738a2bfa0fd2bfcfcbffab9cf2ec596c64d3f1db7468b95785f5adb21ed5580972fd87f2e854
    //origin pwd: password  ; result > true
    //origin pwd: 123456  ; result > false
  }

  public static Boolean checkPW(String userId, String originalPassword, String encryptedPassword)  throws NoSuchAlgorithmException, InvalidKeySpecException {
    return validatePassword(originalPassword, encryptedPassword);
  }

  private static boolean validatePassword(String originalPassword, String storedPassword) throws NoSuchAlgorithmException, InvalidKeySpecException {
    String[] parts = storedPassword.split(":");
    int iterations = Integer.parseInt(parts[0]);
    byte[] salt = fromHex(parts[1]);
    byte[] hash = fromHex(parts[2]);


    PBEKeySpec spec = new PBEKeySpec(originalPassword.toCharArray(), salt, iterations, hash.length * 8);
    SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    byte[] testHash = skf.generateSecret(spec).getEncoded();

    int diff = hash.length ^ testHash.length;
    for(int i = 0; i < hash.length && i < testHash.length; i++) {
      diff |= hash[i] ^ testHash[i];
    }

    return diff == 0;
  }

  private static byte[] fromHex(String hex) throws NoSuchAlgorithmException {
    byte[] bytes = new byte[hex.length() / 2];
    for(int i = 0; i<bytes.length ;i++) {
      bytes[i] = (byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
    }

    return bytes;

  }

  private static String generateStorngPasswordHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
    int iterations = 1000;
    char[] chars = password.toCharArray();
    byte[] salt = getSalt();
    PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 64*8);
    SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    byte[] hash = skf.generateSecret(spec).getEncoded();

    return iterations + ":" + toHex(salt) + ":" + toHex(hash);
  }

  private static byte[] getSalt() throws NoSuchAlgorithmException {
    SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
    byte[] salt = new byte[16];
    sr.nextBytes(salt);

    return salt;
  }

  private static String toHex(byte[] array) throws NoSuchAlgorithmException {
    BigInteger bi = new BigInteger(1, array);
    String hex = bi.toString(16);
    int paddingLength = (array.length * 2) - hex.length();
    if (paddingLength > 0) {
      return String.format("%0" + paddingLength + "d", 0) + hex;
    }

    return hex;
  }
}


参考

https://www.infoq.cn/article/how-to-encrypt-the-user-password-correctly

https://cloud.tencent.com/developer/article/1186925

https://blog.csdn.net/qq_37939251/article/details/83153941

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值