爆破专栏丨Spring Security系列教程之SpringSecurity中的密码加密_spring(1)

最后

现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

//放行register接口
                .antMatchers(“/user/register”)
                .permitAll()
                .antMatchers(“/user/“)
                .hasRole(“USER”)
                .antMatchers(”/app/
”)
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                //对跨域请求伪造进行防护---->csrf:利用用户带有登录状态的cookie进行攻击的手段
                .csrf()
                .disable();
    }

//配置采用哪种密码加密算法
    @Bean
    public PasswordEncoder passwordEncoder() {
        //不使用密码加密
        //return NoOpPasswordEncoder.getInstance();

//使用默认的BCryptPasswordEncoder加密方案
        return new BCryptPasswordEncoder();

//strength=10,即密钥的迭代次数(strength取值在4~31之间,默认为10)
        //return new BCryptPasswordEncoder(10);

//利用工厂类PasswordEncoderFactories实现,工厂类内部采用的是委派密码编码方案.
        //return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

}


**注意:**这里我们可以有多种创建PasswordEncoder对象的写法!并且别忘了把“/user/register”注册接口直接放行,注册接口不应该拦截。


**3. 测试运行**


最后把项目启动起来,测试一下/user/register接口,注册添加一个新的用户,可以看到添加成功后的用户信息返回如下。


