添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

授权与验证有一大堆的相关的安全知识

1.1 登录态

HTTP是无状态协议,无法有效地追踪用户的状态,无法有效地根据不同用户展示不同的页面。后来HTTP发展了Cookie和Session来解决这个问题。

1.1.1 Session

这是一个普通的表单登录

登录成功后,服务器返回一个Set-Cookie字段,关键在于JSESSIONID的这个Cookie

登录成功以后,之后的所有页面请求都会带有这个JSESSIONID的Cookie,服务器就能根据这个Cookie知道是哪个用户在访问当前页面了。注意,在上一次返回了JSESSIONID以后,下一次的页面就不再返回JSESSIONID了。

Set-Cookie: JSESSIONID=2C8346A1EBD87CB442F89DCD1E334BFA; Domain=test.com; Path=/; HttpOnly

注意,这个JSESSIONID的写法,是没有过期时间的。这种没有过期时间的Cookie称为Session,浏览器会在它退出的时候,自动删除这个Cookie。所以,我们下次登录的时候,服务器依然不知道我们是谁,就会要求我们再次输入账号与密码。

我们平时登录的时候,都会看到一个“记住我”的按钮。点击以后,下次退出浏览器以后再登录,服务器也会知道我们是谁,我们不需要再次输入账号与密码。

Set-Cookie: JSESSIONID=2C8346A1EBD87CB442F89DCD1E334BFA; Domain=test.com; Max-Age=3600; Expires=Wed, 02-Jun-2021 11:48:58 GMT; Path=/; HttpOnly

一个直观的做法,是直接给JSESSIONID赋予过期时间,这样退出浏览器以后再次登录,浏览器依然会发送这个JSESSIONID的值。

但是,这样做会有个问题。服务器无法区分,当前用户是输入账号密码授权进来的,还是退出浏览器以后重新进来的。两者的安全性明显不同,我们需要对不同授权方式进来的用户给与不同的权限。

1.1.3 RememberMe

SpringSecurity的做法是,做两个Cookie,JSESSIONID依然是每次浏览器范围内的一个Cookie,然后有一个长期的remember-me的Cookie。

Set-Cookie: remember-me=ZHBlYlB5cEEzQnJScXBZYmlPcnJydyUzRCUzRDpETHBQMFVLRE91TWREU2phcmNOQzV3JTNEJTNE; Max-Age=3600; Expires=Wed, 02-Jun-2021 11:48:58 GMT; Domain=test.com; Path=/; HttpOnly

注意,remember-me的Cookie是有过期时间的,在退出浏览器以后,remember-me的Cookie不会自动删除。

那么,下次退出浏览器重新进入以后,JSESSIONID就没有了,但是remember-me的Cookie依然会继续发送给服务器

服务器就在验证了remember-me的Cookie的合法性以后,会续期remember-me的Cookie,并且分配新的JSESSIONID。这个过程用户是没有感知的,他看到的是退出浏览器重新进入后依然保持了登录态。同时,服务器知道这个用户是从remember-me进来的,而不是通过输入账号密码进来的。

1.1.4 展开

其实JSESSIONID与remember-me的关系就像,AccessToken与RefreshToken的关系。AccessToken代表你的凭证,RefreshToken是可以后备保存来刷新凭证。AccessToken的有效期比较短,所以即使被盗也是影响有限,RefreshToken是存放在本地的Token,很少在网络上传输的,安全性会更强。

但是由于浏览器的关系,remember-me的Cookie是每次都会在网络上传输,它的安全性相比RefreshToken就差很多了。

1.2 会话锁定攻击

会话固定攻击,看 这里

