Spring Security 认证方式的深度思考
引言
讲一下COOKIE
和SESSION
?
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 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
没有被篡改过且合法就好了。
总结
多思考,多总结。