乐趣区

Spring-Security-认证方式的深度思考

引言

讲一下 COOKIESESSION

哔哩哔哩。。。

如果 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 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 没有被篡改过且合法就好了。

这个我也进行了深入的思考,记得上次面试结束时想面试官请教问题,我们为什么要学 TCP,学HTTPS 签名,学习底层原理啊?主要原因是开源中间件并不能解决所有问题,有的时候需要我们自主研发。

我觉得说得太正确了,如果我们只是一个小公司 996 的程序员,我们可能遇不到并发,遇不到高可用,也遇不到分布式和微服务。但是如果访问量非常大的时候,我们不能仅仅依赖于国外的中间件。

就像 Spring Cloud 一样,我们视 Spring Cloud 为业界楷模,微服务的标杆,可是阿里的态度呢?因为国外没有双十一,没有六一八,所以阿里 / 京东不敢在一次这么大的业务场景去使用国外的中间件。如果扛不住访问,那损失将是数以千亿计。

总结

秋招面试,虽然挂在了三面,但是通过和面试官的交流,受益良多。

天天写增删改查、业务,那我们可能会被一个能力强培训过几个月的实习生替代。真正能体现工程师水平的,是思考的深度。

多思考,多总结,工程师,不止于框架。

退出移动版