Spring Security(学习笔记) -- 密码加密源码分析与防御计时攻击以及RememberMe案例记录!

发布于:2024-04-30 ⋅ 阅读:(31) ⋅ 点赞:(0)

重点标识

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

转一下日期,距离我现在时间,刚好两周。
在这里插入图片描述

结语

岁月如歌,且吟且唱,且珍惜!


网站公告

今日签到

点亮在社区的每一天
去签到