讲一下 COOKIE
和 SESSION
?
哔哩哔哩。。。
如果 COOKIE
被禁用了怎么办?
可以使用 Token
来代替 COOKIE
进行用户认证。
那你看,既然 Token
就能实现功能,那还要 COOKIE
干什么呢? COOKIE
存在时间这么久,肯定是有它的道理的。
想了半天,不知道。但是感觉应该是 COOKIE
有一些更加强大的功能我不知道。
说到为什么有 COOKIE
?所有的 HTTP
相关资料都是这一句话。
因为 HTTP
协议是无状态的。我不知道大家是否真的理解无状态?
SMTP
协议,先发送 HELO
用来握手;接下来进入 AUTH
阶段,验证用户名密码;然后再进行数据传输。所以双方必须要时刻记住当前连接的状态。
HTTP
协议,每个请求都是完全独立的,服务器直接处理客户端的请求,而不需要去维护连接状态那样麻烦。
无状态,是指 HTTP
协议不需要维护各种复杂的通信状态,只是简单的请求与相应,不涉及状态的变更。从而使得 HTTP
协议更加简单。
对于有状态协议而言,如果连接意外断开,那么如果连接意外断开,整个会话就会丢失,重新连接之后一般需要从头开始。对于无状态协议,即使连接断开了,会话状态也不会受到严重伤害。重新请求就是了。
无状态的缺点在于,单个请求需要将所有信息包含在一个请求中一次发送到服务端,这导致单个消息的结构比较复杂。
因为 HTTP
协议是无状态的,所以许多早期的 Web
应用面临的最大问题就是如何维持状态。
网景公司提出了 COOKIE
的概念,以解决该问题。
所以 COOKIE
并不是 HTTP
协议的标准,而是浏览器为了解决 HTTP
无状态引发的问题而提出的解决方案。
当要发送 HTTP
请求时,浏览器会先检查是否有相应的 COOKIE
,有则自动添加在 request header
的 COOKIE
字段中。
这些是浏览器自动帮助我们做的,而且每一次 HTTP
请求浏览器都会自动帮我们做。所以如果 COOKIE
中的数据不是每个请求都需要发送给服务器,那无疑增加了网络开销。
在 Chrome
中打开控制台,选择 Application/Cookies
,然后就可以看到浏览器 Cookie
存储的域,点开就是该域存储的 Cookie
。
最开始 COOKIE
和 Token
几乎是没什么差别的,解决的问题就是如果使用 COOKIE
,这个字段浏览器可以帮我们维护,如果不使用 COOKIE
就需要我们手动在发起 HTTP
请求时维护。
后来,为了防止 XSS
攻击,引入了 HttpOnly
字段。
XSS
:跨站脚本攻击。为网页植入恶意代码,使用户加载并执行攻击者恶意制造的网页程序。
假设我们自己维护的 TOKEN
,或者是没有设置 HTTP-ONLY
的 COOKIE
,我们可以通过代码访问,那恶意代码也可以,无法抵御 XSS
攻击。
通过设置 COOKIE
的 HTTP-ONLY
,通过 document.cookie
将无法再访问 COOKIE
,这样可以避免恶意代码访问 COOKIE
,提高安全性。
现在感觉再去看官方文档,之前好多看不懂的地方也能看懂了,豁然开朗。
@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
,也就是说,根据 COOKIE
找 SESSION
的过程,其实是浏览器存储了 Session
的 id
,服务器根据 id
找 SESSION
对象而已。
此处因为没有使用 Spring Session
,所以 COOKIE
名是 JSESSIONID
, JSESSIONID
是 Tomcat
创建的。 Spring Session
创建的 COOKIE
名为 SESSION
。
所以,这样设计为了方便在 Token
和 COOKIE
两种认证方式之间相互切换,反正是相同的值,底层的逻辑不用变。
当然, COOKIE
也是有它的缺点的。
COOKIE
是浏览器自动添加到 HTTP
请求中的,所以有了 CSRF
攻击。
如果想深入学习 CSRF
,请参考 聊聊CSRF 。
本图片来自博客: 浅谈CSRF攻击方式
当恶意网站请求正常服务接口的时候,浏览器检查有 COOKIE
存在,直接就把 COOKIE
带上发过去了。
在用户不知情的情况下,其他网站伪造了客户的请求,所以后台认证用户,不能单单用 COOKIE
。
这是我之前的配置,也不懂什么是 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
后,前台会有两个 COOKIE
。分别是 SESSION
和 XSRF-TOKEN
。
原理图如下:服务器将 token
发到了客户端的 COOKIE
中,这个 COOKIE
的 XSRF-TOKEN
不是别的功能,就是用于服务端将令牌发送给浏览器。
如果用户是通过我们的 Angular
应用进行访问的, Angular
默认启用 CSRF
安全,直接将 COOKIE
中的 XSRF-TOKEN
字段为我们添加到首部中,发起请求的时候, Angular
应用发送的请求中首部是带有 X-XSRF-TOKEN
的。
如果是恶意网站伪造的应用,只会有浏览器自带的 COOKIE
,就像这个 POSTMAN
一样,只带着 COOKIE
去访问,是被禁止的。
这里要注意的是, CSRF
只会防御对资源有修改的操作。
常用的 REST
规范, GET
、 POST
、 PUT
、 PATCH
、 DELETE
。只有 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
没有被篡改过且合法就好了。
这个我也进行了深入的思考,记得上次面试结束时想面试官请教问题,我们为什么要学 TCP
,学 HTTPS
签名,学习底层原理啊?主要原因是开源中间件并不能解决所有问题,有的时候需要我们自主研发。
我觉得说得太正确了,如果我们只是一个小公司 996
的程序员,我们可能遇不到并发,遇不到高可用,也遇不到分布式和微服务。但是如果访问量非常大的时候,我们不能仅仅依赖于国外的中间件。
就像 Spring Cloud
一样,我们视 Spring Cloud
为业界楷模,微服务的标杆,可是阿里的态度呢?因为国外没有双十一,没有六一八,所以阿里/京东不敢在一次这么大的业务场景去使用国外的中间件。如果扛不住访问,那损失将是数以千亿计。
秋招面试,虽然挂在了三面,但是通过和面试官的交流,受益良多。
天天写增删改查、业务,那我们可能会被一个能力强培训过几个月的实习生替代。真正能体现工程师水平的,是思考的深度。
多思考,多总结,工程师,不止于框架。