前言

改一个老项目,老项目原来存储密码用的明文,要升级成密文。大概分子任务有两个,一是存储密码时加密,一是将原来数据库中的密码加密。

使用BCryptPasswordEncoder加密

BCryptPasswordEncoder是springboot 项目中最常用的密码加密方式,由spring security向我们提供。
我们首先在配置文件下引入spring security

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

但是这也会导致一个问题,如果引入这个包的话就会自动启用spring security,再请求的时候就需要用户认证,未认证的用户请求会报403。但是老项目并没有使用spring security,而是自己写的登录,手动验证用户名密码。所以我需要关闭spring security作用。
在网上查了一下,解决办法是通过在启动类上添加注释去忽略security有关类。

@SpringBootApplication(exclude = { SecurityAutoConfiguration.class })

这个解决方式还有来的及验证,首先在单元测试有问题了。
单元测试在测试登录c层时,报错403,这肯定是因为security认证没有通过,解决办法是通过在
@AutoConfigureMockMvc上加入参数

@AutoConfigureMockMvc(addFilters = false)

@AuthConfigureMockMvc负责依赖注入mockMvc,而我们使用mockMvc进行模拟请求。spring security在校验时通过一条过滤器链,我们增加addFilters = false使得过滤器不执行,从而不进行认证。这个问题解决了,他的子测试类又报问题了。

    @Test
    public void getById() throws Exception {
        logger.info("新增一个学院");
        College college = collegeService.getOneSavedCollege();
        logger.info("获取新增学院,断言成功");
        Long id = college.getId();
        mockMvc.perform(get(baseUrl + "/" + id)
                .cookie(this.cookie))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(id));
    }

他在第7行的mockMvc.perform()报了java.lang.NullPointerException异常,
image.png
打断点发现是cookie为null,这就是父类通过登录获取cookie,子类通过cookie进行身份认证。
但是现在通过测试发现我们禁用过滤器获取不到cookie了。我们禁用过滤器是不想让security起作用,但是也造成了获取不到cookie了。我尝试使用其他方法解决禁用security的问题。通过网上给的方法,如加入@WithMockUser,都没有起效果,请求还是报错403。

我只想要BCryptPasswordEncoder,却因为引入他所在的包产生了负影响。询问老师后,老师给出建议能不能只引入BCryptPasswordEncoder包,在网上找了一番并没有这个包。老师说把BCryptPasswordEncoder类代码复制下来使用,BCryptPasswordEncoder类并没有依赖其他类。引入后,可以正常使用,问题解决。

将生产环境的用户密码明文变密文

因为项目在生产环境已经使用一段时间了,以为密码都变成密文了,判断密码方式也变了,我们也要讲原来用的的密码变为密文,才能不影响用户登录。思路是将所有用户取出来然后密码加密一边然后存回去。关键是如何实现只执行一次。如果密码加密两次肯定就不对了。
参考老项目是通过一个数据库存储后台版本,然后在启动时判断是否是此版本,如果不是,存储此版本并将密码加密。

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        String version = "1.0.0";
        if (!this.schemaHistoryRepository.findById(version).isPresent()) {
            this.schemaHistoryRepository.save(new SchemaHistory(version));
            List<User> users = this.userRepository.findAll();
            for (User user: users) {
                // 将未加密的数据库中的密码加密
                user.setPassword(user.getPassword());
            }
            this.userRepository.saveAll(users);
        }
    }

BCryptPasswordEncoder

在改写setPassword()方法的过程中遇到个问题,因为用户在存储密码时生成一个密文,然后用户登录时前台传给后台json类型的username和password。后台通过setPassword()为user赋值,这时前台传来的密码又加密了一编,而BCryptPasswordEncoder相同数据两次加密结果不一样。导致不能通过判断加密后的密码验证密码错误与否。
这不得不重写一个setPasswordWithEncoder()方法去加密密码,setPassword()方法保留为了接收前台传来的password。
这种解决办法使得改起来变得特别混乱,改单元测试时不知道用什么方法才对。老师建议重新建立一个类用于接收前台传来的用户对象

/**
 * 用于对登录的user实体接收
 */
public class LoginUser {
  private String username;

  private String password;

  public LoginUser(String username, String password) {
    this.username = username;
    this.password = password;
  }

  public LoginUser() {
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }
}

用user实体类的setPassword()方法进行密码加密。接收的时候使用LoginUser,password还是前台传来的,这样就能正确判断密码正确与否了。
刚才我们说BCryptPasswordEncoder对相同数据两次加密结果不相同,这是因为BCryptPasswordEncoder在加密时加入了一个随机生成的盐值,两次生成的盐值不一样,导致加密后的结果不一样。那么他是如何认证密码是否正确的呢?
每次生成盐值时会将明文与对应盐值存储起来,在验证时获取到对应的盐值再进行加密,比较两次加密后的结果是否相同。
image.png
这个盐值也写到了加密后的密码中
想要对加密算法了解更多,请参考[密码安全性策略] (https://segmentfault.com/a/11...

总结

感谢潘老师在解决问题中给予的指导。

参考

https://www.zhihu.com/questio...
https://segmentfault.com/a/11...


小强Zzz
1.2k 声望32 粉丝

引用和评论

0 条评论