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