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

news/2024/4/30 1:10:37/文章来源:https://blog.csdn.net/qq_42764468/article/details/126922157

文章目录

    • 01. 密码加密算法简介
    • 02. 环境准备
    • 03. 认证流程源码分析
      • 步骤1: AbstractAuthenticationProcessingFilter#doFilter 认证请求入口方法
      • 步骤2:UsernamePasswordAuthenticationFilter#attemptAuthentication 尝试认证方法
      • 步骤3:ProviderManager#authenticate 认证方法
      • 步骤4:AbstractUserDetailsAuthenticationProvider#authenticate 具体认证方式
        • 方法1:AbstractUserDetailsAuthenticationProvider#retrieveUser 根据username获取用户信息
        • 方法2:AbstractUserDetailsAuthenticationProvider#preAuthenticationChecks 校验用户是否锁定,是否过期,是否禁用
        • 方法3: AbstractUserDetailsAuthenticationProvider#additionalAuthenticationChecks 密码认证源码
          • 步骤1:WebSecurityConfigurerAdapter#matches 密码比较
          • 步骤2:WebSecurityConfigurerAdapter#getPasswordEncoder 获取密码加密算法
          • 步骤3:PasswordEncoderFactories#createDelegatingPasswordEncoder 创造密码加密实例
          • 步骤4:DelegatingPasswordEncoder#matches 方法密码比较
          • 步骤5:NoOpPasswordEncoder#matches 无密码加密认证(单例设计模式)
        • 方法4:AbstractUserDetailsAuthenticationProvider#postAuthenticationChecks 校验用户凭证是否过期
        • 方法5:AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication 密码升级源码
          • 步骤1:DaoAuthenticationProvider#additionalAuthenticationChecks 密码升级
          • 步骤2:WebSecurityConfigurerAdapter#upgradeEncoding 判断密码是否需要升级
          • 步骤3:AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication 返回Authentication对象
    • 04. PasswordEncoder 源码
    • 05. DelegatingPasswordEncoder 源码
    • 06. PasswordEncoderFactories 源码
    • 07. 如何使用 PasswordEncoder?
    • 08. 密码加密实战
    • 09. 密码自动升级

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 {@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Overrideprotected 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 GenericFilterBeanimplements 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 successif (this.continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}successfulAuthentication(request, response, chain, authenticationResult);}catch (InternalAuthenticationServiceException failed) {// Authentication failedthis.logger.error("An internal error occurred while trying to authenticate the user.", failed);unsuccessfulAuthentication(request, response, failed);}catch (AuthenticationException ex) {// Authentication failedunsuccessfulAuthentication(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;@Overridepublic 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);}@Nullableprotected String obtainPassword(HttpServletRequest request) {return request.getParameter(this.passwordParameter);}@Nullableprotected 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 AbstractUserDetailsAuthenticationProviderimplements AuthenticationProvider, InitializingBean, MessageSourceAware {/*** @param authentication 待认证的Authentication对象:UsernamePasswordAuthenticationToken* @param Authentication 认证成功后的Authentication对象*/@Overridepublic 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 {@Overrideprotected 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 {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 根据用户名获取用户详情信息UserDetails user = this.users.get(username.toLowerCase());// 非空校验if (user == null) {throw new UsernameNotFoundException(username);}// 返回UserDetailsreturn 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 AbstractUserDetailsAuthenticationProviderimplements AuthenticationProvider, InitializingBean, MessageSourceAware {// 内部类private class DefaultPreAuthenticationChecks implements UserDetailsChecker {@Overridepublic 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 : 数据库中查询到的用户密码*/@Overridepublic 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 获取 PasswordEncoderthis.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);// idToPasswordEncoder 就是当前 PasswordEncoderthis.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*/@Overridepublic boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {if (rawPassword == null && prefixEncodedPassword == null) {return true;}// 从 {noop}123 中提取出 id=noopString id = extractId(prefixEncodedPassword);// 根据id获取密码加密算法实现类PasswordEncoder delegate = this.idToPasswordEncoder.get(id);if (delegate == null) {return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);}// 从 {noop}123 中提取出密码 encodedPassword=123String encodedPassword = extractEncodedPassword(prefixEncodedPassword);// 调用密码加密算法实现类的matches方法进行密码认证 return delegate.matches(rawPassword, encodedPassword);}// 例如从{noop}、{bcrypt}中提取出noop、bcryptprivate String extractId(String prefixEncodedPassword) {if (prefixEncodedPassword == null) {return null;}// PREFIX="{"// start = 0int start = prefixEncodedPassword.indexOf(PREFIX);if (start != 0) {return null;}// 从指定索引开始,返回此字符串中第一次出现"}"的索引// end = 5int end = prefixEncodedPassword.indexOf(SUFFIX, start);if (end < 0) {return null;}// 截取出noop、bcryptreturn prefixEncodedPassword.substring(start + 1, end);}// 例如:从{noop}123中提取出密码 123// 例如:从{bcrypt}$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a 中// 提取出密码$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6aprivate 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() {}@Overridepublic String encode(CharSequence rawPassword) {return rawPassword.toString();}// 比较密码是否相同@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return rawPassword.toString().equals(encodedPassword);}/*** 返回一个单例*/public static PasswordEncoder getInstance() {return INSTANCE;}}

方法4:AbstractUserDetailsAuthenticationProvider#postAuthenticationChecks 校验用户凭证是否过期

public abstract class AbstractUserDetailsAuthenticationProviderimplements AuthenticationProvider, InitializingBean, MessageSourceAware {// 内部类private class DefaultPostAuthenticationChecks implements UserDetailsChecker {@Overridepublic 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;@Overrideprotected 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;// 判断密码是否需要升级@Overridepublic 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;@Overridepublic boolean upgradeEncoding(String prefixEncodedPassword) {// 从prefixEncodedPassword中提取出idString id = extractId(prefixEncodedPassword);// 如果id不是bcrypt,则需要进行密码升级,返回trueif (!this.idForEncode.equalsIgnoreCase(id)) {return true;}else {String encodedPassword = extractEncodedPassword(prefixEncodedPassword);return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);}}// 例如从{noop}、{bcrypt}中提取出noop、bcryptprivate String extractId(String prefixEncodedPassword) {if (prefixEncodedPassword == null) {return null;}// PREFIX="{"// start = 0int start = prefixEncodedPassword.indexOf(PREFIX);if (start != 0) {return null;}// 从指定索引开始,返回此字符串中第一次出现"}"的索引// end = 5int end = prefixEncodedPassword.indexOf(SUFFIX, start);if (end < 0) {return null;}// 截取出noop、bcryptreturn prefixEncodedPassword.substring(start + 1, end);}// 例如:从{noop}123中提取出密码 123// 例如:从{bcrypt}$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a 中// 提取出密码$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6aprivate String extractEncodedPassword(String prefixEncodedPassword) {// 从prefixEncodedPassword中获取"}"所在的索引int start = prefixEncodedPassword.indexOf(SUFFIX);// 截取索引start之后的字符串return prefixEncodedPassword.substring(start + 1);}}
步骤3:AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication 返回Authentication对象
public abstract class AbstractUserDetailsAuthenticationProviderimplements 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 原始密码*/@Overridepublic String encode(CharSequence rawPassword) {return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);}/*** 用来比较密码的方法* @param rawPassword 原始密码* @param prefixEncodedPassword 数据库中密码*/@Overridepublic 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 数据库中的密码*/@Overridepublic 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 {@Overridepublic String encode(CharSequence rawPassword) {throw new UnsupportedOperationException("encode is not supported");}@Overridepublic 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;}// 对原始密码进行加密@Overridepublic String encode(CharSequence rawPassword) {return getPasswordEncoder().encode(rawPassword);}// 将原始密码rawPassword 使用加密算法后和数据库中的密码进行比较@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return getPasswordEncoder().matches(rawPassword, encodedPassword);}// 对数据库中的encodedPassword 密码升级@Overridepublic 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;}}@Overridepublic String toString() {return getPasswordEncoder().toString();}}
}

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

