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