前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Security 多种加密方案共存,老破旧系统整合利器!

Spring Security 多种加密方案共存,老破旧系统整合利器!

作者头像
江南一点雨
发布2020-06-24 16:28:06
1.2K0
发布2020-06-24 16:28:06
举报
文章被收录于专栏:玩转JavaEE玩转JavaEE

松哥给最近连载的 Spring Security 系列也录制了视频教程,感兴趣的小伙伴请戳这里->Spring Boot+Vue+微人事视频教程(Spring Boot 第十章就是 Spring Security)。

关于密码加密的问题,松哥之前已经和大家聊过了,参考:

这篇文章中,松哥给大家介绍了两种密码加密方案,但是两种都是独立使用的!能不能在同一个项目中同时存在多种密码加密方案呢?答案是肯定的!

今天松哥就来和大家聊一聊,如何在 Spring Security 中,让多种不同的密码加密方案并存。

为什么要加密?常见的加密算法等等这些问题我就不再赘述了,大家可以参考之前的:Spring Boot 中密码加密的两种姿势!,咱们直接来看今天的正文。

1.PasswordEncoder

在 Spring Security 中,跟密码加密/校验相关的事情,都是由 PasswordEncoder 来主导的,PasswordEncoder 拥有众多的实现类:

这些实现类,有的已经过期了,有的用处不大。对于我们而言,最常用的莫过于 BCryptPasswordEncoder。

PasswordEncoder 本身是一个接口,里边只有三个方法:

代码语言:javascript
复制
public interface PasswordEncoder {
 String encode(CharSequence rawPassword);
 boolean matches(CharSequence rawPassword, String encodedPassword);
 default boolean upgradeEncoding(String encodedPassword) {
  return false;
 }
}
  • encode 方法用来对密码进行加密。
  • matches 方法用来对密码进行比对。
  • upgradeEncoding 表示是否需要对密码进行再次加密以使得密码更加安全,默认为 false。

PasswordEncoder 的实现类,则具体实现了这些方法。

2.PasswordEncoder 在哪里起作用

对于我们开发者而言,我们通常都是在 SecurityConfig 中配置一个 PasswordEncoder 的实例,类似下面这样:

代码语言:javascript
复制
@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

剩下的事情,都是由系统调用的。今天我们就来揭开系统调用的神秘面纱!我们一起来看下系统到底是怎么调用的!

首先,松哥在前面的文章中和大家提到过,Spring Security 中,如果使用用户名/密码的方式登录,密码是在 DaoAuthenticationProvider 中进行校验的,大家可以参考:SpringSecurity 自定义认证逻辑的两种方式(高级玩法)

我们来看下 DaoAuthenticationProvider 中密码是如何校验的:

代码语言:javascript
复制
protected void additionalAuthenticationChecks(UserDetails userDetails,
  UsernamePasswordAuthenticationToken authentication)
  throws AuthenticationException {
 if (authentication.getCredentials() == null) {
  throw new BadCredentialsException(messages.getMessage(
    "AbstractUserDetailsAuthenticationProvider.badCredentials",
    "Bad credentials"));
 }
 String presentedPassword = authentication.getCredentials().toString();
 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
  throw new BadCredentialsException(messages.getMessage(
    "AbstractUserDetailsAuthenticationProvider.badCredentials",
    "Bad credentials"));
 }
}

可以看到,密码校验就是通过 passwordEncoder.matches 方法来完成的。

那么 DaoAuthenticationProvider 中的 passwordEncoder 从何而来呢?是不是就是我们一开始在 SecurityConfig 中配置的那个 Bean 呢?

我们来看下 DaoAuthenticationProvider 中关于 passwordEncoder 的定义,如下:

代码语言:javascript
复制
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
 private PasswordEncoder passwordEncoder;
 public DaoAuthenticationProvider() {
  setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
 }
 public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
  this.passwordEncoder = passwordEncoder;
  this.userNotFoundEncodedPassword = null;
 }

 protected PasswordEncoder getPasswordEncoder() {
  return passwordEncoder;
 }
}

从这段代码中可以看到,在 DaoAuthenticationProvider 创建之时,就指定了 PasswordEncoder,似乎并没有用到我们一开始配置的 Bean?其实不是的!在 DaoAuthenticationProvider 创建之时,会制定一个默认的 PasswordEncoder,如果我们没有配置任何 PasswordEncoder,将使用这个默认的 PasswordEncoder,如果我们自定义了 PasswordEncoder 实例,那么会使用我们自定义的 PasswordEncoder 实例!

从何而知呢?