![图片](https://img-blog.csdnimg.cn/img_convert/b2ecae94aaba80ef971373d2c1bef2d9.png)


与此同时,我们的数据库中,也有了相关信息:


![图片](https://img-blog.csdnimg.cn/img_convert/3c0035ee9dc918d01c45450b8d691935.png)


可以看到,我们的密码已经被BCrypt Password Encoder 方案进行了加密,此时我们进行登录时,也需要采用加密的密码才能进行访问了。


**4. BCryptPasswordEncoder加解密原理**


我前面说过,BCrypt Password Encoder加密时,每次都会随机生成一个盐值混入到密码中,以此保证即使密码明文一样,最终得到的密文也不一样。但是这时候问题就来了,这个盐值是BCryptPasswordEncoder自动生成的,我们程序员也不知道,那到时候怎么进行密码的比对呢?因为比对密码时,肯定也需要把明文添加盐值后再加密才能比对啊!别急,往下看!


**BCrypt Password Encoder调用 encode(..) 方法对密码明文加密时,每次都会随机的生成一个盐值,把这个盐值和明文再一起混淆最终得到密码的密文,所以这个最终的密文分为两部分:盐值和最终加密的结果。**


**BCryptPasswordEncoder调用matches(..)方法对比的时候,会利用自己特定的方法,先从密文里面拿出盐值,然后利用该盐值对密码的明文进行加密得到一个新的密文,最后利用这个新生成的密文和之前的密文进行对比,这样就能知道传递过来的密码是否和存储的密码是否一样了。**


**三. 利用其他Encoder进行加密实现**


**1. MessageDigestPasswordEncoder的用法**


除了可以使用上面提到的默认的BCrypt Password Encoder加密方案之外,我们还可以使用**Message Digest Password Encoder**方案,该方案内部是采用"MD5"、"SHA-1"、"SHA-256"等信息摘要算法实现的加密,所以我们需要在构造的时候传入MD5等算法名称字符串。这个配置在SecurityConfig类中实现即可!



@Bean
public MessageDigestPasswordEncoder messageDigestPasswordEncoder(){
    
    return new MessageDigestPasswordEncoder(“MD5”);
}


配置好了Message Digest Password Encoder对象,我们就可以利用该encoder对象对密码明文,比如“123”进行加密,就会得到如下密文:



{EUjIxnT/OVlk5J54s3LaJRuQgwTchm1gduFHTqI0qjo=}4b40375c57c285cc56c7048bb114db23


利用Message Digest Password Encoder 的encode(..) 加密方法,每次都会随机生成盐值,所以对相同的明文进行多次加密,每次得到的结果是不一样的。


Message Digest Password Encoder这个加密的最终结果也是分为两部分:**盐值 + MD5 (password+盐值)**。那么当我们调用 matches(..) 方法对比密码的时候,也是先从密文中得到盐值,然后利用该盐值再加密明文,最后利用这个新生成的密文和之前的密文进行对比。


**2. DelegatingPasswordEncoder的用法**


我们还有另一种加密实现写法,就是利用Delegating Password Encoder来进行实现。


Delegating Password Encoder是Spring Security 推出的一套兼容方案,该方案会根据加密类型的id字符串(idFor Encode),去自身缓存的所有加密方式中(idTo Password Encoder)取出对应的加密方案对象,然后对明文进行加密和密文的对比。Delegating Password Encoder对象的初始化,一般是使用 Spring Security 提供的一个工厂构造方法:



public static PasswordEncoder createDelegatingPasswordEncoder() {
  String encodingId = “bcrypt”;
  Map<String, PasswordEncoder> encoders = new HashMap<>();
  encoders.put(encodingId, new BCryptPasswordEncoder());
  encoders.put(“ldap”, new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
  encoders.put(“MD4”, new org.springframework.security.crypto.password.Md4PasswordEncoder());
  encoders.put(“MD5”, new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“MD5”));
  encoders.put(“noop”, org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
  encoders.put(“pbkdf2”, new Pbkdf2PasswordEncoder());
  encoders.put(“scrypt”, new SCryptPasswordEncoder());
  encoders.put(“SHA-1”, new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“SHA-1”));
  encoders.put(“SHA-256”, new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“SHA-256”));
  encoders.put(“sha256”, new org.springframework.security.crypto.password.StandardPasswordEncoder());
  encoders.put(“argon2”, new Argon2PasswordEncoder());

return new DelegatingPasswordEncoder(encodingId, encoders);
 }


这个工厂的静态构造方法把常用的几种密码方案都注入到了缓存Map中,默认注入的 encodingId 对应的是 BCrypt Password Encoder加密方案,这样系统就可以达到在新存储密码可以使用 BCrypt Password Encoder 加密方案进行加密,但是对于数据库里面以前用其他方式加密的密码也支持比对。我们可以复写该方法,然后修改这个“encodingId”的值,就可以在几种加密算法中进行切换了。


**四. 源码解析**


利用上面的代码,我们就实现了密码加密,还是很简单的,那么加密的底层原理是怎么样的呢?我们看看源码是怎么定义的吧。


**1. PasswordEncoder接口解读**


根据上文可知,Spring Security 为我们提供了一套简单易用的密码加密和比对规则,主要是利用org.springframework.security.crypto.password.PasswordEncoder 接口来进行实现,在该接口中定义了如下三个方法:



public interface PasswordEncoder {

/**
  * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
  * greater hash combined with an 8-byte or greater randomly generated salt.
  */
 String encode(CharSequence rawPassword);

/**
  * Verify the encoded password obtained from storage matches the submitted raw
  * password after it too is encoded. Returns true if the passwords match, false if
  * they do not. The stored password itself is never decoded.
  *
  * @param rawPassword the raw password to encode and match
  * @param encodedPassword the encoded password from storage to compare with
  * @return true if the raw password, after encoding, matches the encoded password from
  * storage
  */
 boolean matches(CharSequence rawPassword, String encodedPassword);

/**
  * Returns true if the encoded password should be encoded again for better security,
  * else false. The default implementation always returns false.
  * @param encodedPassword the encoded password to check
  * @return true if the encoded password should be encoded again for better security,
  * else false.
  */
 default boolean upgradeEncoding(String encodedPassword) {
  return false;
 }
}


**在PasswordEncoder接口中,有3个方法如下:**


* **encode()方法,**用于对密码进行加密,参数 rawPassword 表示我们传入的密码明文,返回值是加密之后的密文;
* **matches()方法,**表示对密码进行比对,参数 rawPassword 代表用户登录时传入的密码,encodedPassword 则代表加密后的密码(一般从数据库中查询而来);
* **upgradeEncoding()方法,**则用于判断是否需要对密码进行再次加密,以使得密码更加安全, 默认不需要。


**2. PasswordEncoder的默认实现子类**


该接口有众多的实现子类,而Spring Security默认使用的是BCryptPasswordEncoder这个子类,但注意:默认使用,并不代表就是最优的方案哦!


![图片](https://img-blog.csdnimg.cn/img_convert/d6c7beb7920fdab7797e76b50f196bc9.png)


在这些众多实现类中,其中常用的有下面这么几个子类:


* BCrypt Password Encoder:Spring Security 默认使用的加密方案,使用BCrypt强哈希方法来加密;
* Message Digest Password Encoder:用作传统的加密方式加密(支持 MD5、SHA-1、SHA-256...);
* **Delegating Password Encoder:最常用,推荐使用该方案,根据加密类型id进行不同方式的加密,兼容性强;**
* NoOpPasswordEncoder:明文,不做加密处理;
* 其他子类。


**3. matches()默认的执行时机**


密码的加密,肯定需要我们程序员自己选择一个合适的时机进行操作,比如在注册时给用户密码进行加密。这时候你可能会有疑问,我们利用Spring Security默认的登录页面进行登录时,好像咱们自己并没有手动进行密码的对比吧?


对的!Spring Security的登录页面中,密码对比是自动进行的!只要我们配置了BCrypt Password Encoder或者其他策略,Spring Security都会自动按照我们这个策略进行密码的比对,不需要我们程序员自己编码比对。那么这个自动比对是在哪里实现的呢?我们往下看!


Password Encoder接口中有个 matches()方法,默认情况下是由系统自动调用的,当我们基于数据库进行认证授权时,默认是在 **Dao Authentication Provider #additional Authentication Checks()** 方法中调用的。



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”));
  }
 }


