文章目录
- 密码加密
- 一、简介
- 密码为什么要加密
- 常见的加密解决方案
- PasswordEncoder详解
- DelegatingPasswordEncoder
- 二、自定义加密方式
- 1. 使用灵活的密码加密方案(BCryptPasswordEncoder)
- 加密
- 验证(推荐)需要在密码前指定加密类型`{bcrypt}`
- 2. 使用固定的密码加密方案(BCryptPasswordEncoder)
- 三、密码自动升级
- 实现UserDetailsPasswordService接口的updatePassword方法
- RememberMe记住我
- 一、简介
- 二、基本应用
- 三、原理分析
- RememberMeServices
- 默认使用TokenBasedRememberMeService
- 总结:
- RememberMe流程图
- 四、持久化令牌
- 实现一:需要手动创建数据库表结构
- 实现二:这种方式可以自动创建数据库表结构
- 五、前后端分离开发记住我
- 编写自定义的MyPersistentTokenBasedRememberMeServices
- 修改SecurityConfig配置
- 附:
密码加密
一、简介
密码为什么要加密
密码泄露,多个网站用同一密码。salt加盐。
常见的加密解决方案
- Hash算法:
最早使用类似SHA-256、SHA-512、MD5这样的单向Hash算法。用户注册后,数据库保存加密后的字串,当用户输入密码时,进行加密比对。
但是由于彩虹表攻击以及硬件发展,计算机每秒执行数十亿次hash计算,这意味着及时密码加密加盐也不安全。 - 单项自适应函数:
在SpringSecurity中,主推单向自适应函数。这种自适应单向函数在进行密码比对时,会有意占用大量系统资源(CPU、内存等),增加恶意用户攻击难度。
可以通过bcrypt、PBKDF2、sCrypt、argon2来体验这种自适应单项函数加密。
PasswordEncoder详解
通过对认证流程分析,实际的密码比较是由PasswordEncoder完成的,因此只需要使用PasswordEncoder不同的实现来完成的。
public interface PasswordEncoder {
// 用来进行明文加密
String encode(CharSequence rawPassword);
// 用来比较密码
boolean matches(CharSequence rawPassword, String encodedPassword);
// 用来对密码进行升级
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
默认提供的加密算法如下:
DelegatingPasswordEncoder
在SpringSecurity后,默认的密码加密方式为DelegatingPasswordEncoder(一个代理类,而不是加密方案)。主要用来代理上面不同的加密方案。为什么不直接使用加密方案,而采用代理类?
- 兼容性:使用DelegatingPasswordEncoder可以帮助使用旧密码加密方式的系统顺利迁移到SpringSecurity。允许在同一个系统中同时存在不同加密方案。
- 便捷性:密码存储方案是可以变化的。使用DelegatingPasswordEncoder作为默认密码加密方案,在需要修改加密方案时,只需要做出小部分改动。
通过源码分析可以知道,如果在工厂类中制定了PasswordEncoder,就会使用PasswordEncoder,否则默认使用DelegatingPasswordEncoder。
SecurityConfigure -> AuthenticationManager -> PasswordEncoder -> DelegatingPasswordEncoder -> PasswordEncoderFactory
二、自定义加密方式
1. 使用灵活的密码加密方案(BCryptPasswordEncoder)
加密
@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;
}
2. 使用固定的密码加密方案(BCryptPasswordEncoder)
// 使用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方法
要实现密码的自动升级,我们只需要实现UserDetailsPasswordService接口中的updatePassword方法即可。
@Component
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUname(username);
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("用户名不正确");
}
List<Role> roles = userMapper.getRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
@Override
public 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记住我
一、简介
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();
}
- 前端自定义页面
<p><input type="checkbox" name="remember-me">记住我</p>
<!-- 参数:remember-me:on -->
- 测试
配置session过期时间1分钟。
登陆时勾选记住我选项,然后重启服务端,就可以在测试接口中免登录。
server:
servlet:
session:
timeout: 1
三、原理分析
1. 求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没值的话表示用户尚未登录,此时调用autoLogin 方法进行自动登录。
2. 当自动登录成功后返回的rememberMeAuth 不为null 时,表示自动登录成功,此时调用authenticate方法对 key 进行校验,并且将登录成功的用户信息保存到SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含RememberMeServices 中的 1oginSuccess 方法。
3. 如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调.onUnsuccessfulAuthentication 和onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现这就是RememberMeAuthenticationFilter 过滤器所做的事情,成功将RememberMeServices的服务集成进来。
RememberMeServices
public interface RememberMeServices {
// 从请求中获取参数,完成自动登录
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
// 自动登录失败的回调
void loginFail(HttpServletRequest request, HttpServletResponse response);
// 自动登录成功的回调
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}
默认使用TokenBasedRememberMeService
processAutoLoginCookie方法用来验证 Cookie 中的令牌信息是否合法:
- 首先判断 cookieTokens 长度是否为3,格式不对,则直接抛出异常
- 从cookieTokens 数组中提取出第1项(过期时间),判断令牌是否过期,过期则抛出异常。
- 根据用户名(cookieTokens[1])查询出当前用户对象
- 调用 makeTokenSignature 方法生成一个签名,签名的生成过程如下:将用户名、令牌过期时间、用户密码以及 key组成一个宇符串,中间用
:
隔开,通过MD5进行加密,并将加密结果转为一个字符串返回。 - 判断第4步生成的签名和通过 Cookie 传来的签名是否相等(cookieTokens[1]),相等表示令牌合法,则直接返回用户对象,否则抛出异常.
1. onLoginSuccess回调中,首先获取用户经和密码信息,如果登录成功后用户名密码从successfulAuthentication对象中擦除,则从数据库中重新加载。
2. 计算出令牌的过期时间,令牌默认有效期是两周。
3. 根据令牌的过期时间、用户名以及用户密码,计算签名
4. 调用 setCookie 方法设置 Cookie.。参数按顺序是用户名、过期时间以及签名,在setCookie 方法中会将数组转为字符串,并进行 Base64编码后响应给前端。
总结:
RememberMe流程图
四、持久化令牌
实现一:需要手动创建数据库表结构
@EnableWebSecurity
public class SecurityConfig2 {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private DataSource dataSource;
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
public 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();
}
@Bean
public RememberMeServices rememberMeServices() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 启动时候创建表结构
tokenRepository.setCreateTableOnStartup(true);
return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),
myUserDetailsService, tokenRepository);
}
}
实现二:这种方式可以自动创建数据库表结构
@EnableWebSecurity
public class SecurityConfig2 {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private DataSource dataSource;
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
public 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();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
List<Map<String, Object>> result = jdbcTemplate.queryForList(" select 1 from information_schema.tables where table_name = 'persistent_logins' ");
// 这里一直为true在后续启动时会报错,所以我们在启动之前先查询表结构是否存在,如果存在就不创建
repository.setCreateTableOnStartup(result.isEmpty());
repository.setDataSource(dataSource);
return repository;
}
}
五、前后端分离开发记住我
cookie实现:
- 认证成功保存记住我cookie到客户端
- 只有cookie写入客户端成功才能实现自动登录功能
编写自定义的MyPersistentTokenBasedRememberMeServices
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
// 这里可以在LoginFilter中读取出来,保存到request中。
String paramValue = String.valueOf(request.getAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER));
// 也可以在这里获取
// try {
// Map<String, String> 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);
}
}
修改SecurityConfig配置
这里只展示关键部分代码,详细参考附录一说明。
/**
* @author Huathy
* @date 2023-03-06 23:07
* @description
*/
// 开启web安全
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
@Bean
public LoginVcFilter loginVcFilter() throws Exception {
log.info(" === loginFilter init ===");
LoginVcFilter loginVcFilter = new LoginVcFilter();
// 2. 指定认证处理URL
loginVcFilter.setFilterProcessesUrl("/dologin");
// 设置认证成功时使用自定义的记住我功能
loginVcFilter.setRememberMeServices(rememberMeServices());
return loginVcFilter;
}
@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
log.info(" === 替换了 loginVcFilter === ");
http.addFilterAt(loginVcFilter(), UsernamePasswordAuthenticationFilter.class);
// 开启记住我 这里是设置携带记住我cookie的处理
http.rememberMe().rememberMeServices(rememberMeServices());
return http.build();
}
@Bean
public RememberMeServices rememberMeServices() {
return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), myUserDetailsService, new InMemoryTokenRepositoryImpl());
}
}
附:
- 本文所涉及源码地址:https://gitee.com/huathy/study-all