引言

讲一下COOKIESESSION

balabala。。。

如果COOKIE被禁用了怎么办?

可以使用Token来代替COOKIE进行用户认证。

那你看,既然Token就能实现功能,那还要COOKIE干什么呢?COOKIE存在时间这么久,肯定是有它的道理的。

想了半天,不知道。

COOKIE 的起源

说到为什么有COOKIE?所有的HTTP相关资料都是这一句话。

因为HTTP协议是无状态的。我不知道大家是否真的理解无状态?

如何理解 HTTP 的无状态?

SMTP协议,先发送HELO用来握手;接下来进入AUTH阶段,验证用户名密码;然后再进行数据传输。所以双方必须要时刻记住当前连接的状态。

HTTP协议,每个请求都是完全独立的,服务器直接处理客户端的请求,而不需要去维护连接状态那样麻烦。

无状态,是指HTTP协议不需要维护各种复杂的通信状态,只是简单的请求与相应,不涉及状态的变更。从而使得HTTP协议更加简单。

无状态的优缺点

对于有状态协议而言,如果连接意外断开,那么如果连接意外断开,整个会话就会丢失,重新连接之后一般需要从头开始。对于无状态协议,即使连接断开了,会话状态也不会受到严重伤害。重新请求就是了。

无状态的缺点在于,单个请求需要将所有信息包含在一个请求中一次发送到服务端,这导致单个消息的结构比较复杂。

提出

因为HTTP协议是无状态的,所以许多早期的Web应用面临的最大问题就是如何维持状态。

网景公司提出了COOKIE的概念,以解决该问题。

所以COOKIE并不是HTTP协议的标准,而是浏览器为了解决HTTP无状态引发的问题而提出的解决方案。

工作原理

当要发送HTTP请求时,浏览器会先检查是否有相应的COOKIE,有则自动添加在request headerCOOKIE字段中。

这些是浏览器自动帮助我们做的,而且每一次HTTP请求浏览器都会自动帮我们做。所以如果COOKIE中的数据不是每个请求都需要发送给服务器,那无疑增加了网络开销。

COOKIE 的格式

Chrome中打开控制台,选择Application/Cookies,然后就可以看到浏览器Cookie存储的域,点开就是该域存储的Cookie

最开始COOKIEToken几乎是没什么差别的,解决的问题就是如果使用COOKIE,这个字段浏览器可以帮我们维护,如果不使用COOKIE就需要我们手动在发起HTTP请求时维护。

后来,为了防止XSS攻击,引入了HttpOnly字段。

HTTP-ONLY

XSS:跨站脚本攻击。为网页植入恶意代码,使用户加载并执行攻击者恶意制造的网页程序。

假设我们自己维护的TOKEN,或者是没有设置HTTP-ONLYCOOKIE,我们可以通过代码访问,那恶意代码也可以,无法抵御XSS攻击。

通过设置COOKIEHTTP-ONLY,通过document.cookie将无法再访问COOKIE,这样可以避免恶意代码访问COOKIE,提高安全性。

再看Spring Security官方文档

现在感觉再去看官方文档,之前好多看不懂的地方也能看懂了,豁然开朗。

@RequestMapping("/login")
public Map<String, String> login(HttpSession session) {
    return Collections.singletonMap("token", session.getId());
}

这是官方文章中登陆的示例代码,这其实是一个trick登陆,之前也给大家讲过。因为有Spring Security的层层拦截,所以我们能保证,如果代码执行到了login方法,那一定是合法的请求,所以login中其实没有什么认证的逻辑。

之前一直不明白为什么要把session.getId()返回给浏览器作为token,现在自己实际演练一遍明白了。

建立一个返回sessionId的空SpringBoot项目。

@RestController
@RequestMapping("session")
public class SessionController {

    private final HttpSession httpSession;

    public SessionController(HttpSession httpSession) {
        this.httpSession = httpSession;
    }

