1

序言

在测试的时候,当其他人修改了某一个用户密码,那么想要登陆该用户进行测试则需要重置数据库,那么此时需要增加一种特殊的登陆方式,不依靠账户:Google Authenticator

Google Authenticator

工作原理

使用一个密钥和当前时间戳来获取一个六位的一次性密码。
使用的算法:TOTP(Time-Based One-Time Password基于时间的一次性密码)
举个栗子:例如使用密钥为DPI45HKISEXU6HG7,那么Google Authenticator会根据你刚刚输入的密钥和当前的时间戳通过算法来获取一个一次性的密码。

使用方法:

1.下载:下载到google插件中
2.获取当前的密钥:本项目是使用base32加密获取:

this.token = base32.encodeAsString(appProperties.getToken().getBytes());

对于当前项目,那么如何获取加密后的密钥呢:base32 online

2.点击加号新建,可以选择手动输入验证码或者通过二维码方式输入:
image.png
image.png

启用Authenticator在项目中

1.在pom.xml引入依赖,重新加载

        <!--one time password-->
        <dependency>
            <groupId>com.j256.two-factor-auth</groupId>
            <artifactId>two-factor-auth</artifactId>
            <version>1.3</version>
        </dependency>

2.调用现成的TOTP算法生成一次性密码

public OneTimePasswordImpl(AppProperties appProperties) {
    // 将token使用base32进行转码,原理同base64
    Base32 base32 = new Base32();
    this.token = base32.encodeAsString(appProperties.getToken().getBytes());
  }

/**
   * 仅允许获取1次,获取成功后code值为null
   *
   * @return
   */
  @Override
  public Optional<String> getPassword() {
    try {
      // 直接调用现成库的方法生成一次性密码
      String password = TimeBasedOneTimePasswordUtil.generateCurrentNumberString(this.token);
      // 每个密码只能用一次,如果生成的密码与当前的密码相同,则说明短时间内请求了两次,返回empty
      if (password.equals(this.password)) {
        return Optional.empty();
      } else {
        this.password = password;
      }
    } catch (GeneralSecurityException e) {
      this.logger.error("生成一次性密码时发生错误");
      e.printStackTrace();
    }

    return Optional.of(this.password);
  }

3.建立自己的密码校验器

/**
 * 自定义密码校验器.
 * 注意:其不能够声明为@Component组件出现,否则将触发DaoAuthenticationProvider的构造函数
 * 从而直接注册DelegatingPasswordEncoder校验器
 */
public class MyBCryptPasswordEncoder extends BCryptPasswordEncoder {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());

  /**
   * 一次性密码.
   */
  private final OneTimePassword oneTimePassword;

  public MyBCryptPasswordEncoder(OneTimePassword oneTimePassword) {
    super();
    this.oneTimePassword = oneTimePassword;
  }

  @Override
  public boolean matches(CharSequence rawPassword, String encodedPassword) {
    if (rawPassword == null) {
      throw new IllegalArgumentException("rawPassword cannot be null");
    }

    // 当有一次性密码(每个密码仅能用一次)且未使用时,验证用户是否输入了超密
    Optional oneTimePassword = this.oneTimePassword.getPassword();

    // 判断是否使用一次性密码(如果密码等于通过算法获取的一次性密码,那么当前为使用超级密码登陆
    if (oneTimePassword.isPresent() && oneTimePassword.get().equals(rawPassword.toString())) {
      logger.warn("当前正在使用超级密码登录");
      return true;
    }

    // 不使用超密则执行正常的密码判断
    return super.matches(rawPassword, encodedPassword);
  }
}

4.使用自己建立的密码校验器替换默认的

@Configuration
@EnableWebSecurity
public class MvcSecurityConfig extends WebSecurityConfigurerAdapter {
  private final BCryptPasswordEncoder passwordEncoder;

  public MvcSecurityConfig(OneTimePassword oneTimePassword) {
    this.passwordEncoder = new MyBCryptPasswordEncoder(oneTimePassword);
    User.setPasswordEncoder(this.passwordEncoder);
  }

  @Bean
  PasswordEncoder passwordEncoder() {
    return this.passwordEncoder;
  }
}

5.验证是否成功

直接使用Authenticator生成的密码进行登录测试:
image.png
右下角标注的是剩余时间,该一次性密码是每个一定时间刷新(好像是60秒)。

总结

此次弄超级密码还是花费自己一定时间的,原因在于方法不对,当接到任务之后就直接复制粘贴,然后去查原理,但是没有仔细去看复制的代码的意思和思路。最后的结果就是事半功倍吧。


郝泽龙_HZ
182 声望2 粉丝