会话固定攻击常用于在Url中传递JSESSIONID。

  • 攻击者自己登录网站,记下自己的JSESSIONID
  • 然后构造一个新的url,url的参数上附带自己的JSESSIONID,把这个url发送给别人
  • 被攻击者点击url以后登录,登录态被记录在当前的JSESSIONID
  • 攻击者就能沿用同一个JSESSIONID窃取了别人的登录态
  • 因此,我们的防御方式是:

  • 关闭透明Session,禁止在Url上附带JSESSIONID
  • 每次登录前后,JSESSIONID都需要重新生成
  • JSESSIONID需要以尽量随机的,不易被猜测的方式生成。
  • 1.3 CSRF攻击与HttpOnly

    1.3.1 原因

    CSRF攻击,看 这里

    他的攻击在于,在A网站,可以发送到B网站的跨域请求,但是这个请求只能发送,不能获取结果的。在普通的浏览器上,跨域请求都会附带跨域的Cookie,例如A网站请求B网站的请求时,该请求附带B网站的Cookie,并且会被B网站的服务器处理。浏览器仅仅是不允许A网站读取该跨域请求的返回结果而已。

    请求会到达B网站服务器

    但是,A网站在请求完毕后,无法读取请求的返回结果,浏览器会报出statusCode为0的错误。

    但是,尽管如此,该跨域请求确实附带了B网站的Cookie,也被B网站的服务器处理了,这样最终导致CSRF攻击。你想想你在一个A网站浏览的时候,发现自己登录过的其他银行网站被突然转账了是一件多可怕的事情。

    1.3.2 Origin防御

    一个简单的防御是,服务器B检查,请求的来源,Origin与Referer是不是自己的网站,不是的话就直接拒绝。但是这个方法比较少用,因为Referer在不同浏览器上的实现不同,而且,Referer涉及到用户隐私,用户可以在浏览器中设置请求中不提供Referer属性。

    1.3.3 Session防御

    CSRF攻击与普通请求的关键在于,CSRF攻击总是跨域的。那么跨域请求与普通请求的最大区别在于:

  • 跨域请求无法读取请求的返回值
  • 跨域请求无法在javascript脚本读取跨域的Cookie
  • 因此,我们根据这两个不同的构造出CSRF的防御措施。

    CORS机制

    1.4.1 前端CORS

    SameSite的字段。

    SameSite字段的出发点是站在浏览器的角度来避免CSRF攻击,在Chrome新版本上,所有Cookie在没有设置SameSite字段的情况下,就会默认SameSite=Lax的值。SameSite的意义是,当用户处于A域名网站的时候,触发对B域名的跨域请求时,浏览器不会附带B域名的Cookie,毕竟CSRF攻击就是因为跨域请求时附带了Cookie导致的。

    但是,在特殊的场景下,例如,我们在www.abc.com的网站,对api.abc.com的域名进行请求时,如果浏览器不对api.abc.com的域名附带Cookie,就会导致服务器无法区分请求者是哪个用户。换句话说,CORS仅仅是保证了我们可以在跨域的情况下获取请求的回复,但无法保证我们在跨域的情况需要附带Cookie的问题。

    1.5.1 根域名同域

    如果在client.test.com网站请求对server.test.com的域名,由于两者都是同一个根域名test.com。那么解决方法就可以很简单,让server.test.com的回复,让Cookie设置Domain为test.com。

    那么虽然这个server.test.com的请求是跨域请求,但Cookie不是跨域的,依然会被附带在server.test.com的请求上,从而绕过了SameSite的限制。

    1.5.2 根域名不同域

    但是,如果我们在testclient.com网站请求对testserver.com的域名,两者是不属于同一个根域名的,这个Cookie就会因为默认的SameSite设置被拦截,无法在跨域请求中附带出去。所以,这个时候,我们只能对JESESSIONID进行特殊设置,将SameSite设置为None,并且Secure也要打开。这个时候,JESESSIONID就会在下次的跨域请求附带上了。

    1.5.3 根域名不同域的CSRF防御

    当然,JESSIONID没有了SameSite的保护,那么任何对testserver.com请求,即使不是CORS请求,也会附带上这个JESSIONID,这显然会导致CSRF攻击。

    我们之前讨论的CSRF防御都是基于,网站域名与API域名都是同域的情况,而目前是不同域的情况,该如何防御CSRF攻击呢?

    解决方法是让API请求从接口处返回CSRFToken

    然后让这个CSRFToken附带在Header上来提交。我们模拟一下情况:

  • 对于可信网站,由于网站能通过CORS验证,所以,他能在HTTP回复获取到这个CSRFToken,因此请求成功。
  • 对于不可信网站,当它发出CORS请求时,由于它不能通过CORS验证,所以,他不能在HTTP回复获取到这个CSRFToken,因此请求失败。如果它直接发非CORS请求,它的Header没有CSRFToken,所以请求也会失败。
  • 另外一种情况,我们用1.3.4的Cookie防御方式,模拟一下情况:

  • 对于可信网站,CSRFToken通过Cookie返回过来,但是这个Cookie是在testserver.com域名下的,前端js在testclient.com域名下的,无法直接读取到这个Cookie,更无法附带在请求的Header上,所以,请求失败。
  • 1.6 单点登录

    单点登录,注意与CORS的区分。CORS解决的是跨域请求的问题,Cookie是依然是跨域上。单点登录是,在A域名上登录,但是在B域名下可以感知到这个登录态。

    其实,CAS单点登录的原理简单。它的方式是在A域名登录后,A域名跳转到B域名的固定网址上,并附带着一个特殊的code字段。B域名服务器根据这个code字段到A域名服务器查询登录态和用户信息,然后写入到自己的Cookie和Session上就可以完成自己域名下登录态的写入。

    这个过程其实和OAuth是十分相似的。

    2 表单登录

    代码在这里

    2.1 UserDetail

    这里

    这里

    这里这里

    4.4 集群会话

    这里

    EasyCaptcha推荐使用Redis作为存储答案的方式,但是要注意,该代码忘了清空答案。

    6 CSRF

    代码在这里

    这里

    我们设计一个在client.test.com网站向server.test.com域名的跨域登录请求。

    7.1 DNS

    127.0.0.1       client.test.com
    127.0.0.1       server.test.com

    编辑/etc/hosts,加入这两条

    7.2 nginx

    server{
        listen 80;
        server_name server.test.com;
        location / {
            proxy_http_version 1.1;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Connection "";
            proxy_pass http://localhost:9595;
    

    加入TestServer配置

    server{
            listen 80;
            server_name client.test.com;
            location / {
                    proxy_http_version 1.1;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header Connection "";
                    proxy_pass http://localhost:9596;
    

    加入TestClient配置

    7.3 客户端CORS

    这里

    我们设计一个在testclient.com网站向testserver.com域名的跨域登录请求。两个是在不同根域下面的。

    8.1 DNS

    127.0.0.1   testclient.com
    127.0.0.1   testserver.com

    在/etc/hosts中加入以上dns

    8.2 nginx

    server{
        listen 443 ssl;
        server_name testclient.com;
        ssl_certificate /usr/local/etc/nginx/conf/server.crt;
        ssl_certificate_key /usr/local/etc/nginx/conf/server.key;
        ssl_session_timeout 5m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        location / {
            proxy_http_version 1.1;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Connection "";
            proxy_pass http://localhost:9596;
    

    加入TestClientTLS的配置,注意,自己生成一个本地证书。

    server{
        listen 443 ssl;
        server_name testserver.com;
        ssl_certificate /usr/local/etc/nginx/conf/server.crt;
        ssl_certificate_key /usr/local/etc/nginx/conf/server.key;
        ssl_session_timeout 5m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        location / {
            proxy_http_version 1.1;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Connection "";
            proxy_pass http://localhost:9595;
    

    加入TestServerTLS的配置,注意,自己生成一个本地证书。

    8.3 客户端cors

    这里

    Spring Security要实现多租户需要支持:

  • 在表单登录和Remember-Me登录以后,需要将当前的租户ID写入Cookie和UserDetail,写入Cookie的目的是为了在所有API上自动带上租户ID,方便网关路由。写入UserDetail的目的是,Session的租户ID更加安全可靠,是与Cookie的租户ID相比较以确定无篡改。
  • 拉取和校验用户信息,以及Remember-me的持久化令牌,都需要设置租户ID,然后才去拉数据库
  • 在Session并发控制的时候,需要根据userName和tenantId的组合来确定当前有多少个session。而不是仅仅通过userName来判断。
  • Http的拦截器上,校验UserDetail上的tenantId,和cookie上的相比较,来确定是否合法
  • 12.1 登录后写入Cookie

    这里

    13.1 Session过期时间

  • fishedee
  • 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!
  • 2021-05-30-《有效的单元测试》读书笔记
  • 2021-05-29-SpringBoot的经验汇总
  • 2021-06-13-Flyway工具经验汇总
  • 2021-05-14-《Hibernate实战第二版》读书笔记
  • 2021-06-17-Java性能分析工具
  •