SpringSecurity 用户密码加密方式升级的源码分析

        用户密码加密方式升级这种需求,一般在老项目翻版重做或者是迁移了其他第三方的用户时可能会用到。

        以老项目密码采用MD5,需要转换为SM3加密方式为例,实现的效果是用户在登录时,如果加密方式是MD5而不是SM3,且密码对比成功就升级为SM3。

        SpringSecurity 的认证部分是通过 AuthenticationManager 实现的,不考虑我们自定义直接实现AuthenticationManager 进行认证,那么在框架中实际的认证工作就是AuthenticationProvider 的实现类来完成的。(这里没看过源码的朋友可能不理解,但是并不影响我们通过源码学习加密方式升级的思路)

1、DaoAuthenticationProvider

我们首先来看下 DaoAuthenticationProvider 的源码,它包含以下几个主要方法

        方法 retrieveUser() 的作用是根据用户输入的账号或其他唯一可以标识身份的信息从数据库中查询出整个用户对象的信息,一般通过userDetailService实现,这里不是我们关注的重点。

        在获取到数据库中的用户信息后,接下来就是使用用户输入的密码与查询到的密码做对比了。通过源码可以发现对比的工作是通过 passwordEncoder.matches()方法实现的。我们知道从数据库中查询到的密码的加密方式有两种可能,MD5和SM3,显然matches()方法必须同时满足这两种加密方式的校验。

   @Override
   protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            //前台传的密码进行加密和数据库存的密码做对比
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

        我们可以通过setPasswordEncoder(PasswordEncoder passwordEncoder)方法来设置需要的子类实现,但是子类实现必须同时满足这MD5和SM3两种方式的校验。

         实际上,security框架在DaoAuthenticationProvider构造器中已经默认为我们设置了一个PasswordEncoder对象,而这个对象就满足多种加密方式的校验。

public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

 2、PasswordEncoderFactories

        通过PasswordEncoderFactories的实现我们可以看到,它是以bcrypt作为默认的加密方式,然后将其他加密方式的实现放到了一个map中,尽管这并不满足我们默认SM3 的需求,但从这里基本可以推测出,最终肯定是从DelegatingPasswordEncoder对象管理的众多加密实现中选择和数据库加密方式匹配的进行验证了。如果我们能将默认的bcrypt修改为SM3,也就完成了新注册账号的采用SM3加密的工作。

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());

        return new DelegatingPasswordEncoder(encodingId, encoders);
    }

 3、DelegatingPasswordEncoder

        我们回到matches()方法,去看DelegatingPasswordEncoder对象的实现,第一个参数是明文密码,第二个参数是数据库中查询到的密码,可以看到这段代码先是从数据库中查询到的密码中提取了一个id,然后有使用这个id从DelegatingPasswordEncoder对象的管理的map中提取了对应的加密实现,用这个具体的实现去做密码匹配校验,这是典型的代理模式应用。

        不难知道,这个id就是map的key值。跟踪extractId()的实现就可以知道数据库中的密码前面应该拼接了一个{MD5}或{SM3},这就需要我们迁移原始数据时做一下加密标识 。eg:{MD5}e10adc3949ba59abbe56e057f20f883e

@Override
    public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
        if (rawPassword == null && prefixEncodedPassword == null) {
            return true;
        }
        //从数据库中查询到的密码中提取id
        String id = extractId(prefixEncodedPassword);
        //N. 这里获取真正用来验证密码的加密实现,注意是根据前缀从列表中获取的,不是默认方式
        PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
        if (delegate == null) {
            return this.defaultPasswordEncoderForMatches
                    .matches(rawPassword, prefixEncodedPassword);
        }
        String encodedPassword = extractEncodedPassword(prefixEncodedPassword);//去掉前缀
        return delegate.matches(rawPassword, encodedPassword);
    }

        通过分析上面的代码,我们只需要自己new一个DelegatingPasswordEncoder对象,按照需求自定义 new DelegatingPasswordEncoder(encodingId, encoders)的两个参数,也就是设置默认的id和加密方式是SM3,在encoders中加入SM3加密方式实现。(这里需要我们新增PasswordEncoder接口的SM3加密实现)

        在初始化DaoAuthenticationProvider对象时,需要通过setPasswordEncoder()设置自定义的DelegatingPasswordEncoder对象。到这里我们已经可以实现对数据库中密码存在多种加密方式的兼容,接下来回到DaoAuthenticationProvider中。

4、DaoAuthenticationProvider

        分析下面的代码,只要 upgradeEncoding 为 true,就会触发密码加密方式的升级。这里的encode()方法返回值newPassword就是使用的默认加密方式SM3的加密结果。

@Override
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        //令upgradeEncoding 为true就可以直接更新密码了
        boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            //客户端传的密码-(这里是明文)
            String presentedPassword = authentication.getCredentials().toString();
            //加密的密码
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            //更新密码
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }

        return super.createSuccessAuthentication(principal, authentication, user);
    }

        最后我们只要实现UserDetailsPasswordService接口,逻辑就是操作数据库修改密码。同时不用忘记在初始化 DaoAuthenticationProvider对象时,调用setUserDetailsPasswordService()方法。

public interface UserDetailsPasswordService {

	UserDetails updatePassword(UserDetails user, String newPassword);
}

        最后总结一下密码升级的思路,最核心的就是在数据库密码的前面加上一个前缀,例如{MD5},{SM3}...,做密码校验时,先从数据库中查询出密码,根据密码的前缀对用户输入的密码采用对应的加密方式进行加密,然后对比两个密码是否相同。如果相同,且前缀又不是{SM3},就把用户输入的密码再用SM3再加密一次,替换掉数据库里的密码字段。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值