我们再来看看 DaoAuthenticationProvider 是怎么初始化的。

DaoAuthenticationProvider 的初始化是在 InitializeUserDetailsManagerConfigurer#configure 方法中完成的,我们一起来看下该方法的定义:

代码语言:javascript
复制
public void configure(AuthenticationManagerBuilder auth) throws Exception {
 if (auth.isConfigured()) {
  return;
 }
 UserDetailsService userDetailsService = getBeanOrNull(
   UserDetailsService.class);
 if (userDetailsService == null) {
  return;
 }
 PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
 UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
 DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
 provider.setUserDetailsService(userDetailsService);
 if (passwordEncoder != null) {
  provider.setPasswordEncoder(passwordEncoder);
 }
 if (passwordManager != null) {
  provider.setUserDetailsPasswordService(passwordManager);
 }
 provider.afterPropertiesSet();
 auth.authenticationProvider(provider);
}

从这段代码中我们可以看到:

  1. 首先去调用 getBeanOrNull 方法获取一个 PasswordEncoder 实例,getBeanOrNull 方法实际上就是去 Spring 容器中查找对象。
  2. 接下来直接 new 一个 DaoAuthenticationProvider 对象,大家知道,在 new 的过程中,DaoAuthenticationProvider 中默认的 PasswordEncoder 已经被创建出来了。
  3. 如果一开始从 Spring 容器中获取到了 PasswordEncoder 实例,则将之赋值给 DaoAuthenticationProvider 实例,否则就是用 DaoAuthenticationProvider 自己默认创建的 PasswordEncoder。

至此,就真相大白了,我们配置的 PasswordEncoder 实例确实用上了。

3.默认的是什么?

同时大家看到,如果我们不进行任何配置,默认的 PasswordEncoder 也会被提供,那么默认的 PasswordEncoder 是什么呢?我们就从这个方法看起:

代码语言:javascript
复制
public DaoAuthenticationProvider() {
 setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}

继续:

代码语言:javascript
复制
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 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() {}
}

可以看到:

  1. 在 PasswordEncoderFactories 中,首先构建了一个 encoders,然后给所有的编码方式都取了一个名字,再把名字做 key,编码方式做 value,统统存入 encoders 中。
  2. 最后返回了一个 DelegatingPasswordEncoder 实例,同时传入默认的 encodingId 就是 bcrypt,以及 encoders 实例,DelegatingPasswordEncoder 看名字应该是一个代理对象。

我们来看下 DelegatingPasswordEncoder 的定义:

代码语言:javascript
复制
public class DelegatingPasswordEncoder implements PasswordEncoder {
 private static final String PREFIX = "{";
 private static final String SUFFIX = "}";
 private final String idForEncode;
 private final PasswordEncoder passwordEncoderForEncode;
 private final Map<String, PasswordEncoder> idToPasswordEncoder;
 private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
 public DelegatingPasswordEncoder(String idForEncode,
  Map<String, PasswordEncoder> idToPasswordEncoder) {
  if (idForEncode == null) {
   throw new IllegalArgumentException("idForEncode cannot be null");
  }
  if (!idToPasswordEncoder.containsKey(idForEncode)) {
   throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
  }
  for (String id : idToPasswordEncoder.keySet()) {
   if (id == null) {
    continue;
   }
   if (id.contains(PREFIX)) {
    throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
   }
   if (id.contains(SUFFIX)) {
    throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
   }
  }
  this.idForEncode = idForEncode;
  this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
  this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
 }
 public void setDefaultPasswordEncoderForMatches(
  PasswordEncoder defaultPasswordEncoderForMatches) {
  if (defaultPasswordEncoderForMatches == null) {
   throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
  }
  this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
 }

 @Override
 public String encode(CharSequence rawPassword) {
  return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
 }