    @GetMapping
    public String session() {
        return httpSession.getId();
    }
}

我们发现sessionId其实就是COOKIE,也就是说,根据COOKIESESSION的过程,其实是浏览器存储了Sessionid,服务器根据idSESSION对象而已。

此处因为没有使用Spring Session,所以COOKIE名是JSESSIONIDJSESSIONIDTomcat创建的。Spring Session创建的COOKIE名为SESSION

所以,这样设计为了方便在TokenCOOKIE两种认证方式之间相互切换,反正是相同的值,底层的逻辑不用变。

CSRF

当然,COOKIE也是有它的缺点的。

COOKIE是浏览器自动添加到HTTP请求中的,所以有了CSRF攻击。

如果想深入学习CSRF,请参考聊聊CSRF

本图片来自博客:浅谈CSRF攻击方式

当恶意网站请求正常服务接口的时候,浏览器检查有COOKIE存在,直接就把COOKIE带上发过去了。

在用户不知情的情况下,其他网站伪造了客户的请求,所以后台认证用户,不能单单用COOKIE

这是我之前的配置,也不懂什么是CSRF啊,直接就禁用了。

Spring Security 启用 CSRF

很简单,直接.csrf()就配好了。

@Component
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 配置Spring Security
         * 最开始觉得这个挺难的,其实这个配置别把它当成代码看
         * 直接把它当成句子看,用and连接,就明白了
         * 学习了CSRF,感觉应该启用,防止跨站请求伪造
         * 前台会多存一个CSRF的认证字段
         */
        http
                // 启用CSRF
                .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 使用Basic认证方式进行验证进行验证
                .and().httpBasic()
                // 要求SpringSecurity对后台的任何请求进行认证保护
                .and().authorizeRequests().antMatchers("/host/status").permitAll().anyRequest().authenticated()
                // 关闭Security的Session,使用Spring Session
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
                // 设置frameOptions为sameOrigin,否则看不见h2控制台
                .and().headers().frameOptions().sameOrigin();
    }
}

CSRF 防御原理

启用CSRF后,前台会有两个COOKIE。分别是SESSIONXSRF-TOKEN

原理图如下:服务器将token发到了客户端的COOKIE中,这个COOKIEXSRF-TOKEN不是别的功能,就是用于服务端将令牌发送给浏览器。

如果用户是通过我们的Angular应用进行访问的,Angular默认启用CSRF安全,直接将COOKIE中的XSRF-TOKEN字段为我们添加到首部中,发起请求的时候,Angular应用发送的请求中首部是带有X-XSRF-TOKEN的。

如果是恶意网站伪造的应用,只会有浏览器自带的COOKIE,就像这个POSTMAN一样,只带着COOKIE去访问,是被禁止的。

防御哪些请求?

这里要注意的是,CSRF只会防御对资源有修改的操作。

常用的REST规范,GETPOSTPUTPATCHDELETE。只有GET是不对资源进行修改的。

所以,CSRF不能防御GET方法请求。使用GET方法时,只使用COOKIE,得到了正确的数据,说明CSRF没有对GET方法进行防御。

启用了CSRF后,就一定要遵守规范,如果非要把安全性要求极高的接口用GET方法暴露,Spring也很无奈。

单元测试

跑一遍单元测试,果然和我们预想的一样。启用CSRF后,出错的单元测试都是对资源进行修改的方法,说明我们总结的结论是正确的。

perform方法中,点一个with,调用csrf()方法即可。

Spring Boot Test默认没有这个包,需要手动引入依赖。

方法包路径:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

依赖:

<!-- Spring Security Test -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.1.6.RELEASE</version>
    <scope>test</scope>
</dependency>

学习的思考

在移动端,往往使用JWT的方式进行了用户认证。

JWT是无状态的,所以服务器端无需存储认证信息,减轻服务器的压力,只要保证签发的JWT没有被篡改过且合法就好了。

总结

多思考,多总结。

03-05 21:47