08. 密码加密实战

@SpringBootTest
class SpringSecurity01ApplicationTests {@Testvoid contextLoads() {BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();// $2a$10$FgTxfDmntiSVypnKefnVLuyaee1X0P9u1o/EXqPMGxvWNg4Cf9HtWSystem.out.println(bCryptPasswordEncoder.encode("123"));Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();// 053fc6cf124c27bd47e20cc1e9f156c222a0532bb5f6e16653b943f3f83a98c5855e133335e626e8System.out.println(pbkdf2PasswordEncoder.encode("123"));}
}

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

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Beanpublic 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();
//    }@Beanpublic 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=rootmybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.hh.entitylogging.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<>();@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> grantedAuthorities = new ArrayList<>();roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));return grantedAuthorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic 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 mapperPUBLIC "-//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">selectr.id,r.name,r.name_zh nameZhfrom role r, user_role urwhere r.id = ur.ridand ur.uid = #{uid}</select>
</mapper>

⑤ 创建Service :

@Service
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {@Autowiredprivate UserDao userDao;@Overridepublic 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;}@Overridepublic 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 {@Autowiredpublic MyUserDetailsService userDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 开启请求的权限管理http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().csrf().disable();}
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_9929.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL-锁

MySQL-锁 锁分类 MySQL中锁按照粒度分&#xff0c;分为以下三类 全局锁&#xff1a;锁定数据库中的所有表表级锁&#xff1a;每次操作锁住整张表行级锁&#xff1a;每次操作锁住对应的行数据 全局锁 全局锁就是对整个数据库实例加锁&#xff0c;加锁后整个实例就处于只读状…

Koltin协程:异步热数据流的设计与使用

一.异步冷数据流 在Kotlin协程&#xff1a;协程的基础与使用中&#xff0c;通过使用协程中提供的flow方法可以创建一个Flow对象。这种方法得到的Flow对象实际上是一个异步冷数据流&#xff0c;代码如下&#xff1a; private suspend fun test() {val flow flow {emit(1)emit(…

ESP8266升級SDK到V3.0版本編譯報錯

編譯報錯信息 bin/libmain2.a(app_main.o): In function `user_uart_wait_tx_fifo_empty: (.irom0.text+0x678): undefined reference to `user_pre_init bin/libmain2.a(app_main.o): In function `system_phy_freq_trace_enable: (.irom0.text+0x6e4): undefined reference t…

SpringCloud基础7——Redis分布式缓存

用于复习快速回顾。 目录 1.Redis持久化 1.1.数据备份文件RDB持久化方案 1.1.1.执行时机 1.1.2.RDB原理 1.1.3.小结&#xff0c;bgsave流程、执行时间、缺点 1.2.追加文件AOF持久化方案 1.2.1.AOF原理 1.2.2.AOF配置 1.2.3.AOF文件重写 1.3.RDB与AOF对比 2.Redis主…

getBean方法源码

一、 三个API // 根据name获取bean Override public Object getBean(String name) throws BeansException {return doGetBean(name, null, null, false); }// 根据name获取bean&#xff0c;如果获取到的bean和指定类型不匹配&#xff0c;则抛出异常 Override public <T>…

C++11 - 8 -智能指针

C11 - 智能指针前言&#xff1a;普通指针&#xff1a;安全隐患&#xff1a;其他函数异常&#xff1a;new函数异常&#xff1a;智能指针&#xff1a;RAII原理&#xff1a;smart_ptr&#xff1a;auto_ptr&#xff1a;管理权转移&#xff1a;优点&#xff1a;缺点&#xff1a;uniq…

报告分享|2022汽车生态营销白皮书

报告链接&#xff1a;http://tecdat.cn/?p28679 不确定性增加&#xff0c;消费意愿在压力下等待释放 今年3月以来&#xff0c;受国际冲突和国内环境双重影响&#xff0c;能源价格大幅上涨&#xff0c;导致全球供应不稳定趋势加深&#xff0c;宏观经济下行压力明显&#xff0…

程序设计竞赛-过了这个村没这个店

文章目录个人经验竞赛简介蓝桥杯天梯赛CCPCICPC其他个人经验 初闻不知曲中意&#xff0c;再闻已是曲中人。 标题无意夸张&#xff0c;但是竞赛生涯的时间真的不长&#xff0c;机会真的错过了就没有了。一般来说&#xff0c;大一打基础&#xff0c;学习编程语言、数据结构和算法…

Moment.js的常用函数、借助vue和Moment.js实现一个简单的时钟

前言 项目中关于时间的处理是挺常见的&#xff0c;虽然之前就知道有Moment.js这个库&#xff0c;但是一直没有接触过。只不过最近同事在项目中使用了&#xff0c;那也只能简单学习一下&#xff0c;不然遇到了完全看不懂。 本文只介绍一下常用的函数&#xff0c;其他内容可以在…

想换工作?那还不赶紧来看看这份面试题

引言 “寒冬”之下&#xff0c;诸如 “Android 凉了”之类的话我已经屡见不鲜了&#xff0c;现在互联网行业的热潮已经褪去&#xff0c;开始恢复冷静&#xff1b;这样一来&#xff0c;互联网公司就会面向大量的开发者们&#xff0c;因此对应的要求只会越来越高&#xff1b;据反…

用纯css实现一个图片拼接九宫格

<style> body{ margin: 0; padding: 0; // 设定居中 display: flex; justify-content: center; align-items: center; height: 100vh; } .container{ width: 300px; height: 300px; display: flex; // 子盒子布局&#xff0c;要让子盒子之间有间隙就把宽高设大一些。 jus…

报告分享|2022年中国机器人产业图谱及云上发展研究报告

报告链接&#xff1a;http://tecdat.cn/?p28681 报告在分析当前我国机器人市场现状与产业图谱的基础上&#xff0c;对人工智能、5G、云计算、边缘计算等新兴技术赋能机器人智能化、轻量化、柔性化发展进行了理性探讨&#xff0c;结合阿里云加速器企业案例探讨了机器人企业的上…

连接查询-mysql详解(五)

上篇文章说了&#xff0c;mysql5.6.6版本之前数据默认在系统表空间&#xff0c;之后默认在独立表空间&#xff0c;innodb因为索引和数据在一个b树&#xff0c;所以两个文件&#xff0c;一个文件结构&#xff0c;一个存数据&#xff0c;myISAM则是三个文件。一个聚簇索引有两个段…

小程序云开发学习笔记

小程序云开发学习笔记 初始化 在app.js里面 小程序一开始就初始化&#xff0c;多次调用只有第一次触发 onLaunch() { console.log("小程序打开"); wx.cloud.init({ env: ayang-8g50ew302a3a6c5a, //云开发id }) } 数据库操作 查询&#xff08;一定要配置数据权限&a…

高等工程数学 —— 第一章 (1)距离与范数

前言 研一生活开始了&#xff0c;看了大家对我之前博客的鼓励让我知道写博客是一件多么有意义的事情。写这些让我遇见许多陌生的有缘人&#xff0c;有老骥伏枥的大叔、也有可爱温暖的学妹…… 这里将高等工程数学的笔记留给不爱吃香菜的月亮&#xff0c;希望这些陪伴过我的微光…

ElasticSearch(四)【高级查询】

四、高级查询 说明 ES中提供了一种强大的检索数据方式&#xff0c;这种检索方式称之为Query DSL&#xff0c;Query DSL是利用Rest API传递JSON格式的请求体&#xff08;Request Body&#xff09;数据与ES进行交互&#xff0c;这种方式的丰富查询语法让ES检索变得更强大&#xf…

Grafana alert预警+钉钉通知

1 Grafana alert预警 如下图所示&#xff0c;主要是前3步&#xff0c;设置alert rules、contact points 、notification policies。alert rules主要设置触发警告的规则&#xff1b;contact points设置通过什么发送预警&#xff0c;如钉钉&#xff1b;notification policies 将…

哲学家干饭问题 C++

哲学家干饭问题 C 哲学家就餐问题可以这样表述&#xff0c;假设有五位哲学家围坐在一张圆形餐桌旁&#xff0c;做以下两件事情之一&#xff1a;吃饭&#xff0c;或者思考。吃东西的时候&#xff0c;他们就停止思考&#xff0c;思考的时候也停止吃东西。餐桌上有五碗意大利面&am…

Vue2.0到3.0的过渡,setup,ref函数,reactive函数,计算属性computed

setup 1、Vue3.0的组件中所有用到的:数据、方法等等&#xff0c;均要配置在setup中&#xff0c;若要使用里面的数据&#xff0c;可以用return将其返回出来 2、若在setup中返回的是一个对象&#xff0c;则对象中的数据、方法、在模板中均可直接使用 例如 <template><di…

[Git] 系列三随意修改提交记录以及一些技巧

[Git] 系列三随意修改提交记录以及一些技巧 Author: Xin Pan Date: 2022.09.17 文章目录[Git] 系列三随意修改提交记录以及一些技巧整理提交记录未知提交号哈希值时怎么办&#xff1f;一些技巧本地栈式提交方法一方法二TagDescribe高级命令总结好了&#xff0c;大概总结好了。…