Spring Secuirty 密码加密认证讲解

  • Post author:
  • Post category:其他



目录


一、密码加密介绍


1.1 常见加密的策略


1.Hash 算法


2.单向自适应函数


二、Security加密结构


2.1 PasswordEncoder


2.2 密码认证流程


2.3 DeletaingPasswordEncoder


1.为什么默认一个代理类不是具体的呢?


2. DeletaingPasswordEncoder 的装配原理


3.如何自定义PasswordEncoder


2.3 密码升级


1.原理


2.升级流程


3.自定义密码升级


一、密码加密介绍

2011 年12⽉21 ⽇,有⼈在⽹络上公开了⼀个包含600万个 CSDN ⽤户资料

的数据库,数据全部为明⽂储存,包含⽤户名、密码以及注册邮箱。事件

发⽣后 CSDN 在微博、官⽅⽹站等渠道发出了声明,解释说此数据库系

2009 年备份所⽤,因不明原因泄漏,已经向警⽅报案,后⼜在官⽹发出了公开道歉信。在接下来的⼗多天⾥,⾦⼭、⽹易、 京东、当当、新浪等多家公司被卷⼊到这次事件中。整个事件中最触⽬惊 ⼼的莫过于 CSDN 把⽤户密码明⽂存储,由于很多⽤户是多个⽹站共⽤⼀ 个密码,因此⼀个⽹站密码泄漏就会造成很⼤的安全隐患。由于有了这么

多前⻋之鉴,我们现在做系统时,密码都要加密处理。

在前⾯的案例中,凡是涉及密码的地⽅,我们都采⽤明⽂存储,在实际项⽬中这肯定是不可取的,因为这会带来极⾼的安全⻛险。在企业级应⽤ 中,密码不仅需要加密,还需要加 盐 ,最⼤程度地保证密码安全。

加盐的作用就是为了防止数据库被攻破后看到数据库的加密的密码,但是黑客可以用过常用的一些加密算法进行破解此时这个盐的作用的就出现了,而这个盐有是一个随机生成的字符串,这个盐值是需要进行保存的,在进行显示的时候。而盐值有的保存用户表,这种方式也是可用行的,但是在计算的需要将盐进行散列或者前后进行分散多次加盐以保证密码的安全性,也有的将盐存放到redis里和数据库是分开的。

1.1 常见加密的策略

1.Hash 算法

最早我们使用类似 SHA-256 、SHA-512 、MD5 等这样的单向 Hash 算法。用户注册成功后,保存在数据库中不再是用户的明文密码,而是经过 SHA-256 加密计算的一个字符串,当用户进行登录时,用户输入的明文密码用 SHA-256 进行密码,加密成功之后,再和存储在数据库中的密码进行比对,进而确定用户登录信息是否有效。如果系统遭到攻击,最多也只是存储在数据库中密文被泄露。

这样就绝对安全了吗?由于彩虹表这种攻击方式的存在以及随着计算机硬件的发展,每秒执行数十亿次 HASH 计算已经变得轻轻松松,这意味着即便给密码加密加盐也不再安全。

参考:

彩虹表


彩虹表


彩虹表

2.单向自适应函数


单向自适应函数




这种方式加密是可逆的,但是相同的密码无法得到同一个结果。每次加密都是生成新的结果。比较的时候,会把加密的结果解析成明文比较。验证口令的系统总是可以获取口令的明文的

在 Spring Security 中,我们现在是用一种自适应单向函数 (Adaptive One-way Functions) 来处理密码问题,这种自适应单向函数在密码匹配时,会有意占用大量系统资源 (例如 CPU、内存等),这样可以增加恶意用户攻击系统的难度。在 Spring Security 中,开发者可以通过 bcrypt、PBKDF2、sCrypt 以及 argon2 来体验这种自适应单向函数加密。由于自适应单向函数有意占用大量系统资源,因此每个登录认证请求都会大大降低应用程序的性能,但是 Spring Security 不会采取任何措施来提高密码验证速度,因为它正是通过这种方式来增强系统的安全性。

  • BCryptPasswordEncoder

BCryptPasswordEncoder 使⽤ bcrypt 算法对密码进⾏加密,为了提⾼密码的安全性,bcrypt算法故意降低运⾏速度,以增强密码破解的难度。同时 BCryptP asswordEncoder “为⾃⼰带盐”开发者不需要额外维护⼀个“盐” 字段,使⽤ BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明⽂每次⽣成的加密字符串都不相同。

  • Argon2PasswordEncoder

