浅析如何使用SpringSecurity实现密码加密

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

  在Spring boot中配置密码加密非常容易,只需要修改上文配置的PasswordEnoder这个Bean的实现即可:

@Bean
PasswordEncoder passwordEncoder(){
    //参数10就是strength,即密钥的迭代次数(默认为10)
    return new BCryptPasswordEncoder(10);
}

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

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

  用户将密码从前端传来之后,通过调用BCryptPasswordEncoder实例中的encode方法对密码进行加密处理,加密完成后将密文存入数据库。

一、Spring Security实现密码加密方法

  首先,Spring Security提供了强大的加密工具PasswordEncoder,PasswordEncoder接口的代码如下:

public interface PasswordEncoder {
    String encode(CharSequence var1);
 
    boolean matches(CharSequence var1, String var2);
 
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

  该接口下定义了三个方法,encode是对加密数据的加密方法,而Boolean类型的match方法是用来验证密码和加密后密码是否一致的,如果一致则返回true。和authentication.encoding包中的PasswordEncoder接口相比,详细的可以到authentication.encoding包下查找相应的代码,这里就不列举了。

二、BCryptPasswordEncoder类

  Spring Security提供了BCryptPasswordEncoder类,该类实现了Spring的PasswordEncoder接口,使用BCrypt强哈希方法来对密码进行加密,通过BCrypt强哈希方法每一次加密的结果都不一样(补充说明:即使不同的用户注册时输入相同的密码,存入数据库的密文密码也会不同。),我们可以看看BCryptPasswordEncoder的源码:

public class BCryptPasswordEncoder implements PasswordEncoder {
    private Pattern BCRYPT_PATTERN;
    private final Log logger;
    private final int strength;
    private final SecureRandom random;
 
    public BCryptPasswordEncoder() {
        this(-1);
    }
 
    public BCryptPasswordEncoder(int strength) {
        this(strength, (SecureRandom)null);
    }
 
    public BCryptPasswordEncoder(int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
        this.logger = LogFactory.getLog(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.strength = strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }
 
    public String encode(CharSequence rawPassword) {
        String salt;
        if (this.strength > 0) {
            if (this.random != null) {
                salt = BCrypt.gensalt(this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.strength);
            }
        } else {
            salt = BCrypt.gensalt();
        }
 
        return BCrypt.hashpw(rawPassword.toString(), salt);
    }
 
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }
}

  BCryptPasswordEncoder类实现了PasswordEncoder 接口下方法,实现了方法encode,方法encode采用强哈希的BCrypt方式进行加密。

三、应用

  我们在编写用户实体类的时候,可能会对密码password进行加密,这时候我们就可以编写一个加密的方法,例如:

public void setEncodePassword(String password){
    //PasswordEncoder是一个接口,而BCryptPasswordEncoder实现了这个接口,里面有一个encode方法是对面进行加密
    PasswordEncoder encoder = new BCryptPasswordEncoder();
    String encodePWD =encoder.encode(password); //加密完之后赋值给encodePWD
    this.password = encodePWD;
}
......
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
@Slf4j
public class UserService {
    private UserDao userDao;

    public Boolean login(User user) {
        User checkedUser = userDao.checkPhoneRegister(user.getPhoneNum());
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        if(checkedUser == null) {
            String encode = encoder.encode(user.getPassword()); // 加密密码
            user.setPassword(encode);
            userDao.insert(user);
            return true;
        }
        // 判断密码是否正确
        Boolean correct = encoder.matches(user.getPassword(), checkedUser.getPassword());
        return correct;
    }
}

  用起来就很方便哦。

四、在 WebSecurityConfig 校验登录

  在 WebSecurityConfig 增加如下代码:

  @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                //配置 UserDetailsService 实现类,实现自定义登录校验
                .userDetailsService(dbUserDetailService)
                //配置密码加密规则
                .passwordEncoder(passwordEncoder());
    }
    /**
     * 密码加密,必须为 @Bean ,否则报错
     *     作用:实例化密码加密规则,该规则首先会校验数据库中存储的密码是否符合其规则(经过 BCryptPasswordEncoder 加密的密码
     * 的字符串符合一定的规则):
     *     1.若不符合,直接报错;
     *     2.若符合,则会将前端传递的密码经过 BCryptPasswordEncoder 加密,再和数据库中的密码进行比对,一样则通过
     *     所以,这里要求,我们存入进数据库的密码不能是明文,而必须是经过 BCryptPasswordEncoder 加密后,才能存入数据库
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

1、BCryptPasswordEncoder相关知识:

  用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。

  特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。

  BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。

2、BCryptPasswordEncoder 是在哪里使用的?

(1)登录时用到了 DaoAuthenticationProvider

  它有一个方法 additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication),此方法用来校验从数据库取得的用户信息和用户输入的信息是否匹配。

(2)在注册时,需要对用户密码加密

  应用 BCryptPasswordEncoder 之后,明文密码是无法被识别的,就会校验失败,只有存入密文密码才能被正常识别。所以,应该在注册时对用户密码进行加密。

private String encryptPassword(String password) {
    // BCryptPasswordEncoder 加密
    return new BCryptPasswordEncoder().encode(password);
}

 

posted @ 2021-06-15 21:29  古兰精  阅读(2596)  评论(0编辑  收藏  举报