【Spring Security】解答Spring Boot 中密码加密的正确方式?

Spring Boot 项目中密码如何加密

先说一句:密码是采用非对称加密是无法解密的。密码无法解密,还是为了确保系统安全。今天就来和大家聊一聊,密码要如何处理,才能在最大程度上确保我们的系统安全。密码加密我们一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法。

散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。

我们常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。但是仅仅使用散列函数还不够,单纯的只使用散列函数,如果两个用户密码明文相同,生成的密文也会相同,这样就增加的密码泄漏的风险。为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码密文也不相同,这可以极大的提高密码的安全性。

传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能是用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置比较繁琐。

Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoderBCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。

不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。

代码实践

commons-codec 加密

commons-codec 是一个 Apache 上的开源项目,用它可以方便的实现密码加密。在 Spring Security 还未推出 BCryptPasswordEncoder 的时候,commons-codec 还是一个比较常见的解决方案。所以,这里我先来给大家介绍下 commons-codec 的用法。

首先我们需要引入 commons-codec 的依赖:

<dependency>
 <groupId>commons-codec</groupId>
 <artifactId>commons-codec</artifactId>
 <version>1.11</version>
</dependency>

然后自定义一个 PasswordEncoder:

@Component
public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()));
    }
}

在 Spring Security 中,PasswordEncoder 专门用来处理密码的加密与比对工作,我们自定义 MyPasswordEncoder 并实现 PasswordEncoder 接口,还需要实现该接口中的两个方法:

  1. encode 方法表示对密码进行加密,参数 rawPassword 就是你传入的明文密码,返回的则是加密之后的密文,这里的加密方案采用了 MD5。
  2. matches 方法表示对密码进行比对,参数 rawPassword 相当于是用户登录时传入的密码,encodedPassword 则相当于是加密后的密码(从数据库中查询而来)。

最后记得将 MyPasswordEncoder 通过 @Component 注解标记为 Spring 容器中的一个组件。

这样用户在登录时,就会自动调用 matches 方法进行密码比对。当然,使用了 MyPasswordEncoder 之后,在用户注册时,就需要将密码加密之后存入数据库中,方式如下:

public int reg(User user) {
    ...
    //插入用户,插入之前先对密码进行加密
    user.setPassword(passwordEncoder.encode(user.getPassword()));
    result = userMapper.reg(user);
    ...
}

其实很简单,就是调用 encode 方法对密码进行加密。

BCryptPasswordEncoder 加密

但是自己定义 PasswordEncoder 还是有些麻烦,特别是处理密码加盐问题的时候。所以在 Spring Security 中提供了BCryptPasswordEncoder,使得密码加密加盐变得非常容易。只需要提供 BCryptPasswordEncoder 这个 Bean 的实例即可,代码如下:

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(10);
}

创建 BCryptPasswordEncoder 时传入的参数 10 就是 strength,即密钥的迭代次数(也可以不配置,默认为 10)。

用户信息是存储在数据库中的,因此需要在用户注册时对密码进行加密处理,如下:

@Service
public class RegService {
    public int reg(String username, String password) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
        String encodePasswod = encoder.encode(password);
        return saveToDb(username, encodePasswod);
    }
}

用户将密码从前端传来之后,通过调用 BCryptPasswordEncoder 实例中的 encode 方法对密码进行加密处理,加密完成后将密文存入数据库。我这里为了演示方便,在单元测试中修改我之前项目中的密码;代码如下:

package org.wavefar.securityconfig;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.wavefar.securityconfig.dao.UserDao;
import org.wavefar.securityconfig.entity.Role;
import org.wavefar.securityconfig.entity.User;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

@SpringBootTest
class SecurityConfigApplicationTests {
    @Autowired
    UserDao userDao;
    @Test
    void contextLoads() throws UnsupportedEncodingException {
        User u1 = new User();
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        //如果是修改内容需增加ID
        u1.setId(1L);
        u1.setUsername("summer");
        u1.setPassword(bCryptPasswordEncoder.encode("123"));
        u1.setAccountNonExpired(true);
        u1.setAccountNonLocked(true);
        u1.setCredentialsNonExpired(true);
        u1.setEnabled(true);
        List<Role> rs1 = new ArrayList<>();
        Role r1 = new Role();
        //如果是修改需增加ID
        r1.setId(1l);
        r1.setName("ROLE_admin");
        r1.setNameZh("管理员");
        rs1.add(r1);
        u1.setRoles(rs1);
        userDao.save(u1);

        User u2 = new User();
        //如果是修改需增加ID
        u2.setId(2L);
        u2.setUsername("wavefar");
        u2.setPassword(bCryptPasswordEncoder.encode("123"));

        u2.setAccountNonExpired(true);
        u2.setAccountNonLocked(true);
        u2.setCredentialsNonExpired(true);
        u2.setEnabled(true);
        List<Role> rs2 = new ArrayList<>();
        Role r2 = new Role();
        //如果是修改需增加ID
        r2.setId(2l);
        r2.setName("ROLE_user");
        r2.setNameZh("普通用户");
        rs2.add(r2);
        u2.setRoles(rs2);
        userDao.save(u2);
    }

}

执行单元测试后,我们查看数据库密码已经显示加密状态了。

 select * from t_user;
+----+---------------------+--------------------+-------------------------+---------+
| id | password                                                        | username |
+----+---------------------+--------------------+-------------------------+---------+
|  1 |   $2a$10$AzvIsb217cesGzXWklzfOe7Q8QU9vIrPgG/sYgAv75FT8fMn5FQla  | summer    |
|  2 |    $2a$10$ywbAGWFEXWCEt6.9tl7TgOCEzyA4exmh3qy3S1PXk0UG9539wIC6m | wavefar  |
+----+---------------------+--------------------+-------------------------+---------+

源码浅析

最后我们再来稍微看一下 PasswordEncoder。PasswordEncoder 是一个接口,里边只有三个方法:

public interface PasswordEncoder {
 String encode(CharSequence rawPassword);
 boolean matches(CharSequence rawPassword, String encodedPassword);
 default boolean upgradeEncoding(String encodedPassword) {
  return false;
 }
}
  • encode 方法用来对密码进行加密。
  • matches 方法用来对密码进行比对。
  • upgradeEncoding 表示是否需要对密码进行再次加密以使得密码更加安全,默认为 false。

Spring Security 为 PasswordEncoder 提供了很多实现:

图片

但是老实说,自从有了 BCryptPasswordEncoder,我们很少关注其他实现类了。PasswordEncoder 中的 encode 方法,是我们在用户注册的时候手动调用。

matches 方法,则是由系统调用,默认是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中调用的。

protected void additionalAuthenticationChecks(UserDetails userDetails,
  UsernamePasswordAuthenticationToken authentication)
  throws AuthenticationException {
 if (authentication.getCredentials() == null) {
  logger.debug("Authentication failed: no credentials provided");
  throw new BadCredentialsException(messages.getMessage(
    "AbstractUserDetailsAuthenticationProvider.badCredentials",
    "Bad credentials"));
 }
 String presentedPassword = authentication.getCredentials().toString();
 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
  logger.debug("Authentication failed: password does not match stored value");
  throw new BadCredentialsException(messages.getMessage(
    "AbstractUserDetailsAuthenticationProvider.badCredentials",
    "Bad credentials"));
 }
}

可以看到,密码比对就是通过 passwordEncoder.matches 方法来进行的。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

52it.club

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值