Argon2PasswordEncoder 使⽤ Argon2 算法对密码进⾏加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题 Argon2也是故意降低运算速度,同时需要⼤量内存,以确保系统的安全性。

  • Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder 使⽤ PBKDF2 算法对密码进⾏加密,和前⾯⼏种类似,PBKDF2 算法也是⼀种故意降低运算速度的算法,当需要 FIPS (Federal Information Processing Standard,美国联邦信息处理标准)认证时, PBKDF2 算法是⼀个很好的选择。

  • SCryptPasswordEncoder

SCryptPasswordEncoder 使⽤scrypt 算法对密码进⾏加密,和前⾯的⼏种类似,serypt 也是⼀种故意降低运算速度的算法,⽽且需要⼤量内存。



所有的算法都是通过增加执行时间来进行安全的功能

二、Security加密结构

2.1 PasswordEncoder

public interface PasswordEncoder {
	String encode(CharSequence rawPassword);
	boolean matches(CharSequence rawPassword, String encodedPassword);
	default boolean upgradeEncoding(String encodedPassword) 
    {
		return false; } 
	
}
  • encode 用来进行明文加密的
  • matches 用来比较密码的方法 (明文密码和数据库密码进行比对的)
  • upgradeEncoding 用来给密码进行升级的方法

SpringSecurity 就是通过这个接口进行完成密码的加密和认证的 。这个父接口,它有这众多子类,可以通过他的子类来定义不同的密码加密策略。

db0adb3c0fd0eb4d68c0c47bf8f2d5ca.png

2.2 密码认证流程

一下是默认的SpringSecurity 的认证流程,没有进行配置改动。

1.首先会走到UserNamePasswordAuthenticationFilter (默认表单认证)在attemptAuthentication 里会调用 AuthenticationManager 的authenticate 方法进行认证。

2.进入到AuthenticationManger的认证方法

3.遍历所有 AuthenticationProvider 判断那个支持


4.最终默认都不支持会调用parent 进行处理 ,这里为什么会parent 不展开说了。

5.调用 AbstractUserDetailsAuthenticationProvider 的认证方法

6.执行  retrieveUser 查询用户是否存在

7.调用 DaoAuthenticationProvider 的 retrieveUser 的查询方法

8.调用密码的认证

9.调用  DaoAuthenticationProvider 的 additionalAuthenticationChecks 密码认证方法

10.调用 DelegatingPasswordEncoder 的  matches 的认证方法

11.最后进行指定的PasswordEncoder 的方法验证

以上是密码认证的流程,和源码。

通过对认证流程源码的分析得知,实际密码比较是由PasswordEncoder完成的,因此只需要使用PasswordEncoder 不同的实现就可以实现不同方式加密

其实为什么默认是DeletatingPasswordEncoder 不是具体的呢?

2.3 DeletaingPasswordEncoder

1.为什么默认一个代理类不是具体的呢?

passwordEncorder 在SpringSecurity 默认实现并不是使用具体的某个实现类而是DelegatingPasswordEnocoder ,它是一个代理类,它并不是具体的实现,它是通过这个类将传来的id(也就是{noop})去拿到一种具体的实现,这样的好处是向上兼容 也能向下兼容,不管使用那种加密方式可以处理

spring Security 把所有的加密方式放到一个map中根据id去比较找对应的加密类,根据{}里的值去比较。

不固定数据的密码的加密就可以在数据存储时携带{noop}类型的数据就可以了,他会被Delegating进行选择处理

默认使用DelegatingPasswordEnocoder 进行密码加密处理,比较灵活只要给定指定的前缀就可以处理,还可以固定死,就直接替换掉 DelegatingPasswordEnocoder 为指定的加密方式类就可以了

  • 兼容性:使用 DelegatingPasswordEncoder 可以帮助需要使用旧密码加密方式的系统顺利迁移到 Spring Security 中, 它允许在同一个系统中同时存储不同的密码加密方案。
  • 便捷性:密码存储的最佳方案不可能一直不变,如果使用 DelegatingPasswordEncoder作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小一部分代码就可以实现。

2. DeletaingPasswordEncoder 的装配原理

它是通过 PasswordEncoderFactories 创建出来的 。那么PasswordEncoderFactories 是个什么呢?

他里面的map保存这所有的加密方式 key 就是 id ,取出的时候是 {noop} 格式的

public class PasswordEncoderFactories {