 @Override
 public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
  if (rawPassword == null && prefixEncodedPassword == null) {
   return true;
  }
  String id = extractId(prefixEncodedPassword);
  PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
  if (delegate == null) {
   return this.defaultPasswordEncoderForMatches
    .matches(rawPassword, prefixEncodedPassword);
  }
  String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
  return delegate.matches(rawPassword, encodedPassword);
 }

 private String extractId(String prefixEncodedPassword) {
  if (prefixEncodedPassword == null) {
   return null;
  }
  int start = prefixEncodedPassword.indexOf(PREFIX);
  if (start != 0) {
   return null;
  }
  int end = prefixEncodedPassword.indexOf(SUFFIX, start);
  if (end < 0) {
   return null;
  }
  return prefixEncodedPassword.substring(start + 1, end);
 }

 @Override
 public boolean upgradeEncoding(String prefixEncodedPassword) {
  String id = extractId(prefixEncodedPassword);
  if (!this.idForEncode.equalsIgnoreCase(id)) {
   return true;
  }
  else {
   String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
   return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
  }
 }

 private String extractEncodedPassword(String prefixEncodedPassword) {
  int start = prefixEncodedPassword.indexOf(SUFFIX);
  return prefixEncodedPassword.substring(start + 1);
 }
 private class UnmappedIdPasswordEncoder implements PasswordEncoder {

  @Override
  public String encode(CharSequence rawPassword) {
   throw new UnsupportedOperationException("encode is not supported");
  }

  @Override
  public boolean matches(CharSequence rawPassword,
   String prefixEncodedPassword) {
   String id = extractId(prefixEncodedPassword);
   throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
  }
 }
}

这段代码比较长,我来和大家挨个解释下:

  1. DelegatingPasswordEncoder 也是实现了 PasswordEncoder 接口,所以它里边的核心方法也是两个:encode 方法用来对密码进行编码,matches 方法用来校验密码。
  2. 在 DelegatingPasswordEncoder 的构造方法中,通过 通过传入的两个参数 encodingId 和 encoders ,获取到默认的编码器赋值给 passwordEncoderForEncode,默认的编码器实际上就是 BCryptPasswordEncoder。
  3. 在 encode 方法中对密码进行编码,但是编码的方式加了前缀,前缀是 {编码器名称} ,例如如果你使用 BCryptPasswordEncoder 进行编码,那么生成的密码就类似 {bcrypt}$2a$10$oE39aG10kB/rFu2vQeCJTu/V/v4n6DRR0f8WyXRiAYvBpmadoOBE.。这样有什么用呢?每种密码加密之后,都会加上一个前缀,这样看到前缀,就知道该密文是使用哪个编码器生成的了。
  4. 最后 matches 方法的逻辑就很清晰了,先从密文中提取出来前缀,再根据前缀找到对应的 PasswordEncoder,然后再调用 PasswordEncoder 的 matches 方法进行密码比对。
  5. 如果根据提取出来的前缀,找不到对应的 PasswordEncoder,那么就会调用 UnmappedIdPasswordEncoder#matches 方法,进行密码比对,该方法实际上并不会进行密码比对,而是直接抛出异常。

OK,至此,相信大家都明白了 DelegatingPasswordEncoder 的工作原理了。

如果我们想同时使用多个密码加密方案,看来使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默认还不用配置。

4.体验

接下来我们稍微体验一下 DelegatingPasswordEncoder 的用法。

首先我们来生成三个密码作为测试密码:

代码语言:javascript
复制
@Test
void contextLoads() {
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
    encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
    DelegatingPasswordEncoder encoder1 = new DelegatingPasswordEncoder("bcrypt", encoders);
    DelegatingPasswordEncoder encoder2 = new DelegatingPasswordEncoder("MD5", encoders);
    DelegatingPasswordEncoder encoder3 = new DelegatingPasswordEncoder("noop", encoders);
    String e1 = encoder1.encode("123");
    String e2 = encoder2.encode("123");
    String e3 = encoder3.encode("123");
    System.out.println("e1 = " + e1);
    System.out.println("e2 = " + e2);
    System.out.println("e3 = " + e3);
}

生成结果如下:

代码语言:javascript
复制
e1 = {bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi
e2 = {MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2
e3 = {noop}123

接下来,我们把这三个密码拷贝到 SecurityConfig 中去:

代码语言:javascript
复制
@Configuration("aaa")
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    protected UserDetailsService userDetailsService() {

        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("javaboy").password("{bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi").roles("admin").build());
        manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build());
        manager.createUser(User.withUsername("江南一点雨").password("{MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2").roles("user").build());
        return manager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                ...
    }
}

这里三个用户使用三种不同的密码加密方式。

配置完成后,重启项目,分别使用 javaboy/123、sang/123 以及 江南一点雨/123 进行登录,发现都能登录成功。

5.意义何在?

为什么我们会有这种需求?想在项目种同时存在多种密码加密方案?其实这个主要是针对老旧项目改造用的,密码加密方式一旦确定,基本上没法再改了(你总不能让用户重新注册一次吧),但是我们又想使用最新的框架来做密码加密,那么无疑,DelegatingPasswordEncoder 是最佳选择。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 江南一点雨 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.PasswordEncoder
  • 2.PasswordEncoder 在哪里起作用
  • 3.默认的是什么?
  • 4.体验
  • 5.意义何在?
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档