从源码中可以看到,Spring Security的密码比对,首先会从authentication对象中获取密码值,然后通过 passwordEncoder.matches 方法来进行比对,如果比对失败则抛出BadCredentialsException异常,这就是我们登陆时Spring Security的默认密码比对实现。


**五. 实现多密码加密方案共存**


以上咱们就实现了如何在Spring Security中进行密码加解密了,这就完了吗?还没!Spring Security的密码加解密还有更高级的一个功能!


**1. 需求背景**


我们进行开发时,经常需要对老旧项目进行改造。这个老旧项目,一开始用的密码加密方案可能是MD5,后来因为种种原因,可能会觉得这个MD5加密不合适,想更新替换一种新的加密方案。但是我们进行项目开发时,密码加密方式一旦确定,基本上没法再改了,毕竟我们不能让用户重新注册再设置一次新密码吧。但是我们此时确实又想使用最新的密码加密方案,那怎么办呢?


这时候,我们就可以考虑使用Delegating Password Encoder来实现多密码加密方案了!


**2. 实现过程**


**2.1 配置DelegatingPasswordEncoder**


我们在Security Config配置类中,配置Delegating Password Encoder对象。



@Bean
public PasswordEncoder passwordEncoder() {

//利用工厂类PasswordEncoderFactories实现,工厂类内部采用的是委派密码编码方案!
    //推荐使用该方案,因为后期可以实现多密码加密方案共存效果!
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}


**2.2 定义测试接口**


为了测试出我们多密码加密共存的效果,我们定义如下3个接口,分别用3种不同的加密方案对密码进行加密。