	/**
	 * Creates a {@link DelegatingPasswordEncoder} with default mappings. Additional
	 * mappings may be added and the encoding will be updated to conform with best
	 * practices. However, due to the nature of {@link DelegatingPasswordEncoder} the
	 * updates should not impact users. The mappings current are:
	 *
	 * <ul>
	 * <li>bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)</li>
	 * <li>ldap - {@link org.springframework.security.crypto.password.LdapShaPasswordEncoder}</li>
	 * <li>MD4 - {@link org.springframework.security.crypto.password.Md4PasswordEncoder}</li>
	 * <li>MD5 - {@code new MessageDigestPasswordEncoder("MD5")}</li>
	 * <li>noop - {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}</li>
	 * <li>pbkdf2 - {@link Pbkdf2PasswordEncoder}</li>
	 * <li>scrypt - {@link SCryptPasswordEncoder}</li>
	 * <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li>
	 * <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li>
	 * <li>sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}</li>
	 * <li>argon2 - {@link Argon2PasswordEncoder}</li>
	 * </ul>
	 *
	 * @return the {@link PasswordEncoder} to use
	 */
	@SuppressWarnings("deprecation")
	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);
	}

	private PasswordEncoderFactories() {}
}

DeletaingPasswordEncoder 是通过 WebSecurityConfigurerAdapter 里进行自动装配的,

首先先回去容器里拿,没有就会创建

创建完成后会将各个UserDetailsService的主要父类的 PasswordEncoder赋值。


        这是从容器去找是否有 ,如果容器没有就会通过工厂类进行创建。

PasswordEncoderFactories.createDelegatingPasswordEncoder();

3.如何自定义PasswordEncoder

通过源码分析得知如果在⼯⼚中指定了PasswordEncoder,就会使⽤指 PasswordEncoder,否则就会使⽤默认DelegatingPasswordEncoder。

通过以上源码得出,如果想使用指定的PasswordEncoder 直接在容器中创建一个Bean 即可。

    /**
     * 设置指定密码类
     */
    @Bean
    public PasswordEncoder passwordEncoder (){
        return new BCryptPasswordEncoder();
    }

总结 :如果使用默认的密码加密方式 就需要使用 在数据库的密码 前 {id} 方式来指定加密方式,如果是指定了PasswordEncoder 则不需要

  • 1.默认方式的 DeletatingPasswordEncoder :{noop}123
  • 2.指定加密方式 :$2a$10$RqdGd8FSj9/VrmVi1Me2xOHKkXdgKk9gh6R2LYOdpgFUZIrxflu2C

2.3 密码升级

1.原理

推荐使⽤DelegatingPasswordEncoder 的另外⼀个好处就是⾃动进⾏密码加密⽅案的升级,这个功能在整合⼀些⽼的系统时⾮常有⽤。

密码升级推荐使用DelegatingPasswordEncoder 是因为,密码升级会根据DeletatingPasswordEncoder 的默认的 PasswordEncoder 进行升级 随着版本的升级,他默认的PasswordEncoder 也会升级,所以不需要改动源码,如果使用是指定的PasswordEncoder 密码升级的话只会升级成当前PasswordEcoder 要想修改的话则需要修改代码来修改PasswordEncoder 。


要想更新密码的加密策略就需要在认证成功之后

密码自动升级是基于ProviderManager 的 authenticate 的AbstractUserDetailsAuthenticationProvider 的 authenticate 的 这个认证方法里调用了子类 AbstractUserDetailsAuthenticationProvider 的 additionalAuthenticationChecks 密码的认证 ,认证完成之后最后调用DaoAuthenticationProvider 的 createSuccessAuthentication 的 在进行判断是否需要进行密码升级,如果需要则完成升级。

层级调用关系

  • AuthenticationProvider 默认的实现类是
    • AbstractUserDetailsAuthenticationProvider 的实现类
      • DaoAuthenticationProvider

通过对认证流程源码的分析得知,实际密码比较是由PasswordEncoder完成的,因此只需要使用PasswordEncoder 不同的实现就可以实现不同方式加密

#  Spring Security 默认的密码升级流程

1. 用户输入的账号密码认证成功之后进行

2. 在ProviderManager 的实现类中的认证方法中最后调用 createSuccessAuthentication 创建认证用户结果的时候进行密码的升级

3. 默认是基于内存升级 默认基于内存的话,回去先判断它是否是当前DelegatingPasswordEncoder的加密方式的,如果不是则会基于内存去修改。

从以上源码得知 要进行密码升级 当前用户信息是已经完成认证通过的,需要根据用户名称去修改密码就可以了。


# 自定义密码升级步骤

– 1.创建基于数据库的 UserDetatilsService 和 UserDetailsPasswordService 的实现类 (可以合并为一个)

– 2.替换成新建UserDetailsService。

– 3.实现updatePassword 方法,在里面根据用户名修改密码就可以了

2.升级流程

1.认证完成之后会调用这个方法进行检测是否需要升级代码和封装用户token

