这篇文章中,松哥给大家介绍了两种密码加密方案,但是两种都是独立使用的!能不能在同一个项目中同时存在多种密码加密方案呢?答案是肯定的!
站在用户的角度思考问题,与客户深入沟通,找到南部网站设计与南部网站推广的解决方案,凭借多年的经验,让设计与互联网技术结合,创造个性化、用户体验好的作品,建站类型包括:做网站、网站制作、企业官网、英文网站、手机端网站、网站推广、域名申请、雅安服务器托管、企业邮箱。业务覆盖南部地区。
今天松哥就来和大家聊一聊,如何在 Spring Security 中,让多种不同的密码加密方案并存。
为什么要加密?常见的加密算法等等这些问题我就不再赘述了,大家可以参考之前的:Spring Boot 中密码加密的两种姿势!,咱们直接来看今天的正文。
在 Spring Security 中,跟密码加密/校验相关的事情,都是由 PasswordEncoder 来主导的,PasswordEncoder 拥有众多的实现类:
这些实现类,有的已经过期了,有的用处不大。对于我们而言,最常用的莫过于 BCryptPasswordEncoder。
PasswordEncoder 本身是一个接口,里边只有三个方法:
- public interface PasswordEncoder {
- String encode(CharSequence rawPassword);
- boolean matches(CharSequence rawPassword, String encodedPassword);
- default boolean upgradeEncoding(String encodedPassword) {
- return false;
- }
- }
PasswordEncoder 的实现类,则具体实现了这些方法。
对于我们开发者而言,我们通常都是在 SecurityConfig 中配置一个 PasswordEncoder 的实例,类似下面这样:
- @Bean
- PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
剩下的事情,都是由系统调用的。今天我们就来揭开系统调用的神秘面纱!我们一起来看下系统到底是怎么调用的!
首先,松哥在前面的文章中和大家提到过,Spring Security 中,如果使用用户名/密码的方式登录,密码是在 DaoAuthenticationProvider 中进行校验的,大家可以参考:SpringSecurity 自定义认证逻辑的两种方式(高级玩法)。
我们来看下 DaoAuthenticationProvider 中密码是如何校验的:
- 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 的定义,如下:
- 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 方法中完成的,我们一起来看下该方法的定义:
- 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);
- }
从这段代码中我们可以看到:
至此,就真相大白了,我们配置的 PasswordEncoder 实例确实用上了。
同时大家看到,如果我们不进行任何配置,默认的 PasswordEncoder 也会被提供,那么默认的 PasswordEncoder 是什么呢?我们就从这个方法看起:
- public DaoAuthenticationProvider() {
- setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
- }
继续:
- public class PasswordEncoderFactories {
- public static PasswordEncoder createDelegatingPasswordEncoder() {
- String encodingId = "bcrypt";
- Map
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() {}
- }
可以看到:
我们来看下 DelegatingPasswordEncoder 的定义:
- 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
idToPasswordEncoder; - private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
- public DelegatingPasswordEncoder(String idForEncode,
- Map
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 + "\"");
- }
- }
- }
这段代码比较长,我来和大家挨个解释下:
OK,至此,相信大家都明白了 DelegatingPasswordEncoder 的工作原理了。
如果我们想同时使用多个密码加密方案,看来使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默认还不用配置。
接下来我们稍微体验一下 DelegatingPasswordEncoder 的用法。
首先我们来生成三个密码作为测试密码:
- @Test
- void contextLoads() {
- Map
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);
- }
生成结果如下:
- e1 = {bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi
- e2 = {MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2
- e3 = {noop}123
接下来,我们把这三个密码拷贝到 SecurityConfig 中去:
- @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 进行登录,发现都能登录成功。
为什么我们会有这种需求?想在项目种同时存在多种密码加密方案?其实这个主要是针对老旧项目改造用的,密码加密方式一旦确定,基本上没法再改了(你总不能让用户重新注册一次吧),但是我们又想使用最新的框架来做密码加密,那么无疑,DelegatingPasswordEncoder 是最佳选择。
好啦,这就是今天和小伙伴们分享的多种密码加密方案问题,感兴趣的小伙伴记得点个在看鼓励下松哥哦~
本文转载自微信公众号「江南一点雨」,可以通过以下二维码关注。转载本文请联系江南一点雨公众号。
本文标题:SpringSecurity如何实现多种加密方案共存
URL地址:http://www.stwzsj.com/qtweb/news22/13472.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联