文章目录
- 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}