重点标识
Security的密码验证原理其实还是比较容易理解的,说白了就是密码比对。
计时攻击Security是如何防御的?
Security中的RememberMe怎么弄?
HttpSecurity的获取
在配置config配置中,我们经常要用到HttpSecurity给过滤器链进行配置,那HttpSecurity是从哪来的呢,看下源码。
大概解释一下,在HttpSecurityConfiguration这个类里面有一个httpSecurity方法,就是下面截图那个,这里面的逻辑也很简单,先是懒加载PasswordEncoder,然后看存不存在,不存在则去Spring容器看啊可能,也并没有,就createDelegatingPasswordEncoder创建一个,就是前面说的密码加密,接着new了一个默认的认证方式,就是把默认的密码加密方式装配进去,然后设置parent,authenticationManager,关闭csrf,配置logout,和我们之前在config中写的差不多。
其实就是懒加载,为null 则通过getBeanOrNull去Spring容器找。没找到,则创建一个返回,
private PasswordEncoder getPasswordEncoder() {
if (this.passwordEncoder != null) {
return this.passwordEncoder;
} else {
PasswordEncoder passwordEncoder = (PasswordEncoder)this.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 var3) {
return null;
}
}
计时攻击
我们知道,密码比对是在DaoAuthenticationProvider里面做的
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
查找数据库有没有这个用户,这个地方有意思的一点就是,自己抛了一个异常InternalAuthenticationServiceException,又自己捕获了,
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
用户名不存在被捕获,然后调用mitigateAgainstTimingAttack,就是下面那个,比对的密码,就是用户输入的,和上面那个字符串常量加密,对比,肯定不一致。
这里
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
这样做的好处就是,为了防御计时攻击,防止根据用户名解析,让攻击方分析出来,哪些用户真的存在。
简单解释一下,做密码比对,密码为空,直接报错。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
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"));
}
}
}
RememberMe
这个记录是通过消息摘要的方式做的,重启后,RememberMe依然有效。
实际上,这玩意也相当于,一种认证方式。在Authentication也有实现,与UsernamePasswordAuthenticationToken平级。
只需要在配置中配置一下,即可:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a->a.requestMatchers("/hello").authenticated()
.requestMatchers("/rm").rememberMe()
.requestMatchers("/fullAuth").fullyAuthenticated().anyRequest().authenticated()
)
.formLogin(f -> f.loginPage("/login.html").loginProcessingUrl("/login").permitAll())
.rememberMe(r ->
//这里注意,要给他一个key,不然每次重启,系统自动给一个,就无法达到重启系统后,还能通过rememberMe认证的目的了
r.key("tongzhou")
)
.csrf(c->c.disable());
return http.build();
}
在附上一个简陋的登陆界面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="POST">
<table>
<tr>
<td>用户名</td>
<td><input type="text" value="admin" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" value="123" name="password"></td>
</tr>
<tr>
<td>RememberMe</td>
<td>
<input type="checkbox" value="on" name="remember-me">
</td>
</tr>
<tr>
<td><input type="submit" value="登录"></td>
</tr>
</table>
</form>
</body>
</html>
提供三个测试接口
@RestController
public class HelloController {
/**
* 都可以访问
* @return
*/
@GetMapping("/hello")
public String hello(){
return "hello";
}
/**
*
* rememberMe登录就才可以访问
* @return
*/
@GetMapping("/rm")
public String full(){
return "rm";
}
/**
* 账号密码登录才能访问
* @return
*/
@GetMapping("/fullAuth")
public String fullAuth(){
return "fullAuth";
}
}
好了,这样就可以,简单解释一下哈,hello,只要认证就能访问,也就是说,服务重启,这个接口依然可以访问,/rm,RememberMe才能访问, fullAuth必须登录后才能访问,重启下系统,除了fullAuth,其他都可以正常访问,不用再次验证,成功!
简单看一下他的认证:
rememberMe成功后,会返回一个加密的cookie:
remember-me=YWRtaW46MTcxNTE1OTA4MDY5ODpTSEEyNTY6NzQyZjk5MjEzYTk1YmI1YzdmY2YzZDMzNTA4NWRkNWJmZGMxZDMyMjhkMjJiZjNhOGVhZmM0OGMxNzMxMTYyZQ;
解密下看看:
@Test
void test1(){
byte[] encode = Base64.getDecoder().decode("YWRtaW46MTcxNTE1OTA4MDY5ODpTSEEyNTY6NzQyZjk5MjEzYTk1YmI1YzdmY2YzZDMzNTA4NWRkNWJmZGMxZDMyMjhkMjJiZjNhOGVhZmM0OGMxNzMxMTYyZQ".getBytes());
System.out.println(new String(encode,0,encode.length));
}
结果如下:用户,时间戳,以及一个SHA256的信息摘要,
admin:1715159080698:SHA256:742f99213a95bb5c7fcf3d335085dd5bfdc1d3228d22bf3a8eafc48c1731162e
转一下日期,距离我现在时间,刚好两周。
结语
岁月如歌,且吟且唱,且珍惜!