应对浏览器Cookie新属性sameSite的临门一脚

如果使用新版浏览器,在登录时,控制台会打印一个新的警告(之前没有遇到过):

image.png

文本为:

某些 Cookie 滥用推荐的“sameSite“属性 由于 Cookie _jfinal_captcha”的“sameSite”属性设置为“none”,但缺少“secure”属性,此 Cookie 未来将被拒绝。 若要了解“sameSite“的更多信息,请参阅:https://developer.mozilla.org/docs/Web/HTTP/Cookies

除了对验证码的警告,对doLogin中的cookie(jfinal-club里面key=jfinalId)也有一个警告。

该警告暂时不影响项目的进行。但是本着理科男负责任的态度和较真的精(mao)神(bing),作了反馈,并抽空断断续续研究了几天。

反馈地址https://jfinal.com/feedback/7348

1、首先,名词解释

1.1 先搜一下

搜了一下,是Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。

以前,如果SameSite属性没有设置,或者没有得到运行浏览器的支持,那么它的行为等同于None,Cookies会被包含在任何请求中——包括跨站请求。
但是,在新版本的浏览器中,SameSite的默认属性是SameSite=Lax。换句话说,当Cookie没有设置SameSite属性时,将会视作SameSite属性被设置为Lax——这意味着Cookies将会在当前用户使用时被自动发送。如果想要指定Cookies在同站、跨站请求都被发送,那么需要明确指定SameSite为None

若要了解“sameSite“的更多信息,请参阅:https://developer.mozilla.org/docs/Web/HTTP/Cookies

简而言之,如果没有为Cookie设置sameSite属性,那么默认是None。但是浏览规定,None的情况下,你必须设置Secure属性为真

而目前最适合的设置为Lax

1.2、后台语言的支持程度

目前还没有哪个后台语言的 API 支持了 SameSite 属性,比如 php 里的 setcookie 函数,或者 java 里的 java.net.HttpCookie 类,如果你想使用 SameSite,需要使用更底层的 API 直接修改 Set-Cookie 响应头。Node.js 本来就没有专门设置 cookie 的 API,只有通用的 setHeader 方法,不过 Node.js 的框架 Express 已经支持了 SameSite。

2、踩过的坑

坑1、不能用HttpServletResponse的addCookie方式增加sameSite属性。

1.2里面已经说过了,javax.servlet.http.Cookie里面没这个属性,你设置不进去。好吧,继承一个,增加属性,说干就干。

但是增加的属性,用HttpServletResponse的addCookie方法是加不进去的,里面是安全拷贝。

坑2、无法用全局Handler获取response里面的cookie来改造

原因有2:

  1. HttpServletResponse不允许获取cookie,更无法修改cookie内容

  2. HttpServletResponse中,getHeaders只对setHeader(和addHeader)的内容起效,无法获取Set-Cookie内容。因此无法全局判断修改。

坑3、直接setCookie("myCookie", "myValue; saveSite=Lax")这种方式无效

具体看反馈内容。

3、别整没用的,直接上代码

3.1、没错,继承一个Cookie,命名为NewCookie,增加sameSite属性

package xxx.xxx.common.controller;

import javax.servlet.http.Cookie;

/**
 * @author himans 2020-6-5
 */
public class NewCookie extends Cookie {
   private String sameSite;
   public final static String LAX = "Lax";
   public final static String NONE = "None";
   public final static String STRICT = "Strict";
   public final static String SET_COOKIE = "Set-Cookie";

   public NewCookie(String name, String value) {
      super(name, value);
   }

   public String getSameSite() {
      return sameSite;
   }

   public NewCookie setSameSite(String sameSite) {
      this.sameSite = sameSite;
      if (NONE.equals(sameSite)) {
         setSecure(true);
      }
      return this;
   }

   @Override
   public String toString() {
      // 参照Controller中的doSetCookie代码
      StringBuilder sb = new StringBuilder(getName()).append("=").append(getValue());
      sb.append(";Path=").append(getPath()==null?"/":getPath());
      if (getDomain() != null) {
         sb.append(";Domain=").append(getDomain());
      }
      if (isHttpOnly()) {
         sb.append(";HttpOnly");
      }
      sb.append(";Max-Age=").append(getMaxAge());
      // 默认Lax
      sb.append(";SameSite=").append(getSameSite()==null?"Lax":getSameSite());
      if (getSecure()) {
         sb.append(";Secure");
      }
      if (getComment() != null) {
         sb.append(";Comment=").append(getComment());
      }

      return sb.toString();
   }
}

3.2、修改LoginController中的doLogin代码:

核心代码,13-18行:

/**
 * 登录
 */
@Before(LoginValidator.class)
public void doLogin() {
    boolean keepLogin = getParaToBoolean("keepLogin", false);
    String loginIp = IpKit.getRealIp(getRequest());
    Ret ret = srv.login(getPara("userName"), getPara("password"), keepLogin, loginIp);
    if (ret.isOk()) {
        String sessionId = ret.getStr(LoginService.COOKIE_SESSION_NAME);
        int maxAgeInSeconds = ret.getInt("maxAgeInSeconds");
        // setCookie(LoginService.COOKIE_SESSION_NAME, sessionId, maxAgeInSeconds, true);
        // 核心代码
        NewCookie cookie = new NewCookie(LoginService.COOKIE_SESSION_NAME, sessionId);
        cookie.setMaxAge(maxAgeInSeconds);
        cookie.setHttpOnly(true);
        getResponse().addHeader(NewCookie.SET_COOKIE, cookie.toString());
        setAttr(LoginService.ACCT_ATTR, ret.get(LoginService.ACCT_ATTR));
        // 如果 returnUrl 存在则跳过去,否则跳去首页
        ret.set("returnUrl", getPara("returnUrl", "/"));
    }
    renderJson(ret);
}

注意18行,前提是不能有重复的cookie,若有Controller.setCookie相同的key(如第12行),该代码(12行)要去掉。

3.3、设置SameSite其它属性:

cookie.setSameSite(NewCookie.STRICT);

cookie.setSameSite(NewCookie.NONE).setSecure(true);


4、其它

这里仅展示了为doLogin()中cookie增加SameSite属性的代码。

验证码生成里面的Cookie,只要从Jfianl继承一个新的CaptchaRender来进行render即可。此处不再赘述。

评论区

杜福忠

2020-06-05 18:45

赞加收藏~
本着理科男负责任的态度和较真的精(mao)神(bing),反馈下,楼主把反馈地址后面那连接贴错了,写了两个HTTP,只能点击 “反馈地址”

山东小木

2020-06-05 19:07

有代码参考 建议JFinal也加入内置方案

himans

2020-06-07 09:37