密码泄露,多个网站用同一密码。salt加盐。

通过对认证流程分析,实际的密码比较是由PasswordEncoder完成的,因此只需要使用PasswordEncoder不同的实现来完成的。
public interface PasswordEncoder {// 用来进行明文加密String encode(CharSequence rawPassword);// 用来比较密码boolean matches(CharSequence rawPassword, String encodedPassword);// 用来对密码进行升级default boolean upgradeEncoding(String encodedPassword) {return false;}
}
默认提供的加密算法如下:


在SpringSecurity后,默认的密码加密方式为DelegatingPasswordEncoder(一个代理类,而不是加密方案)。主要用来代理上面不同的加密方案。为什么不直接使用加密方案,而采用代理类?
通过源码分析可以知道,如果在工厂类中制定了PasswordEncoder,就会使用PasswordEncoder,否则默认使用DelegatingPasswordEncoder。
SecurityConfigure -> AuthenticationManager -> PasswordEncoder -> DelegatingPasswordEncoder -> PasswordEncoderFactory
@Test
void test_bcrypt_security() {BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);String encode = bCryptPasswordEncoder.encode("123");System.out.println("encode = " + encode);
}
{bcrypt}// 使用PasswordEncoder的第一种方式
@Bean
public UserDetailsService userDetailsService() {InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();memoryUserDetailsManager.createUser(User.withUsername("whx").password("{bcrypt}$2a$10$DAmuV68SQcVTIXf9Pvb3kerV6KSmX6sNNV/o9LUoejrC0A21Bw/m.").roles("admin").build());return memoryUserDetailsManager;
}
// 使用PasswordEncoder的第二种方式
@Bean
public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder(10);
}
@Bean
public UserDetailsService userDetailsService() {InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();memoryUserDetailsManager.createUser(User.withUsername("whx").password("$2a$10$DAmuV68SQcVTIXf9Pvb3kerV6KSmX6sNNV/o9LUoejrC0A21Bw/m.").roles("admin").build());return memoryUserDetailsManager;
}
要实现密码的自动升级,我们只需要实现UserDetailsPasswordService接口中的updatePassword方法即可。
@Component
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.getUserByUname(username);if (ObjectUtils.isEmpty(user)) {throw new UsernameNotFoundException("用户名不正确");}List roles = userMapper.getRolesByUid(user.getId());user.setRoles(roles);return user;}@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {User param = (User) user;Integer result = userMapper.updateUnameByName(user.getUsername(), newPassword);if (result == 1) {param.setPassword(newPassword);}return param;}
}
RememberMe记住我。是一种服务端的行为,而不是将用户密码保存在cookie中。传统登录方式是基于Session的,这样一旦用户关闭浏览器重开,就需要再次登录,太过麻烦。
实现思路是通过cookie来记住当前用户身份。当用户登录成功后,通过算法,将用户信息、时间戳加密后通过响应头带回前端存到cookie。当重开浏览器后,会自动将cookie信息发送给服务器进行校验分析。从而确定用户身份。具有时效性,一般的为一周左右。
http.rememberMe();@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated();http.formLogin();// 开启记住我功能http.rememberMe();http.csrf().disable();return http.build();
}
记住我
server:servlet:session:timeout: 1
1. 求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没值的话表示用户尚未登录,此时调用autoLogin 方法进行自动登录。
2. 当自动登录成功后返回的rememberMeAuth 不为null 时,表示自动登录成功,此时调用authenticate方法对 key 进行校验,并且将登录成功的用户信息保存到SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含RememberMeServices 中的 1oginSuccess 方法。
3. 如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调.onUnsuccessfulAuthentication 和onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现这就是RememberMeAuthenticationFilter 过滤器所做的事情,成功将RememberMeServices的服务集成进来。
public interface RememberMeServices {// 从请求中获取参数,完成自动登录Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);// 自动登录失败的回调void loginFail(HttpServletRequest request, HttpServletResponse response);// 自动登录成功的回调void loginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication);
}

processAutoLoginCookie方法用来验证 Cookie 中的令牌信息是否合法:
:隔开,通过MD5进行加密,并将加密结果转为一个字符串返回。
1. onLoginSuccess回调中,首先获取用户经和密码信息,如果登录成功后用户名密码从successfulAuthentication对象中擦除,则从数据库中重新加载。
2. 计算出令牌的过期时间,令牌默认有效期是两周。
3. 根据令牌的过期时间、用户名以及用户密码,计算签名
4. 调用 setCookie 方法设置 Cookie.。参数按顺序是用户名、过期时间以及签名,在setCookie 方法中会将数组转为字符串,并进行 Base64编码后响应给前端。



@EnableWebSecurity
public class SecurityConfig2 {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Autowiredprivate DataSource dataSource;@Autowiredprivate JdbcTemplate jdbcTemplate;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated();http.formLogin();// 开启记住我功能http.rememberMe()// 是否总是记住我.alwaysRemember(true)// 指定rememberme的实现.rememberMeServices(rememberMeServices());http.csrf().disable();return http.build();}@Beanpublic RememberMeServices rememberMeServices() {JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);// 启动时候创建表结构tokenRepository.setCreateTableOnStartup(true);return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),myUserDetailsService, tokenRepository);}
}
@EnableWebSecurity
public class SecurityConfig2 {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Autowiredprivate DataSource dataSource;@Autowiredprivate JdbcTemplate jdbcTemplate;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated();http.formLogin();// 开启记住我功能http.rememberMe()// 是否总是记住我.alwaysRemember(true)// 指定rememberme的实现.tokenRepository(persistentTokenRepository());http.csrf().disable();return http.build();}@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();List
cookie实现:
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {@Overrideprotected boolean rememberMeRequested(HttpServletRequest request, String parameter) {// 这里可以在LoginFilter中读取出来,保存到request中。String paramValue = String.valueOf(request.getAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER));// 也可以在这里获取
// try {
// Map userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// String rememberVal = String.valueOf(userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER));
// } catch (IOException e) {
// e.printStackTrace();
// }if (paramValue != null) {if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {return true;}}return false;}public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {super(key, userDetailsService, tokenRepository);}
}
这里只展示关键部分代码,详细参考附录一说明。
/*** @author Huathy* @date 2023-03-06 23:07* @description*/
// 开启web安全
@EnableWebSecurity
@Slf4j
public class SecurityConfig {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Autowiredprivate AuthenticationConfiguration authenticationConfiguration;@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();return authenticationManager;}@Beanpublic LoginVcFilter loginVcFilter() throws Exception {log.info(" === loginFilter init ===");LoginVcFilter loginVcFilter = new LoginVcFilter();// 2. 指定认证处理URLloginVcFilter.setFilterProcessesUrl("/dologin");// 设置认证成功时使用自定义的记住我功能loginVcFilter.setRememberMeServices(rememberMeServices());return loginVcFilter;}@Beanprotected SecurityFilterChain configure(HttpSecurity http) throws Exception {log.info(" === 替换了 loginVcFilter === ");http.addFilterAt(loginVcFilter(), UsernamePasswordAuthenticationFilter.class);// 开启记住我 这里是设置携带记住我cookie的处理http.rememberMe().rememberMeServices(rememberMeServices());return http.build();}@Beanpublic RememberMeServices rememberMeServices() {return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), myUserDetailsService, new InMemoryTokenRepositoryImpl());}
}
下一篇:机器学习笔记:正则化