@RestController@RequestMapping(“/user”)public class UserController {    @Autowired    private PasswordEncoder passwordEncoder;    @Autowired    private UserMapper userMapper;    @GetMapping(“hello”)    public String hello() {        return “hello, user”;    }    /**     * 采用默认的PasswordEncoder,即BCryptPasswordEncoder来加密。     *     * 添加用户.这里我们采用表单形式传参,传参形式如下:     * http://localhost:8080/user/register?username=test&password=123     /    @GetMapping(“/register”)    public User registerUser(@RequestParam(required = false) User user) {        user.setEnable(true);        user.setRoles(“ROLE_ADMIN”);        //对密码进行加密        user.setPassword(passwordEncoder.encode(user.getPassword()));        userMapper.addUser(user);        return user;    }    /*     * 利用MD5加密密码     /    @GetMapping(“/registerMd5”)    public User registerUserWithMd5(@RequestParam(required = false, name = “username”) String username, @RequestParam(required = false, name = “password”) String password) {        User user = new User();        user.setUsername(username);        user.setEnable(true);        user.setRoles(“ROLE_ADMIN”);        Map<String, PasswordEncoder> encoders = new HashMap<>(16);        //encoders.put(“bcrypt”, new BCryptPasswordEncoder());        //encoders.put(“noop”, org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());        encoders.put(“MD5”, new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“MD5”));        DelegatingPasswordEncoder md5Encoder = new DelegatingPasswordEncoder(“MD5”, encoders);        //对密码进行加密        user.setPassword(md5Encoder.encode(password));        userMapper.addUser(user);        return user;    }    /*     * 不进行密码加密     */    @GetMapping(“/registerNoop”)    public User registerUserWithNoop(@RequestParam(required = false, name = “username”) String username, @RequestParam(required = false, name = “password”) String password) {        User user = new User();        user.setUsername(username);        user.setEnable(true);        user.setRoles(“ROLE_ADMIN”);        Map<String, PasswordEncoder> encoders = new HashMap<>(16);        //encoders.put(“bcrypt”, new BCryptPasswordEncoder());        //encoders.put(“MD5”, new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“MD5”));        encoders.put(“noop”, NoOpPasswordEncoder.getInstance());        DelegatingPasswordEncoder noopEncoder = new DelegatingPasswordEncoder(“noop”, encoders);        //对密码进行加密        user.setPassword(noopEncoder.encode(password));        userMapper.addUser(user);        return user;    }}


**2.3 对以上3个接口放行**



@Override    protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                .antMatchers(“/admin/“)                .hasRole(“ADMIN”)                //放行register接口                .antMatchers(”/user/register")                .permitAll()                .antMatchers(“/user/registerMd5”)                .permitAll()                .antMatchers(“/user/registerNoop”)                .permitAll()                .antMatchers("/user/”)                .hasRole(“USER”)                .antMatchers(“/app/**”)                .permitAll()                .anyRequest()                .authenticated()                .and()                .formLogin()                .permitAll()                .and()                //对跨域请求伪造进行防护---->csrf:利用用户带有登录状态的cookie进行攻击的手段                .csrf()                .disable();    }


**2.4 测试接口**


我们在浏览器中分别请求以上的3个接口,添加3个用户。


添加一个利用MD5加密的密码用户:


![图片](https://img-blog.csdnimg.cn/img_convert/cc579edb9e7da4f9270b97638dae2ac5.png)


添加一个不加密的密码用户:


![图片](https://img-blog.csdnimg.cn/img_convert/c2d0cb1098be4c96886ca1d2a127772d.png)


我的数据库中,此时就会有3个采用不同加密方案的用户了。


![图片](https://img-blog.csdnimg.cn/img_convert/5988fd0c39e9790de37ed2e3e59eecc5.png)


然后我们可以分别利用这三个用户进行登录,可以发现在同一个项目中,实现了支持3种不同的密码加密方案的效果。



### 最后

![](https://img-blog.csdnimg.cn/img_convert/9aad69d81d81e931edcf230ca7a1ddcf.webp?x-oss-process=image/format,png)

![](https://img-blog.csdnimg.cn/img_convert/09b30201c32dd624cd382242851b40ce.webp?x-oss-process=image/format,png)

![](https://img-blog.csdnimg.cn/img_convert/35ee54a345609867a7d0a2116321361c.webp?x-oss-process=image/format,png)

![](https://img-blog.csdnimg.cn/img_convert/4e4328c9d603e91fc89457bb59aadd3d.webp?x-oss-process=image/format,png)

![](https://img-blog.csdnimg.cn/img_convert/3f97b12a9d26e71c31b2aed64103f80c.webp?x-oss-process=image/format,png)

![](https://img-blog.csdnimg.cn/img_convert/c742ff619aae474ae253681e11ed6cca.webp?x-oss-process=image/format,png)


由于篇幅原因,就不多做展示了


> **本文已被[CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)收录**

**[需要这份系统化的资料的朋友,可以点击这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

(img-GzxwFrbb-1715692597595)]

[外链图片转存中...(img-yj69fFio-1715692597595)]

[外链图片转存中...(img-fEo4rAko-1715692597596)]


由于篇幅原因,就不多做展示了


> **本文已被[CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)收录**

**[需要这份系统化的资料的朋友,可以点击这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值