• SpringSecurity系列 - 13 SpringSecurity 密码加密认证 PasswordEncoder


    文章目录

    01. 密码加密算法简介

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

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

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

    BCryptPasswordEncoder:使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时 BCryptPasswordEncoder “为自己带盐”开发者不需要额外维护一个“盐” 字段,使用BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生
    成的加密字符串都不相同。

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

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

    SCryptPasswordEncoder:使用scrypt 算法对密码进行加密,和前面的几种类似,serypt 也是一种故意降低运算速度的算法,而且需要大量内存。

    02. 环境准备

    ① 控制器

    @RestController
    public class IndexController {
        @RequestMapping("/index")
        public String index() {
            System.out.println("hello index");
            return "hello index";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ② SpringSecurity 配置类:

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Bean
        public UserDetailsService userDetailsService(){
            InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
            inMemoryUserDetailsManager.createUser(User
                    .withUsername("root")
                    .password("{noop}123")
                    .roles("admin").build());
            return inMemoryUserDetailsManager;
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
           auth.userDetailsService(userDetailsService());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 开启请求的权限管理
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable();
        }
    }
    
    • 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

    03. 认证流程源码分析

    步骤1: AbstractAuthenticationProcessingFilter#doFilter 认证请求入口方法

    访问登录页面,输入配置的用户名密码root/123登录:
    在这里插入图片描述
    请求首先进入AbstractAuthenticationProcessingFilter#doFilter方法,AbstractAuthenticationProcessingFilter过滤器是请求认证处理的入口:

    public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
        implements ApplicationEventPublisherAware, MessageSourceAware {
    
        private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            // 判断请求是否是需要认证
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
                return;
            }
            try {
                // 调用实现类的attemptAuthentication方法尝试认证
                Authentication authenticationResult = attemptAuthentication(request, response);
                if (authenticationResult == null) {
                    return;
                }
                this.sessionStrategy.onAuthentication(authenticationResult, request, response);
                // Authentication success
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }
                successfulAuthentication(request, response, chain, authenticationResult);
            }
            catch (InternalAuthenticationServiceException failed) {
                // Authentication failed
                this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
                unsuccessfulAuthentication(request, response, failed);
            }
            catch (AuthenticationException ex) {
                // Authentication failed
                unsuccessfulAuthentication(request, response, ex);
            }
        }
    }
    
    • 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

    步骤2:UsernamePasswordAuthenticationFilter#attemptAuthentication 尝试认证方法

    执行 attemptAuthentication(request, response) 会调用UsernamePasswordAuthenticationFilter#attemptAuthentication方法,该方法会从请求中获取用户名和密码,然后将用户名和密码封装成一个待认证的Authentication对象,交给AuthenticationManager接口的子类ProviderManager类去做认证。

    public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    	
        // 表单登录默认的用户名name属性值
        public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    	// 表单登录默认的密码name属性值
        public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    	// 表单登录默认的登录请求路径和登录方式
        private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");                                                    	
        private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
        private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
        private boolean postOnly = true;
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 判断请求方式是不是post方式
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            // 从请求中获取用户名
            String username = obtainUsername(request);
            username = (username != null) ? username : "";
            username = username.trim();
            // 从请求中获取密码
            String password = obtainPassword(request);
            password = (password != null) ? password : "";
            // 将用户名和密码封装成待认证的Authentication对象
            // UsernamePasswordAuthenticationToken继承自Authentication类
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            setDetails(request, authRequest);
            // 调用AuthenticationManager接口的子类进行用户密码的认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        
        @Nullable
    	protected String obtainPassword(HttpServletRequest request) {
    		return request.getParameter(this.passwordParameter);
    	}
    
    	@Nullable
    	protected String obtainUsername(HttpServletRequest request) {
    		return request.getParameter(this.usernameParameter);
    	}
    }
    
    • 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

    UsernamePasswordAuthenticationToken 源码:

    public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    	// 用户名
        private final Object principal;
    	// 密码
        private Object credentials;
    
        public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
            super(null);
            this.principal = principal;
            this.credentials = credentials;
            setAuthenticated(false);
        }
        // ....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    步骤3:ProviderManager#authenticate 认证方法

    调用 this.getAuthenticationManager().authenticate(authRequest) 方法会进入ProviderManager#authenticate方法。在ProviderManager类中会维护一个AuthenticationProvider列表,AuthenticationProvider是具体认证方式的接口,不同的认证方式对应不同的实现类,比如匿名认证方式为AnonymousAuthenticationProvider,用户名密码认证方式为DaoAuthenticationProvider。。。

    真正去执行认证的是每个AuthenticationProvider接口的实现类,在ProviderManager类中首先会遍历AuthenticationProvider列表,判断当前AuthenticationProvider实现类支不支持对传入的Authentication对象的认证,如果不支持继续下一次循环,如果支持就调用当前AuthenticationProvider实现类的authenticate方法执行认证。

    public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
        
        private List<AuthenticationProvider> providers = Collections.emptyList();
        private AuthenticationManager parent;
        
       /**
       * @param authentication 待认证的Authentication对象:UsernamePasswordAuthenticationToken
       * @param Authentication 认证成功后的Authentication对象
       */
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    		Class<? extends Authentication> toTest = authentication.getClass();
    		AuthenticationException lastException = null;
    		AuthenticationException parentException = null;
    		Authentication result = null;
    		Authentication parentResult = null;
    		int currentPosition = 0;
    		int size = this.providers.size();
    		for (AuthenticationProvider provider : getProviders()) {
                // 判断provider是否支持UsernamePasswordToken对象的认证
    			if (!provider.supports(toTest)) {
                    // 不支持,直接跳出循环
    				continue;
    			}
    			if (logger.isTraceEnabled()) {
    				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
    						provider.getClass().getSimpleName(), ++currentPosition, size));
    			}
    			try {
                    // 调用provider的authenticate方法进行认证
    				result = provider.authenticate(authentication);
    				if (result != null) {
    					copyDetails(authentication, result);
    					break;
    				}
    			}
    			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
    				prepareException(ex, authentication);
    				throw ex;
    			}
    			catch (AuthenticationException ex) {
    				lastException = ex;
    			}
    		}
            // 如果当前ProviderManager中的AuthenticationProvider实现类都不能进行认证
    		if (result == null && this.parent != null) {
    			// Allow the parent to try.
    			try {
                    // 尝试调用当前ProviderManager的父类的authenticate进行认证
                    // ProviderManager的父类仍然是ProviderManager
                    // 父类的ProviderManager中也会维护一个AuthenticationProvider列表
                    // 相当于继续回调本类的笨本方法
    				parentResult = this.parent.authenticate(authentication);
    				result = parentResult;
    			}
    			catch (ProviderNotFoundException ex) {
    			}
    			catch (AuthenticationException ex) {
    				parentException = ex;
    				lastException = ex;
    			}
    		}
           // ...
        }
    }
    
    • 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
    • 62
    • 63
    • 64

    步骤4:AbstractUserDetailsAuthenticationProvider#authenticate 具体认证方式

    provider.authenticate(authentication) 最终会使用 DaoAuthenticationProvider 对 UsernamePasswordToken 对象进行认证,由于 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider,因此请求最终会进入AbstractUserDetailsAuthenticationProvider#authenticate方法。

    在该方法中会根据用户名称获取用户信息,然后对用户信息进行校验,校验通过后返回Authentication对象。

    public abstract class AbstractUserDetailsAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {
            
      /**
       * @param authentication 待认证的Authentication对象:UsernamePasswordAuthenticationToken
       * @param Authentication 认证成功后的Authentication对象
       */
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String username = determineUsername(authentication);
            boolean cacheWasUsed = true;
            // 从缓存中获取用户信息
            UserDetails user = this.userCache.getUserFromCache(username
            // 缓存中获取不到                                                   
            if (user == null) {
                cacheWasUsed = false;
                try {
                    // 1、根据输入的username从数据源中获取UserDetails用户信息
                    user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
                }catch (UsernameNotFoundException ex) {
                    this.logger.debug("Failed to find user '" + username + "'");
                    // ...
                }
                Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
            }
            try {
                // 2、从数据库源中获取UserDetails后,校验用户状态
                this.preAuthenticationChecks.check(user);
                // 5、校验密码是否正确
                additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
            }catch (AuthenticationException ex) {
                // ...
            }
            // 6、校验用户凭证是否过期
            this.postAuthenticationChecks.check(user);
            // 7、将从数据源中查询到的用户信息放入缓存  
            if (!cacheWasUsed) {
                this.userCache.putUserInCache(user);
            }
            Object principalToReturn = user;
            if (this.forcePrincipalAsString) {
                principalToReturn = user.getUsername();
            }
            // 返回认证成功的Authentication                                          
            return createSuccessAuthentication(principalToReturn, authentication, 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    下面我们来重点分析该方法做了声明事情:

    • 根据用户输入的username从数据源中获取UserDetails用户信息;
    • 校验用户是否被禁用,使用被锁定,用户账号是否过期;
    • 校验用户输入的原始密码加密后和数据库中密码是否一致;
    • 校验用户凭证是否过期;
    • 返回认证成功的Authentication认证对象;
    方法1:AbstractUserDetailsAuthenticationProvider#retrieveUser 根据username获取用户信息

    进入子类DaoAuthenticationProvider 的retrieveUser方法:

    public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    
        @Override
        protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            prepareTimingAttackProtection();
            try {
                // 调用UserDetailsService接口实现类的loadUserByUsername方法
                // 根据username获取用户详情信息
                UserDetails loadedUser 
                    = this.getUserDetailsService().loadUserByUsername(username);
                // 非空校验
                if (loadedUser == null) {
                    throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
                }
                return loadedUser;
            }
            catch (UsernameNotFoundException ex) {
                mitigateAgainstTimingAttack(authentication);
                throw ex;
            }
            catch (InternalAuthenticationServiceException ex) {
                throw ex;
            }
            catch (Exception ex) {
                throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
            }
        }
        
        protected UserDetailsService getUserDetailsService() {
            return this.userDetailsService;
        }
    }
    
    • 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

    进入UserDetailsService接口的实现类 InMemoryUserDetailsManager#loadUserByUsername 方法:

    public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 根据用户名获取用户详情信息
            UserDetails user = this.users.get(username.toLowerCase());
            // 非空校验
            if (user == null) {
                throw new UsernameNotFoundException(username);
            }
            // 返回UserDetails
            return new User(user.getUsername(), 
                            user.getPassword(), 
                            user.isEnabled(), 
                            user.isAccountNonExpired(),
                            user.isCredentialsNonExpired(), 
                            user.isAccountNonLocked(), 
                            user.getAuthorities());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    UserDetails 类源码:

    public interface UserDetails extends Serializable {
       // 返回授予用户的权限
       Collection<? extends GrantedAuthority> getAuthorities();
    
       // 返回用于验证用户的密码
       String getPassword();
    
       // 返回用于验证用户的用户名
       String getUsername();
    
       // 指示用户的帐户是否已过期, 无法验证过期的帐户
       boolean isAccountNonExpired();
    
       // 指示用户是被锁定还是解锁, 无法验证锁定的用户
       boolean isAccountNonLocked();
    
       // 指示用户的凭据(密码)是否已过期, 过期的凭据会阻止身份验证。
       boolean isCredentialsNonExpired();
    
       // 指示用户是启用还是禁用, 无法验证禁用的用户。
       boolean isEnabled();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    方法2:AbstractUserDetailsAuthenticationProvider#preAuthenticationChecks 校验用户是否锁定,是否过期,是否禁用
    public interface UserDetailsChecker {
       void check(UserDetails toCheck);
    }
    
    • 1
    • 2
    • 3
    public abstract class AbstractUserDetailsAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {
        
        // 内部类
        private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
    
            @Override
            public void check(UserDetails user) {
                // 判断用户是否被锁定,如果被断定,抛出LockedException异常
                if (!user.isAccountNonLocked()) {
                    AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is locked");
                    throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
                                              .getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
                }
                // 判断用户是否被禁用,如果被禁用抛出DisabledException异常
                if (!user.isEnabled()) {
                    AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is disabled");
                    throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
                                                .getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
                }
                // 判断用户是否已过期,如果过期抛出AccountExpiredException异常
                if (!user.isAccountNonExpired()) {
                    AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account has expired");
                    throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
                                                      .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
                }
            }
        }
    }
    
    • 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
    方法3: AbstractUserDetailsAuthenticationProvider#additionalAuthenticationChecks 密码认证源码

    该方法会进入 AbstractUserDetailsAuthenticationProvider 子类 DaoAuthenticationProvider#additionalAuthenticationChecks 方法:

    public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
        // 密码加密接口
        private PasswordEncoder passwordEncoder;
        
        /**
        * @param userDetails 数据源中的  UserDetails 用户信息 
        * @param authentication 待认证的 Authentication对象:UsernamePasswordAuthenticationToken
        */
        @Override
        @SuppressWarnings("deprecation")
        protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            // 判断用户输入的原始密码是否为空,如果为空抛出BadCredentialsException异常
            if (authentication.getCredentials() == null) {
                this.logger.debug("Failed to authenticate since no credentials provided");
                throw new BadCredentialsException(this.messages
                                                  .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
            // 获取用户输入的原始密码
            String presentedPassword = authentication.getCredentials().toString();
            // 将用户输入的原始密码和数据库中的用户密码比较,如果不相桶抛出BadCredentialsException异常
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Failed to authenticate since password does not match stored value");
                throw new BadCredentialsException(this.messages
                                                  .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
    
    • 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

    在这里插入图片描述

    步骤1:WebSecurityConfigurerAdapter#matches 密码比较

    this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()) 方法会进入WebSecurityConfigurerAdapter#matches 方法:

    public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
    
       /**
        * @param rawPassword :用户输入的原始密码
        * @param prefixEncodedPassword : 数据库中查询到的用户密码
        */
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            // 调用 PasswordEncoder 接口实现类的matches方法实现密码比较
            return getPasswordEncoder().matches(rawPassword, encodedPassword);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    步骤2:WebSecurityConfigurerAdapter#getPasswordEncoder 获取密码加密算法
    public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
        
        private PasswordEncoder getPasswordEncoder() {
            if (this.passwordEncoder != null) {
                return this.passwordEncoder;
            }
            // 获取PasswordEncoder实例
            PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
            // 获取默认的密码加密方式
            if (passwordEncoder == null) {
                passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            }
            this.passwordEncoder = passwordEncoder;
            return passwordEncoder;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    步骤3:PasswordEncoderFactories#createDelegatingPasswordEncoder 创造密码加密实例
    public final class PasswordEncoderFactories {
    
        private PasswordEncoderFactories() {
        }
    
        // 使用默认映射创建 DelegatingPasswordEncoder。 
        // 可能会添加其他映射,并且将更新编码以符合最佳实践。 
        @SuppressWarnings("deprecation")
        public static PasswordEncoder createDelegatingPasswordEncoder() {
    
            String encodingId = "bcrypt"; 
            
            // key是密码加密算法表示,value是密码加密算法实现类
            Map<String, PasswordEncoder> encoders = new HashMap<>();
            
            // BCryptPasswordEncoder 加密算法
            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());
    
    		// DelegatingPasswordEncoder 默认使用的就是 BCryptPasswordEncoder 加密算法
            return new DelegatingPasswordEncoder(encodingId, encoders);
        }
    }
    
    • 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

    进入 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<String, PasswordEncoder> idToPasswordEncoder;
    
       /**
        * 构造 DelegatingPasswordEncoder
        * @param idForEncode : encodingId
        * @param idToPasswordEncoder : Map
        */
        public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
            if (idForEncode == null) {
                throw new IllegalArgumentException("idForEncode cannot be null");
            }
            // 如果map的key不包含idToPasswordEncoder,抛出异常
            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;
            // 根据 idForEncode 获取 PasswordEncoder
            this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
            // idToPasswordEncoder 就是当前 PasswordEncoder
            this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
        }
    }
    
    • 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

    通过源码分析看出,DelegatingPasswordEncoder 默认使用的是BCryptPasswordEncoder加密方式。

    通过debug也可以看到PasswordEncoder的默认实现类是DelegatingPasswordEncoder,因此会调用DelegatingPasswordEncoder#matches方法进行密码认证。
    在这里插入图片描述

    步骤4:DelegatingPasswordEncoder#matches 方法密码比较

    DelegatingPasswordEncoder 是 PasswordEncoder接口的默认实习类

    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();
    
        /**
        * @param rawPassword :用户输入的原始密码:123
        * @param prefixEncodedPassword : 数据库中查询到的用户密码:{noop}123
        */
        @Override
        public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
            if (rawPassword == null && prefixEncodedPassword == null) {
                return true;
            }
            // 从 {noop}123 中提取出 id=noop
            String id = extractId(prefixEncodedPassword);
            // 根据id获取密码加密算法实现类
            PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
            if (delegate == null) {
                return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
            }
            // 从 {noop}123 中提取出密码 encodedPassword=123
            String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
            // 调用密码加密算法实现类的matches方法进行密码认证 
            return delegate.matches(rawPassword, encodedPassword);
        }
    
        // 例如从{noop}、{bcrypt}中提取出noop、bcrypt
        private String extractId(String prefixEncodedPassword) {
            if (prefixEncodedPassword == null) {
                return null;
            }
            // PREFIX="{"
            // start = 0
            int start = prefixEncodedPassword.indexOf(PREFIX);
            if (start != 0) {
                return null;
            }
            // 从指定索引开始,返回此字符串中第一次出现"}"的索引
            // end = 5
            int end = prefixEncodedPassword.indexOf(SUFFIX, start);
            if (end < 0) {
                return null;
            }
            // 截取出noop、bcrypt
            return prefixEncodedPassword.substring(start + 1, end);
        }
    
        // 例如:从{noop}123中提取出密码 123
        // 例如:从{bcrypt}$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a 中
        // 提取出密码$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a
        private String extractEncodedPassword(String prefixEncodedPassword) {
            // 从prefixEncodedPassword中获取"}"所在的索引
            int start = prefixEncodedPassword.indexOf(SUFFIX);
            // 截取索引start之后的字符串
            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
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    步骤5:NoOpPasswordEncoder#matches 无密码加密认证(单例设计模式)

    NoOpPasswordEncoder 是无密码加密方式的实现类:

    @Deprecated
    public final class NoOpPasswordEncoder implements PasswordEncoder {
    
        private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();
    
    	// 构造方法私有化
        private NoOpPasswordEncoder() {
        }
    
        @Override
        public String encode(CharSequence rawPassword) {
            return rawPassword.toString();
        }
    
        // 比较密码是否相同
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return rawPassword.toString().equals(encodedPassword);
        }
    
        /**
        * 返回一个单例
        */
        public static PasswordEncoder getInstance() {
            return INSTANCE;
        }
    
    }
    
    • 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
    方法4:AbstractUserDetailsAuthenticationProvider#postAuthenticationChecks 校验用户凭证是否过期
    public abstract class AbstractUserDetailsAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    
        // 内部类
        private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
            @Override
            public void check(UserDetails user) {
                // 判断用户凭证是否过期,如果过期抛出CredentialsExpiredException异常
                if (!user.isCredentialsNonExpired()) {
                    AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account credentials have expired");
                    throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
                                                          .getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                                                                      "User credentials have expired"));
                }
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    方法5:AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication 密码升级源码
    步骤1:DaoAuthenticationProvider#additionalAuthenticationChecks 密码升级
    public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
        private UserDetailsPasswordService userDetailsPasswordService;
    	private PasswordEncoder passwordEncoder;
        
        @Override
        protected Authentication createSuccessAuthentication(Object principal, 
            Authentication authentication,UserDetails user) {
            
            // 如果密码需要升级
            boolean upgradeEncoding = this.userDetailsPasswordService != null
                && this.passwordEncoder.upgradeEncoding(user.getPassword());
            if (upgradeEncoding) {
                // 获取用户输入的原始密码
                String presentedPassword = authentication.getCredentials().toString();
                // 使用DelegatingPasswordEncoder默认的加密算法对密码进行加密
                String newPassword = this.passwordEncoder.encode(presentedPassword);
                // 更新数据库中的密码
                user = this.userDetailsPasswordService.updatePassword(user, newPassword);
            }
            // 调用父类的createSuccessAuthentication方法返回认证成功的Authentication对象
            return super.createSuccessAuthentication(principal, authentication, user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    步骤2:WebSecurityConfigurerAdapter#upgradeEncoding 判断密码是否需要升级
    public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
        
        static class LazyPasswordEncoder implements PasswordEncoder {
    		// ...
            private PasswordEncoder passwordEncoder;
    
            // 判断密码是否需要升级
            @Override
            public boolean upgradeEncoding(String encodedPassword) {
                return getPasswordEncoder().upgradeEncoding(encodedPassword);
            }
    
            private PasswordEncoder getPasswordEncoder() {
                if (this.passwordEncoder != null) {
                    return this.passwordEncoder;
                }
                PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
                if (passwordEncoder == null) {
                    passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
                }
                this.passwordEncoder = passwordEncoder;
                return passwordEncoder;
            }
            
            // ...
        }
    }
    
    • 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
    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;
        @Override
        public boolean upgradeEncoding(String prefixEncodedPassword) {
            // 从prefixEncodedPassword中提取出id
            String id = extractId(prefixEncodedPassword);
            // 如果id不是bcrypt,则需要进行密码升级,返回true
            if (!this.idForEncode.equalsIgnoreCase(id)) {
                return true;
            }
            else {
                String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
                return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
            }
        }
    
        // 例如从{noop}、{bcrypt}中提取出noop、bcrypt
        private String extractId(String prefixEncodedPassword) {
            if (prefixEncodedPassword == null) {
                return null;
            }
            // PREFIX="{"
            // start = 0
            int start = prefixEncodedPassword.indexOf(PREFIX);
            if (start != 0) {
                return null;
            }
            // 从指定索引开始,返回此字符串中第一次出现"}"的索引
            // end = 5
            int end = prefixEncodedPassword.indexOf(SUFFIX, start);
            if (end < 0) {
                return null;
            }
            // 截取出noop、bcrypt
            return prefixEncodedPassword.substring(start + 1, end);
        }
    
        // 例如:从{noop}123中提取出密码 123
        // 例如:从{bcrypt}$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a 中
        // 提取出密码$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a
        private String extractEncodedPassword(String prefixEncodedPassword) {
            // 从prefixEncodedPassword中获取"}"所在的索引
            int start = prefixEncodedPassword.indexOf(SUFFIX);
            // 截取索引start之后的字符串
            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
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    步骤3:AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication 返回Authentication对象
    public abstract class AbstractUserDetailsAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    
        protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    
            UsernamePasswordAuthenticationToken result 
                = new UsernamePasswordAuthenticationToken(
                    principal,
                    authentication.getCredentials(), 
                    this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
    
            result.setDetails(authentication.getDetails());
            this.logger.debug("Authenticated user");
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    04. PasswordEncoder 源码

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

    public interface PasswordEncoder {
    
       // 对原始密码进行编码。 
       String encode(CharSequence rawPassword);
    
       // 验证从存储中获得的编码密码与提交的原始密码在编码后是否匹配。 
       // 用来比较密码的方法
       boolean matches(CharSequence rawPassword, String encodedPassword);
    
       // 来给密码进行升级的方法
       default boolean upgradeEncoding(String encodedPassword) {
          return false;
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    默认提供加密算法如下:
    在这里插入图片描述

    05. DelegatingPasswordEncoder 源码

    在 Spring Security 5.0之后,默认的密码加密方案是 DelegatingPasswordEncoder。从名字上来看,
    DelegatingPaswordEncoder 是一个代理类,而并非一种全新的密码加密方案,DeleggtinePasswordEncoder 主要用来代理上面介绍的不同的密码加密方案。为什么采DelegatingPasswordEncoder 而不是某一个具体加密方式作为默认的密码加密方案呢?主要考虑了如下两方面的因素:

    兼容性:使用 DelegatingPasswrordEncoder 可以帮助许多使用旧密码加密方式的系统顺利迁移到 Spring security 中,它允许在同一个系统中同时存在多种不同的密码加密方案。

    便捷性:密码存储的最佳方案不可能一直不变,如果使用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<String, PasswordEncoder> idToPasswordEncoder;
    
       private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
    
       /**
        * 创建 DelegatingPasswordEncoder 实例
        */
       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;
       }
    
      /**
       * 用来进行明文加密的
       * @param rawPassword 原始密码
       */
       @Override
       public String encode(CharSequence rawPassword) {
          return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
       }
    
       /**
        * 用来比较密码的方法
        * @param rawPassword 原始密码
        * @param prefixEncodedPassword 数据库中密码
        */
       @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);
       }
    
      /**
       * 从prefixEncodedPassword中提取encodeId
       * 比如从 {noop}123 中提取出 noop
       */
       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);
       }
    
      /**
       * 用来给密码进行升级的方法
       * @param prefixEncodedPassword 数据库中的密码
       */
       @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);
          }
       }
    
      /**
       * 从prefixEncodedPassword中提取出加密密码
       * 比如从 {noop}123 中提取出 123
       */
       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
    • 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
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133

    06. PasswordEncoderFactories 源码

    public final class PasswordEncoderFactories {
    
       private PasswordEncoderFactories() {
       }
     
       // 使用默认映射创建 DelegatingPasswordEncoder 实例。
       @SuppressWarnings("deprecation")
       public static PasswordEncoder createDelegatingPasswordEncoder() {
           
          String encodingId = "bcrypt";
          
          // encoders的 key 是密码加密算法的标识
          // encoders的 value 是密码加密算法实例
          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);
       }
    }
    
    • 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

    07. 如何使用 PasswordEncoder?

    查看WebSecurityConfigurerAdapter类中源码:

    public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
    
        // 静态内部类
        static class LazyPasswordEncoder implements PasswordEncoder {
    
            private ApplicationContext applicationContext;
    
            private PasswordEncoder passwordEncoder;
    
            LazyPasswordEncoder(ApplicationContext applicationContext) {
                this.applicationContext = applicationContext;
            }
    
            // 对原始密码进行加密
            @Override
            public String encode(CharSequence rawPassword) {
                return getPasswordEncoder().encode(rawPassword);
            }
    
            // 将原始密码rawPassword 使用加密算法后和数据库中的密码进行比较
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return getPasswordEncoder().matches(rawPassword, encodedPassword);
            }
    
            // 对数据库中的encodedPassword 密码升级
            @Override
            public boolean upgradeEncoding(String encodedPassword) {
                return getPasswordEncoder().upgradeEncoding(encodedPassword);
            }
    
            // 获取密码加密算法实例
            private PasswordEncoder getPasswordEncoder() {
                if (this.passwordEncoder != null) {
                    return this.passwordEncoder;
                }
                PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
                if (passwordEncoder == null) {
                    passwordEncoder 
                         = PasswordEncoderFactories.createDelegatingPasswordEncoder();
                }
                this.passwordEncoder = passwordEncoder;
                return passwordEncoder;
            }
    
            private <T> T getBeanOrNull(Class<T> type) {
                try {
                    return this.applicationContext.getBean(type);
                }
                catch (NoSuchBeanDefinitionException ex) {
                    return null;
                }
            }
    
            @Override
            public String toString() {
                return getPasswordEncoder().toString();
            }
        }
    }
    
    • 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

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

    08. 密码加密实战

    @SpringBootTest
    class SpringSecurity01ApplicationTests {
    
        @Test
        void contextLoads() {
    
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            // $2a$10$FgTxfDmntiSVypnKefnVLuyaee1X0P9u1o/EXqPMGxvWNg4Cf9HtW
            System.out.println(bCryptPasswordEncoder.encode("123"));
    
            Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
            // 053fc6cf124c27bd47e20cc1e9f156c222a0532bb5f6e16653b943f3f83a98c5855e133335e626e8
            System.out.println(pbkdf2PasswordEncoder.encode("123"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    ① 使用固定密码加密方案:

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public UserDetailsService userDetailsService(){
            InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
            inMemoryUserDetailsManager.createUser(User
                    .withUsername("root")
    				.password("$2a$10$FgTxfDmntiSVypnKefnVLuyaee1X0P9u1o/EXqPMGxvWNg4Cf9HtW")
                    .roles("admin")
                    .build());
            return inMemoryUserDetailsManager;
        }
        
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    ② 使用灵活密码加密方案:

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
    //    @Bean
    //    public PasswordEncoder passwordEncoder(){
    //        return new BCryptPasswordEncoder();
    //    }
    
        @Bean
        public UserDetailsService userDetailsService(){
            InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
            inMemoryUserDetailsManager.createUser(User
                    .withUsername("root")
                    // 在密码前加上{bcrypt}
                    .password("{bcrypt}$2a$10$FgTxfDmntiSVypnKefnVLuyaee1X0P9u1o/EXqPMGxvWNg4Cf9HtW")
                    .roles("admin").build());
            return inMemoryUserDetailsManager;
        }
        
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    启动项目使用用户名root和密码123登录即可。

    09. 密码自动升级

    推荐使用DelegatingPasswordEncoder 的另外一个好处就是自动进行密码加密方案的升级,这个功能在整合一些老的系统时非常有用。

    ① 数据库表

    CREATE TABLE `user` (
      `id` int NOT NULL AUTO_INCREMENT,
      `username` varchar(32) DEFAULT NULL,
      `password` varchar(255) DEFAULT NULL,
      `enabled` tinyint(1) DEFAULT NULL,
      `accountNonExpired` tinyint(1) DEFAULT NULL,
      `accountNonLocked` tinyint(1) DEFAULT NULL,
      `credentialsNonExpired` tinyint(1) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
    
    CREATE TABLE `role` (
      `id` int NOT NULL AUTO_INCREMENT,
      `name` varchar(32) DEFAULT NULL,
      `name_zh` varchar(32) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
    
    CREATE TABLE `user_role` (
      `id` int NOT NULL AUTO_INCREMENT,
      `uid` int DEFAULT NULL,
      `rid` int DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `uid` (`uid`),
      KEY `rid` (`rid`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3;
    
    • 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

    插入数据:

    在这里插入图片描述

    ② 整合MyBatis:

    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.2.0version>
    dependency>
    
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>8.0.15version>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>1.2.8version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&useSSL=false&serverTimezone=Hongkong
    spring.datasource.username=root
    spring.datasource.password=root
    
    mybatis.mapper-locations=classpath:mapper/*.xml
    mybatis.type-aliases-package=com.hh.entity
    
    logging.level.com.hh=debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    ③ 编写实体类:

    @Data
    public class User implements UserDetails{
        private Integer id;
        private String username;
        private String password;
        private Boolean enabled;
        private Boolean accountNonExpired;
        private Boolean accountNonLocked;
        private Boolean credentialsNonExpired;
        private List<Role> roles = new ArrayList<>();
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
            return grantedAuthorities;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return accountNonExpired;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return accountNonLocked;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return credentialsNonExpired;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    }
    
    • 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
    @Data
    public class Role {
        private Integer id;
        private String name;
        private String nameZh;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ④ 创建Dao :

    public interface UserDao {
        /**
         * 根据用户名查询用户
         * @param username 用户名
         * @return User
         */
        User loadUserByUsername(String username);
    
        /**
         * 根据用户id查询⻆色
         * @param uid 根据用户id
         * @return 角色列表
         */
        List<Role> getRolesByUid(Integer uid);
    
        /**
         * 更新密码
         * @param username 用户名
         * @param password 密码
         */
        Integer updatePassword(@Param("username") String username,@Param("password") String password);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    
    DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.hh.dao.UserDao">
        <update id="updatePassword">
            update `user` set password=#{password} where username=#{username}
        update>
    
        <select id="loadUserByUsername" resultType="com.hh.entity.User">
            select * from user where username = #{username}
        select>
        
        <select id="getRolesByUid" resultType="com.hh.entity.Role">
            select
                r.id,
                r.name,
                r.name_zh nameZh
            from role r, user_role ur
            where r.id = ur.rid
            and ur.uid = #{uid}
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    ⑤ 创建Service :

    @Service
    public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
    
        @Autowired
        private UserDao userDao;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userDao.loadUserByUsername(username);
            if(Objects.isNull(user)){
                throw new RuntimeException("用户不存在");
            }
            user.setRoles(userDao.getRolesByUid(user.getId()));
            return user;
        }
    
        @Override
        public UserDetails updatePassword(UserDetails user, String newPassword) {
            Integer result = userDao.updatePassword(user.getUsername(), newPassword);
            if (result == 1) {
                ((User) user).setPassword(newPassword);
            }
            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

    ⑥ 创建SpringSecurity:

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Autowired
        public MyUserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
           auth.userDetailsService(userDetailsService);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 开启请求的权限管理
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    启动项目使用用户名root和密码123进行测试,登录成功后查看数据库中的用户密码:
    在这里插入图片描述
    可以看到密码已经从{noop}变成了{bcrypt}

  • 相关阅读:
    milvus数据管理-压缩数据
    什么是mybatis,全是干货
    服务器主机云主机在日常维护需要注意的几个点
    基于springboot的学生毕业选题管理系统
    【luogu CF1286E】Fedya the Potter Strikes Back(字符串)(KMP)(势能分析)(线段树)
    Moonbeam Illuminate/22线上生态盛会|Derek开场演讲
    06.JAVAEE之线程4
    Dr4g0n b4ll: 1 ~ VulnHub
    Codeforces Round 958 (Div. 2)[部分题解ABC]
    SimpleServletHandlerAdapter类简介说明
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/126922157