2.判断是否需要升级,如果需要则进行密码升级

3.是否升级的判断,首先会先拿着当前的密码去选择出加密方式,在进行判断是否和当前最新的密码方式是否不相同。


4.如果返回值为true则进行密码加密的修改

1.获取当前密码

2.用当前最新的加密方式进行加密处理

3.调用 userDetailsPasswordService.  updatePassword 更新密码。

4.最终返回更新后的用户信息。

以上就是密码自动升级的流程,核心就是通过UserDetailsPasswordService 去更新,所以如果需要自定义的话,那么就去实现UserDetailsPassword就可以了。

3.自定义密码升级

步骤:

1.创建UserDetailsPasswordService 的自定义实现

2.将默认的UserDetailsPasswordService替换

代码:

public interface MyUserDetailsPasswordService extends UserDetailsPasswordService , UserDetailsService {
    /**
     * 查询用户及其用户角色信息
     */
    @Override
    UserDetails loadUserByUsername(String userName);

    /**
     * 更新密码
     */
    @Override
    UserDetails updatePassword(UserDetails user, String newPassword);

}
/* 
* @Description TODO  更新用户密码和查询用户信息
 */
@Service
public class UserDetailsPasswordService implements MyUserDetailsPasswordService {

    @Autowired
    private UserDAO userDAO ;
    /**
     *  更新用户密码
     * @param user 要更新的用户信息
     * @param newPassword 当前系统加密方式最新的加密后的密码
     * @return 更新密码后的用户信息
     */
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        if (user!=null){
            int result = userDAO.updatePassword(user.getUsername(), newPassword);
            if (result==1){
                MyUserDetails  myUserDetails = new MyUserDetails();
                BeanUtils.copyProperties(user,myUserDetails);
                myUserDetails.setPassword(newPassword);
                return myUserDetails ;
            }
        }
        return user;
    }

    /**
     * 查询用户信息
     * @param username 用户名称
     * @return 结果
     * @throws UsernameNotFoundException
     */
    @Override
    public MyUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userDAO.loadUserByUsername(username);
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!--指定约束文件-->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- mapper 是当前文件的根标签 , 必须的。namespace :叫做命名空间,唯一值。要求你使用dao接口的全限定名称。-->
<mapper namespace="com.bjpowernode.dao.UserDAO">

    <resultMap id="UserTDO" type="com.bjpowernode.entity.MyUserDetails">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="enabled" column="enabled"/>
        <result property="accountNonExpired" column="accountNonExpired"/>
        <result property="accountNonLocked" column="accountNonLocked"/>
        <result property="credentialsNonExpired" column="credentialsNonExpired"/>

        <collection property="roles" javaType="java.util.List" ofType="com.bjpowernode.entity.Role">
            <result property="id" column="roleId"/>
            <result property="name" column="name"/>
            <result property="nameZh" column="name_zh"/>
        </collection>
    </resultMap>
    <!--密码更新-->
    <update id="updatePassword">
        update user set password = #{newPassword} where username = #{username}
    </update>

    <!--根据用户查询-->
    <select id="loadUserByUsername" resultType="com.bjpowernode.entity.MyUserDetails">
        SELECT
            u.id,
            u.username,
            u.PASSWORD,
            u.enabled,
            u.accountNonExpired,
            u.accountNonLocked,
            u.credentialsNonExpired,
            r.id AS roleId,
            r.NAME,
            r.name_zh
        FROM
            USER AS u
                LEFT JOIN user_role AS rel ON rel.uid = u.id
                LEFT JOIN role AS r ON r.id = rel.rid
        WHERE
            u.username = #{userName}
    </select>
</mapper>
@Mapper
public interface UserDAO {
    /**
     * 查询用户及其用户角色信息
     */
    MyUserDetails loadUserByUsername(@Param("userName") String userName);

    /**
     * 更新密码
     */
    int updatePassword(@Param("username")String username ,@Param("newPassword") String newPassword);
}

@Configuration
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {


    private final MyUserDetailsPasswordService myUserDetailsPasswordService ;
    @Autowired
    public SecurityWebConfig(MyUserDetailsPasswordService myUserDetailsPasswordService) {
        this.myUserDetailsPasswordService = myUserDetailsPasswordService;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 这是将UserDetailsService 和 UserDetailsPasswordService 写在一些的配置写法
        auth.userDetailsService(myUserDetailsPasswordService);
        // 是否两个接口分开的写法
//        auth.userDetailsService(userDetailsService()).userDetailsPasswordManager(new UserDetailsPasswordService());

    }
}

以上就是SpringSecurity 的密码加密全部内容!



版权声明:本文为weixin_52834606原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。