• SpringSecurity - 密码加密


    1. PasswordEncoder 接口

    SpringSecurity中通过PasswordEncoder接口定义了密码加密和比对的相关操作:

    public interface PasswordEncoder {
        // 对明文密码进行加密
        String encode(CharSequence var1);
        // 进行密码比对
        boolean matches(CharSequence var1, String var2);
        // 判断当前密码是否需要升级,默认返回false,表示不需要升级
        default boolean upgradeEncoding(String encodedPassword) {
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    针对密码的所有操作,PasswordEncoder都定义好了,不同的实现类将采用不同的密码加密方案对密码进行处理。

    使用自适应单向函数处理密码问题的常见实现类:

    BCryptPasswordEncoder

    Argon2PasswordEncoder

    SCryptPasswordEncoder

    Pbkdf2PasswordEncoder

    2. DelegatingPasswordEncoder

    SpringSecurity中默认的密码加密方式为DelegatingPasswordEncoder,主要用于代理上面介绍的不同的密码加密方案。

    先来看下PasswordEncoderFactories类,因为正是由它里面的静态方法createDelegatingPasswordEncoder()提供了默认的DelegatingPasswordEncoder实例:

    public class PasswordEncoderFactories {
        public static PasswordEncoder createDelegatingPasswordEncoder() {
            String encodingId = "bcrypt";
            Map<String, PasswordEncoder> encoders = new HashMap();
            encoders.put(encodingId, new BCryptPasswordEncoder());
            encoders.put("ldap", new LdapShaPasswordEncoder());
            encoders.put("MD4", new Md4PasswordEncoder());
            encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
            encoders.put("noop", NoOpPasswordEncoder.getInstance());
            encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
            encoders.put("scrypt", new SCryptPasswordEncoder());
            encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
            encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
            encoders.put("sha256", new StandardPasswordEncoder());
            encoders.put("argon2", new Argon2PasswordEncoder());
            return new DelegatingPasswordEncoder(encodingId, encoders);
        }
        private PasswordEncoderFactories() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    createDelegatingPasswordEncoder() 方法中,首先定义了encoders变量,encoders中存储了每一种密码加密方案的id和所对应的加密类,例如MD4对应Md4PasswordEncoder,noop对应NoOpPasswordEncoder,其中encodingId的默认值为bcrypt,相当于默认使用的加密方案是BCryptPasswordEncoder。

    再来看下DelegatingPasswordEncoder类的源码,由于源码较长,先来看下属性和构造方法:

    public class DelegatingPasswordEncoder implements PasswordEncoder {
        
    }
    
    • 1
    • 2
    • 3
    // 定义前缀PREFIX和后缀SUFFIX用来包裹将来生成的加密方案的id
    private static final String PREFIX = "{";
    private static final String SUFFIX = "}";
    // 默认的加密方案id
    private final String idForEncode;
    // 表示默认的加密方案(BCryptPasswordEncoder)
    // 它的值是根据idForEncode从idToPasswordEncoder集合中提取出来的
    private final PasswordEncoder passwordEncoderForEncode;
    // 用来保存id和家吗方案之间的映射
    private final Map<String, PasswordEncoder> idToPasswordEncoder;
    // 默认的密码比对器,当根据密码加密方案的id无法找到对应的加密方案时,就会使用默认的密码比对器
    // defaultPasswordEncoderForMatches的默认类型是UnmappedIdPasswordEncoder
    // 在UnmappedIdPasswordEncoder的matches方法中不会做任何密码比对操作,直接抛出异常
    private PasswordEncoder defaultPasswordEncoderForMatches = new DelegatingPasswordEncoder.UnmappedIdPasswordEncoder();
    
    public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
        if (idForEncode == null) {
            throw new IllegalArgumentException("idForEncode cannot be null");
        } else if (!idToPasswordEncoder.containsKey(idForEncode)) {
            throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
        } else {
            Iterator var3 = idToPasswordEncoder.keySet().iterator();
            while(var3.hasNext()) {
                String id = (String)var3.next();
                if (id != null) {
                    if (id.contains("{")) {
                        throw new IllegalArgumentException("id " + id + " cannot contain " + "{");
                    }
                    if (id.contains("}")) {
                        throw new IllegalArgumentException("id " + id + " cannot contain " + "}");
                    }
                }
            }
            this.idForEncode = idForEncode;
            // 根据idForEncode获取passwordEncoderForEncode
            this.passwordEncoderForEncode = (PasswordEncoder)idToPasswordEncoder.get(idForEncode);
            this.idToPasswordEncoder = new HashMap(idToPasswordEncoder);
        }
    }
    
    public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {
        if (defaultPasswordEncoderForMatches == null) {
            throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
        } else {
            this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
        }
    }
    
    private class UnmappedIdPasswordEncoder implements PasswordEncoder {
        private UnmappedIdPasswordEncoder() {
        }
    
        public String encode(CharSequence rawPassword) {
            throw new UnsupportedOperationException("encode is not supported");
        }
    
        public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
            String id = DelegatingPasswordEncoder.this.extractId(prefixEncodedPassword);
            throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    encode()方法实现:

    // 具体的加密工作仍然由加密类来完成
    // 只不过在密码加密完成后,给加密后的字符串加上一个前缀{id},用来描述所采用的具体加密方案。
    public String encode(CharSequence rawPassword) {
        return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    不同的前缀代表了后面的字符串采用了不同的加密方案。

    matches()方法实现:

    public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
        if (rawPassword == null && prefixEncodedPassword == null) {
            return true;
        } else {
            // 从加密字符串中提取出具体的加密方案id,也就是{}中的字符
            String id = this.extractId(prefixEncodedPassword);
            // 到集合中获取具体的加密方案
            PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
            // 如果获取到的为null,说明不存在对应的加密实例
            // 那么就采用默认的密码匹配器defaultPasswordEncoderForMatches
            if (delegate == null) {
                return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
            } else {
                // 如果获取到了对应的加密实例,则调用matches()方法完成密码校验
                String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
                return delegate.matches(rawPassword, encodedPassword);
            }
        }
    }
    
    // 从加密字符串中提取出具体的加密方案id,也就是{}中的字符,具体的提取方式就是字符串截取
    private String extractId(String prefixEncodedPassword) {
        if (prefixEncodedPassword == null) {
            return null;
        } else {
            int start = prefixEncodedPassword.indexOf("{");
            if (start != 0) {
                return null;
            } else {
                int end = prefixEncodedPassword.indexOf("}", start);
                return end < 0 ? null : prefixEncodedPassword.substring(start + 1, end);
            }
        }
    }
    
    private String extractEncodedPassword(String prefixEncodedPassword) {
        int start = prefixEncodedPassword.indexOf("}");
        return prefixEncodedPassword.substring(start + 1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    upgradeEncoding() 方法实现:

    public boolean upgradeEncoding(String prefixEncodedPassword) {
        String id = this.extractId(prefixEncodedPassword);
        // 如果当前加密字符串所采用的加密方案不是默认的加密方案(BCryptPasswordEncoder)
        // 就会自动进行密码升级
        if (!this.idForEncode.equalsIgnoreCase(id)) {
            return true;
        } else {
            // 调用默认加密方案的upgradeEncoding()方法怕暖密码是否需要升级
            String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
            return ((PasswordEncoder)this.idToPasswordEncoder.get(id)).upgradeEncoding(encodedPassword);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    3. 实战

    ① 创建一个SpringBoot工程,并引入SpringSecurity依赖,创建一个测试接口:

    @RestController
    public class UserResource {
        @RequestMapping("/hello")
        public String test(){
            return "hello";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ② 在单元测试中生成一段加密字符串:

    @SpringBootTest
    public class UuaApplicationTest {
        @Test
        public void test(){
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            // $2a$10$.758pfnk63n7fUNPLovJS.ydgs0gpSqH0nFrlxVTj/vLozzIk01NS
            System.out.println(encoder.encode("123"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ③ 自定义SecurityConfig类:

    @EnableWebSecurity(debug = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        // 表单登录
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable();
        }
    
        // 自定义用户认证
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("zhangsan")
                    .password("$2a$10$.758pfnk63n7fUNPLovJS.ydgs0gpSqH0nFrlxVTj/vLozzIk01NS")
                    .roles("admin");
        }
    
        // 将BCryptPasswordEncoder示例注册到Spring容器中,这将代替默认的DelegatingPasswordEncoder
        @Bean
        PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    配置完成后启动项目就可以使用zhangsan/123进行登录了。

    由于默认使用的是DelegatingPasswordEncoder,所以页可以不配置PasswordEncode实例,只需要在密码前加上前缀{bcrypt}:

    @EnableWebSecurity(debug = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("zhangsan")
                    .password("{bcrypt}$2a$10$.758pfnk63n7fUNPLovJS.ydgs0gpSqH0nFrlxVTj/vLozzIk01NS")
                    .roles("admin");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    4. 加密方案自动升级

    使用DelegatingPasswordEncoder的另一个好处是会自动进行密码加密升级。

    ① 创建一个数据库test,向数据库中添加一个user表,并添加一条数据,在用户数据中,用户密码是{noop}123:

    create table user(
    	id int(11) not null auto_increment,
        username varchar(32) default null,
        password varchar(255) default null,
        primary key(id)
    )engine=innodb default charset=utf8
    
    INSERT INTO `test`.`user` (`id`, `username`, `password`) VALUES (1, 'zhangsan', '{noop}123');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ② 在项目中引入MyBatis和mysql依赖:

    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.1.3version>
    dependency>
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ③ 在 application.properties 中添加数据库的连接信息:

    spring:
      datasource:
        username: root
        password: root
        url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ④ 创建 User 实体类:

    @Data
    public class User implements UserDetails {
        private Integer id;
        private String username;
        private String password;
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return null;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    ⑤ 创建MyUserDetailsService类:

    @Service
    public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
    
        @Autowired
        private UserMapper userMapper;
    
        // 实现了UserDetailsPasswordService接口中的updatePassword()方法
        // 当系统判断密码加密方案需要升级的时候,就会自动调用updatePassword中的方法去修改数据库中的密码
        // 当数据库中的密码修改成功后,修改User对象的password属性,并将User对象返回
        @Override
        public UserDetails updatePassword(UserDetails user, String newPassword) {
            Integer result = userMapper.updatePassword(user.getUsername(),newPassword);
            if(result==1){
                ((User) user).setPassword(newPassword);
            }
            return user;
        }
    
        // 实现了UserDetailsService接口中的loadUserByUsername()方法
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userMapper.loadUserByUsername(username);
            if(Objects.isNull(user)){
                throw new UsernameNotFoundException("用户不存在");
            }
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    @Mapper
    public interface UserMapper {
    
        User loadUserByUsername(String username);
        Integer updatePassword(@Param("username") String username,
                               @Param("newPassword") String newPassword);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    
    DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.imooc.uua.dao.UserMapper">
        <update id="updatePassword">
            update user set password=#{newPassword} where username=#{username}
        update>
        <select id="loadUserByUsername" resultType="com.imooc.uua.entity.User">
            select * from user where username=#{username};
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ⑥ 配置SecurityConfig:

    @EnableWebSecurity(debug = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        MyUserDetailsService userDetailsService;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    启动项目登录成功后密码就变成了{bcrypt}$2a$10$VJeTeGpn8PhoQ6S3i4nmcuT3ho6QJpmVbtR/PFa4ffag.Dx0RFLVe

    如果使用了DelegatingPasswordEncoder,只要数据库中存储的加密方案不是DelegatingPasswordEncoder中默认的BCryptPasswordEncoder,在登录成功后,都会自动升级为BCryptPasswordEncoder加密。这就是加密方案的升级。

  • 相关阅读:
    IDEA创建Mybatis项目
    累计-同比
    JavaScript基础 JavaScript第一天 2. 变量
    【Linux初阶】Linux小程序 - 进度条
    C/C++、C#、F#、Go AMD x86-64 编译器内部实现乘法运算
    航天与航空的区别,今天终于弄清楚啦!
    Kafka学习
    2022 年 9 月青少年软编等考 C 语言一级真题解析
    Chinese-LLaMA-AIpaca 指令精调
    AcWing 668. 游戏时间2
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/126516806