13、Spring Security 实战 - 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";
    }
}

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

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

步骤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);
	}
}

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);
    }
    // ....
}

步骤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;
			}
		}
       // ...
    }
}

步骤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);
    }
}

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

  • 根据用户输入的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;
    }
}

进入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());
    }
}

UserDetails 类源码:

public interface UserDetails extends Serializable {
   
     
   // 返回授予用户的权限
   Collection<? extends GrantedAuthority> getAuthorities();

   // 返回用于验证用户的密码
   String getPassword();

   // 返回用于验证用户的用户名
   String getUsername();

   // 指示用户的帐户是否已过期, 无法验证过期的帐户
   boolean isAccountNonExpired();

   // 指示用户是被锁定还是解锁, 无法验证锁定的用户
   boolean isAccountNonLocked();

   // 指示用户的凭据(密码)是否已过期, 过期的凭据会阻止身份验证。
   boolean isCredentialsNonExpired();

   // 指示用户是启用还是禁用, 无法验证禁用的用户。
   boolean isEnabled();
}

方法2:AbstractUserDetailsAuthenticationProvider#preAuthenticationChecks 校验用户是否锁定,是否过期,是否禁用
public interface UserDetailsChecker {
   
     
   void check(UserDetails toCheck);
}

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"));
            }
        }
    }
}

方法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: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);
    }
}

步骤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;
    }
}

步骤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);
    }
}

进入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<String, PasswordEncoder>
    */
    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);
    }
}

通过源码分析看出,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);
    }
}

步骤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;
    }

}

方法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"));
            }
        }

    }
}

方法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);
    }
}

步骤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;
        }
        
        // ...
    }
}

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

}

步骤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;
    }
}

04. PasswordEncoder 源码

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

public interface PasswordEncoder {
   
     

   // 对原始密码进行编码。 
   String encode(CharSequence rawPassword);

   // 验证从存储中获得的编码密码与提交的原始密码在编码后是否匹配。 
   // 用来比较密码的方法
   boolean matches(CharSequence rawPassword, String encodedPassword);

   // 来给密码进行升级的方法
   default boolean upgradeEncoding(String encodedPassword) {
   
     
      return false;
   }
}

默认提供加密算法如下:
 

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 + "\"");
      }
   }
}

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

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

通过源码分析得知如果在工厂中指定了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"));
    }
}

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

@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;
    }
    
    // ...
}

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

@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;
    }
    
    // ...
}

启动项目使用用户名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;

插入数据:

 

②整合MyBatis:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>

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

③编写实体类:

@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;
    }
}

@Data
public class Role {
   
     
    private Integer id;
    private String name;
    private String nameZh;
}

④创建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);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!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>

⑤创建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;
    }
}

⑥创建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();
    }
}

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