Web Authentication

Cookie、Session、LocalStorage、SessionStorage 和 IndexedDB;JWT、XSS、CSRF

Cookies
Cookies

持久化

特性 Cookie LocalStorage SessionStorage IndexedDB
数据生命周期 一般由服务端生成;前端 => js-cookie;node => cookie 不清理一直在;浏览器关闭还会保存;不支持跨浏览器 页面关闭就清理;刷新依然在;不支持跨页面交互 除非被清理,否则一直在
数据存储大小 4K 5M 5M 不限制
与服务端通信 每次都会携带在请求头中 => 影响请求性能、容易出现安全问题 不参与 不参与 不参与
特点 字符串键值对存储在本地 字符串键值对在本地存储数据 字符串键值对在本地存储数据 非关系型数据库

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的小块数据,会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

一个典型的 Cookie 包含有存储位置、过期时间、作用域、页面路径、安全标记、过期时间等属性。

Set-Cookie: name=John; Domain=example.com; Path=/; Expires=Sat, 31 Dec 2023 23:59:59 GMT; Secure; HttpOnly

当已有的 Cookie 数量达到浏览器的存储上限(通常约为20个),新的 Cookie 设置将会覆盖或丢弃旧的 Cookie。前端对 Cookie 的设置操作是无感知的,具体是后端通过在响应头中使用 Set-Cookie 字段将 Cookie 信息绑定到浏览器。之后,浏览器会自动在每次请求中携带相应的 Cookie 信息,存储在请求头的 Cookie 字段中。

  • JavaWeb 逻辑
public class HelloAdminUser extends HttpServlet {
  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie cookie = new Cookie("adminUser", "zs");
    cookie.setPath("/"); // tomcat下多应用共享
    ...
    cookie.setHttpOnly(true);
    response.addCookie(cookie);
  }
}
  • 前端封装逻辑

设置 Access-Control-Allow-Credentials: truexhr.withCredentials = true 可以实现跨域传递 Cookie,但是会有 CSRF 的风险。

// 封装 Cookie 的设置、读取与删除
// 设置Cookie
function setCookie (key, value, t) { // t设置cookie多久过期,通常设置天数
  var myTime = new Date();
  myTime.setDate(myTime.getDate() + t);
  document.cookie = key + "=" + value + ";expires=" + myTime.toUTCString();
}

// 读取Cookie
function getCookie (name) {
  var arr = document.cookie.split(';'); // 通过";"分隔cookie
  for (var i = 0 ; i < arr.length ; i++) {
    var arr1 = arr[i].split('=');
    if (arr1[0] == name) {
      return arr1[1];
    }
  }
  return ""; // 遍历完成没有找到则返回空
}

// 删除Cookie
function removeCookie (key) {
  setCookie(key, "1", -1) // 设置的value随意,主要是t的意思则是已过期,可删除
}

Session

Session 是一种通过唯一 ID 识别用户身份的机制。当执行 HttpServletRequestgetSession() 方法时,Web 容器会为每个用户创建一个 HttpSession 实例。每个 HttpSession 实例都有一个唯一的 ID,也称 SessionID,可以通过调用 HttpSessiongetId() 方法来获取。这个 Session ID 用于标识不同的用户会话,使得 Web 应用能够区分并管理不同用户的状态和数据。

@WebServlet(urlPatterns = "/login")
public class LoginServlet extends HttpServlet {
  ...
  // POST 请求时处理用户登录
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String name = req.getParameter("username");
    ...
    HttpSession session = req.getSession();
    session.setAttribute("user", name);
  }
}

Session ID 默认会经 Cookie 存放至浏览器,Cookie 的名称默认为 JSESSIONID,数值是 getId() 获得的 Session ID。

HttpSession 默认会用 Cookie 储存 Session ID,但开发者不需要去操作 Cookie 的细节,具体操作由容器自动完成。

容器储存 Session ID 的 Cookie 默认为关闭浏览器就失效,所以当重启浏览器再次请求应用时,通过 getSession() 取得的就是新的 HttpSession 实例。

尽管容器会根据请求带来的 Session ID 取得对应的 HttpSession。但 HttpSession 实例中尽量不要储存过大的数据,也应在必要时将数据移除或使其失效。

注意 session.setMaxInactiveInterval(maxInactiveSec); 是设置会话超时时间,而不是 Cookie 失效时间。

localStorage & sessionStorage

不同浏览器的本地存储数据不可以共用,即使浏览器被关闭,使用本地存储保存的数据也不会丢失,下次打开浏览器访问时仍然有效,可用于购物车或页游。

会话存储将部分数据保存在当前会话页面,页面关闭后,会话存储中的数据就会被清空。因为保存数据的生命周期较本地存储更短,所以常应用来保存 Token。

/* localStorage|sessionStorage通用方法 */
localStorage|sessionStorage.setItem('key', 'value'); // 保存数据
localStorage|sessionStorage.getItem('key'); // 获取数据
localStorage|sessionStorage.removeItem('key'); // 删除指定数据
localStorage|sessionStorage.clear(); // 删除所有数据
localStorage|sessionStorage.key(n); // 获取第 n 个数据

indexedDB

IndexedDB 是在客户端存储大量的结构化数据的 NoSQL 数据库,使用索引实现对数据的高性能搜索,至少 250MB 的存储空间弥补在 Web Storage 只能存储少量数据的缺陷(约 5MB)。

与 Local Storage 的同步操作不同,异步 IndexedDB 的设计是为了处理大量数据的读写。其内部采用对象仓库存放不限制类型(支持二进制存储)的数据,数据以键值对存储,每个数据记录都有对应的唯一主键,如果重复会抛出一个错误。支持事务但是受到同源限制,只能访问自身域下的 IndexedDB。

// 新建\打开数据库
var request = window.indexedDB.open("mydb", 1.0); // 数据库名,版本号
request.onerror = function(){ console.log("创建或打开数据库失败!"); };
request.onsuccess = function(e){ console.log("创建或打开数据库成功!"); };
var data=[{id:1001,name:"zs",age:22},{id:1002,name:"zy",age:23},{  id:1003,name:"zszy",age:24}];
// 操作数据库
// 在indexedDB中,一个对象仓库就是一张表
request.onupgradeneeded=function(e){
  var db = e.target.result; 
  // 如果数据库中不包含该对象仓库(表),则创建新的对象仓库 
  if(!db.objectStoreNames.contains("zsindexeddb")){ // 判断对象仓库(表)
    var store = db.createObjectStore("zsindexeddb",{keyPath:"id"}); // keyPath: 主键名
    for(var i = 0 ; i < data.length;i++){
      var addRequest = store.add(data[i]);
      addRequest.onerror = function(){ console.log("添加数据失败!") };
      addRequest.onsuccess = function(){ console.log("添加数据成功!") };
    }
  }
};
// 删除数据库
// 执行删除数据库操作后,如果发现数据库依旧存在,可能是因为 indexedDB 没有刷新,在【indexedDB】选项上面单击鼠标右键,点击【Refresh indexedDB】即可实现刷新
var request = window.indexedDB.deleteDatabase("mydb"(数据库名));
request.onerror = function(){ console.log("删除数据库失败!"); };
request.onsuccess = function(e){ console.log("删除数据库成功!"); };
// 凡是涉及对象仓库的增删查改,都是使用事务来处理,都是在请求对象request的onsuccess事件中操作
// 增加add
var data=[{id:1004,name:"java",age:25},{id:1005,name:"js",age:26},{id:1006,name:"c++",age:27}];
var request = window.indexedDB.open("mydb",2.0);
request.onerror = function(){ console.log('创建或打开数据库失败!'); };
request.onsuccess = function(e){
  console.log("创建或打开数据库成功!");
  var {result: db} = e.target; // 获取数据库对象IDBOpenDBRequest
  var transaction = db.transaction(["zsindexeddb"],"readwrite"); // 开启事务 (表名, mode:read|readwrite)
  var store = transaction.objectStore("zsindexeddb"); // 调用 transaction 对象的 objectStore() 方法来连接对象仓库
  for(var i = 0 ; i < data.length;i++){
    var dataRequest = store.add(data[i]);
    dataRequest.onerror = function(){ console.log("添加数据失败!"); };
    dataRequest.onsuccess = function(){ console.log("添加数据成功!"); };
  }
};
// 删除delete
var request = window.indexedDB.open("mydb",2.0);
request.onerror = function(){ console.log('创建或打开数据库失败!'); };
request.onsuccess = function(e){
  console.log("创建或打开数据库成功!");
  var {result: db} = e.target; // 获取数据库对象IDBOpenDBRequest
  var transaction = db.transaction(["zsindexeddb"],"readwrite"); // 开启事务 (表名, mode:read|readwrite)
  var store = transaction.objectStore("zsindexeddb"); // 调用 transaction 对象的 objectStore() 方法来连接对象仓库
  var dataRequest = store.delete(1006);
  dataRequest.onerror = function(){ console.log("删除数据失败!"); };
  dataRequest.onsuccess = function(){ console.log("删除数据成功!"); };
}
// 查找数据
var request = window.indexedDB.open("mydb",2.0);
request.onerror = function(){ console.log('创建或打开数据库失败!'); };
request.onsuccess = function(e){
  console.log("创建或打开数据库成功!");
  var {result: db} = e.target; // 获取数据库对象IDBOpenDBRequest
  var transaction = db.transaction(["zsindexeddb"],"readwrite"); // 开启事务 (表名, mode:read|readwrite)
  var store = transaction.objectStore("zsindexeddb"); // 调用 transaction 对象的 objectStore() 方法来连接对象仓库
  var dataRequest = store.get(1002);
  dataRequest.onerror = function(){ console.log("获取数据失败!"); };
  dataRequest.onsuccess = function(){
    if(this.result == undefined){
      console.log("没有符合条件的数据");
    } else {
      console.log("获取数据成功!", this.result);
    }
  };
}
// 改变数据
var request = window.indexedDB.open("mydb",2.0);
request.onerror = function(){ console.log('创建或打开数据库失败!'); };
request.onsuccess = function(e){
  console.log("创建或打开数据库成功!");
  var {result: db} = e.target; // 获取数据库对象IDBOpenDBRequest
  var transaction = db.transaction(["zsindexeddb"],"readwrite"); // 开启事务 (表名, mode:read|readwrite)
  var store = transaction.objectStore("zsindexeddb"); // 调用 transaction 对象的 objectStore() 方法来连接对象仓库
  var value = { id: 1006, name: "c++", age: 27 }
  var dataRequest = store.put(value);
  dataRequest.onerror = function(){ console.log("改变数据失败!"); };
  dataRequest.onsuccess = function(){
    if(this.result == undefined){
      console.log("没有符合条件的数据");
    } else {
      console.log("改变数据成功!", this.result);
    }
  };
}
// 清空数据
var request = window.indexedDB.open("mydb",2.0);
request.onerror = function(){ console.log('创建或打开数据库失败!'); };
request.onsuccess = function(e){
  console.log("创建或打开数据库成功!");
  var {result: db} = e.target; // 获取数据库对象IDBOpenDBRequest
  var transaction = db.transaction(["zsindexeddb"],"readwrite"); // 开启事务 (表名, mode:read|readwrite)
  var store = transaction.objectStore("zsindexeddb"); // 调用 transaction 对象的 objectStore() 方法来连接对象仓库
  var dataRequest = store.clear(); // 返回操作数据库的对象
  dataRequest.onerror = function(){ console.log("获取数据失败!"); };
  dataRequest.onsuccess = function(){ console.log("清空数据成功!", this.result); };
}

认证与攻击

JWT

JWT JSON Web Token 是基于 JSON 的 RFC7519 开放标准,用作在客户端和服务器之间传递安全可靠的信息,是一种流行的跨域认证解决方案。JWT 和 Token 一样都是访问资源的令牌,不同的是后者需要通过服务器数据库进行查询验证,而前者只需要在服务端进行密钥校验即可,认证成功后会发给客户端一 JSON 对象。

基于令牌的鉴权方式是从客户端通过用户与密码进行首次登录,服务端接收用户的登录请求并进行验证,登录验证成功后根据自定义规则生成令牌信息,并通过响应返回给客户端。这个令牌作为后续用户访问一些接口的凭证,后续访问会根据这个令牌判断用户是否具有权限。

Token 由头部 Header、载荷 Payload 与签名 Signature 组成,. 进行拼接。其中头部和载荷都是以 JSON 格式存放数据并进行 Base64 编码。

  • Header

头部信息主要是描述 JWT 元数据的 JSON 对象,alg 字段属性声明算法,typ 字段属性表示令牌类型,JWT 令牌统一写为 "JWT"。

{ "alg": "HS256", "typ": "JWT" } // alg => algorithm、HS256 => HMAC SHA256

因为 JWT 是字符串,所以对头部信息进行 Base64 编码后字符串如下。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 // Base64编码
  • Payload

载荷即消息体,实际存放需要传递的数据,除开 7 个官方字段外,也可以定义部分私有字段。

{
  "sub": "JWTTOKENTEST",
  "adminUser": "Okk",
  "iat": 1587517811
}

对 Payload 同样进行 Base64 编码后的字符串如下。

ewogICJzdWIiOiAiSldUVE9LRU5URVNUIiwKICAiYWRtaW5Vc2VyIjogIk9rayIsCiAgImlhdCI6IDE1ODc1MTc4MTEKfQ==
  • Signature

Signature 对头部和载荷的内容进行签名,需额外设置一个 secret 或 privatekey 对前两者算法处理。若前部分数据被篡改,但服务器加密的密钥没有泄露,那么解密得到的签名会与之前不一致。

HMACSHA256(base64Url(header) + "." + base64Url(payload), secretKey)
// config.json
{
  "secret": "some-secret-shit-goes-here",
  "refreshTokenSecret": "some-secret-refresh-token-shit",
  "port": 3000,
  "tokenLife": 900,
  "refreshTokenLife": 86400
}
const crypto = require("crypto"), jwt = require("jsonwebtoken");
let userList = [], secret = "test_token"; // 模拟数据库、指定 secret
class UserController { // 用户登录
  static async login(ctx) {
    const data = ctx.request.body;
    if (!data.name || !data.password) {
      return ctx.body = { code: "400", message: "参数不合法" }
    }
    const result = userList.find(item => item.name === data.name && item.password === crypto.createHash('md5').update(data.password).digest('hex'));
    if (result) {
      // 生成 token
      const token = jwt.sign( { name: result.name }, secret, { expiresIn: 60 * 60 } ); // 过期时间 60 * 60 s
      const refreshToken = jwt.sign(user, config.refreshTokenSecret, { expiresIn: config.refreshTokenLife});
      return ctx.body = { code: "200", message: "登录成功", data: { token }
      };
    } else {
      return ctx.body = { code: "400", message: "用户名或密码错误" };
    }
  }
}
module.exports = UserController;

Common Request Headers 是 OSS Object Storage Service 的 RESTful 接口中使用的公共请求头,这些请求头可以被所有的 OSS 请求所使用。Bearer Token

axios.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  config.headers.common['Authorization'] = 'Bearer ' + token; // 留意这里的 Authorization
  return config;
})

服务端在处理前端发送回来的 token 时,需判断 token 是否正确,若不正确则返回错误;需判断 token 是否过期,进而选择刷新 token 或返回 401 进行重新登录。

app.use((ctx, next) => {
  if (ctx.header && ctx.header.authorization) {
    const parts = ctx.header.authorization.split(' ');
    if (parts.length === 2) { // 取出 token 处理
      const scheme = parts[0], token = parts[1];
      if (/^Bearer$/i.test(scheme)) {
        try {
          jwt.verify(token, secret, { complete: true }); // jwt.verify 验证 token 是否有效
        } catch (error) {
          const newToken = refreshToken(user); // token过期 -> 生成新 token
          ctx.res.setHeader('Authorization', newToken); // 将新 token 放入 Authorization 中返回给前端
        }
      }
    }
  }
  return next().catch(err => {
    if (err.status === 401) {
      ctx.status = 401;
      ctx.body = 'Protected resource, use Authorization header to get access\n';
    } else { throw err; }});
 });
  • JWT 小结

字节占用小,便于传输;服务端无需保存会话信息,可水平扩展;一处生成能多处使用,可在分布式系统中,解决单点登录的问题;防护 CSRF 攻击。

Token 一旦生成就无法在过期前终止。JWT 默认不加密,安全方面应考虑 HTTPS 协议传输;载荷部分是简单编码,只能存储非敏感信息;密钥具有安全性要求。

Cross-site scripting XSS

跨站脚本攻击是向目标网站注入恶意代码,并利用脚本的执行来获取用户的敏感信息 (Cookie、SessionID)。

  • Stored XSS(存储型|持久型 XSS)

恶意代码会被提交到受攻击网站的数据库,后续从数据库读取数据时会被动的将攻击脚本拼接在响应内容中返回给浏览器解析执行。常见于带有渲染数据的位置,如论坛贴文、商品评论、用户私信等。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>XSS-demo</title>
  </head>
  <body>
    <div>
      <input type="text" placeholder="用户名" class="username" />
      <br />
      <input type="password" placeholder="密码" class="password" />
      <br />
      <br />
      <button class="btn">登陆</button>
    </div>
  </body>
  <script>
    document.querySelector(".btn").onclick = function () {
      var username = document.querySelector(".username").value;
      var password = document.querySelector(".password").value;
      fetch("http://localhost:3000/login", {
        method: "POST",
        body: JSON.stringify({
          username,
          password,
        }),
        headers: {
          "Content-Type": "application/json",
        },
        mode: "cors",
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (res) {
          alert(res.msg);
          window.location.href = "http://localhost:3000/home";
        })
        .catch((err) => {
          message.error(`本地测试错误 ${err.message}`);
          console.error("本地测试错误", err);
        });
    };
  </script>
</html>
const Koa = require("koa");
const app = new Koa();
const route = require('koa-route');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');

let currentUserName = ''; // 模拟数据库操作的数据
app.use(bodyParser()); // 处理post请求的参数
const login = ctx => {
  const user_name = ctx.request.body.username;
  currentUserName = user_name;
  ctx.response.body = { msg: '登陆成功' };
}
const home = ctx => { ctx.body = currentUserName; }
app.use(cors());
app.use(route.post('/login', login));
app.use(route.get('/home', home));
app.listen(3000, () => { console.log('启动成功'); });
<!-- 输入内容测试 -->
<script>alert("Stored XSS")</script>
  • Reflected XSS(反射型 XSS)

将恶意代码作为请求的组成部分提交给服务端进行处理,最终不安全的脚本会随着响应内容传回给浏览器执行。和存储型相比,反射型不入侵数据库,多见于带传递参数功能的搜索和跳转漏洞。

const Koa = require("koa");
const app = new Koa();
app.use(async ctx => { ctx.body = ctx.request.query.user_name; })
app.listen(3000, () => { console.log('启动成功'); }); // url 输入 http://localhost:3000/?user_name=<script>alert("okk")</script>
  • DOM based XSS(DOM 型 XSS)

JavaScript 以不安全的方式对不受信任的数据来源进行处理,导致将恶意代码直接写回 DOM。和反射型不同,DOM 型的恶意源与接收槽都位于浏览器之中。

Source: It is a Javascript property that accepts data which is controlled by the attacker is known as the source.
Sink: A sink is a potentially dangerous JavaScript function or DOM object that can cause undesirable effects if attacker-controlled data is passed to it.

// Source
document.URL、document.documentURI、document.URLUnencoded、document.baseURI、location、document.cookie、document.referrer、window.name、history.pushState、history.replaceState、localStorage、sessionStorage
// Sink
document.write()、window.location、document.cookie、eval()、document.domain、WebSocket()、element.src、postMessage()、setRequestHeader()、FileReader.readAsText()、ExecuteSql()、sessionStorage.setItem()、document.evaluate()、JSON.parse()、element.setAttribute()、RegExp()
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DOM based XSS Test</title>
  </head>
  <body>
    <div class="login-wrap">
      <input type="text" placeholder="alert('okk') or '' onclick=alert('哈哈,你被攻击了')" class="inputxsstest" style="width:50%;"/>
      <br />
      <br />
      <button class="btn">提交</button>
      <div class="content"></div>
      <!--         <div class="contentx"></div> -->
    </div>
  </body>
  <script>
    var btn = document.querySelector(".btn");
    var content = document.querySelector(".content");
    // var contentx = document.querySelector('.contentx');

    btn.onclick = function () {
      var url = document.querySelector(".inputxsstest").value;
      content.innerHTML = `<a href=${url}>请确认跳转的 URL => ${url}</a>`;
      eval(`${url}`);
    };
  </script>
</html>
  • 防御手段

对输入格式进行检查,将内容的标签进行转义或过滤;设置 HttpOnly 从而防止 JS 获取 Cookie;开启白名单 CSP Content Security Policy。相关库 => XSSCSP

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}
// 全局设置Session-Cookie相交互部分属性
@WebListener
public class SessionCookieInitialization implements ServletContextListener {
    private static final Log log = LogFactory.getLog(SessionCookieInitialization.class);
    public void contextInitialized(ServletContextEvent sce) {
        log.info("now init the Session Cookie");
        ServletContext servletContext = sce.getServletContext();
        SessionCookieConfig sessionCookie = servletContext.getSessionCookieConfig();
        sessionCookie.setName("YONGBOYID");
        sessionCookie.setPath(servletContext.getContextPath());
        sessionCookie.setHttpOnly(true);
        sessionCookie.setSecure(false);
        log.info("name : " + sessionCookie.getName() + "\n" + "domain:"
        + sessionCookie.getDomain() + "\npath:"
        + sessionCookie.getPath() + "\nage:"
        + sessionCookie.getMaxAge());
        log.info("isHttpOnly : " + sessionCookie.isHttpOnly());
        log.info("isSecure : " + sessionCookie.isSecure());
    }
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("the context is destroyed !");
    }
}

Cross-site request forgery CSRF

跨站请求伪造是攻击者冒充受害者向服务器发送非预期的请求。利用受害者在被攻击网站已经获取的凭证,绕过后台的用户验证,以达到冒充用户对被攻击的网站执行某项操作的目的。实现 RESTFUL API 以及添加安全令牌可防止此类攻击方式。

  • 常见的 CSRF 攻击类型 => 图片、表单、链接
<!-- GET 类型的 CSRF -->
<img src="http://bank.example/withdraw?amount=10000&for=hacker" /> 
<!-- POST 类型的 CSRF -->
<form action="http://bank.example/withdraw" method=POST>
  <input type="hidden" name="account" value="victim" />
  <input type="hidden" name="amount" value="10000" />
  <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script> 
<!-- 链接类型的 CSRF -->
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank"> 重磅消息!!!<a/>
  • 防御手段

通过验证码的方式强制用户先进行交互,待确认身份后再完成后续的操作;服务端据 Request Headers 中 Origin 或 Referer 识别请求来源;利用 Response Headers 里 SameSite 属性限制 Cookie 是否仅限于第一方或同一站点上下文使用;Token。

npm 验证码相关包 => puppeteer-extra-plugin-recaptchasvg-captcha

@RestController
@RequestMapping(value = "/api", method = RequestMethod.POST)
public class DemoController {    
  @CrossOrigin(origins = "*")
  @RequestMapping(value = "/get")
  public String get() {...}
}
public class AccessControlAllowOriginFilter implements Filter {
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    HttpServletResponse response = (HttpServletResponse) res;
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
    response.setHeader("Access-Control-Allow-Credentials", "true");
    chain.doFilter(req, response);
  } 
  public void init(FilterConfig filterConfig) {} 
  public void destroy() {} 
}
public class RequestLimitReferer extends HttpServlet {
  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String referer = request.getHeader("referer");
	if (referer == null || !referer.startsWith("http://localhost")) {
      response.sendRedirect("error.index");
	  return;
    }
    String msg = "Okk";
    response.getWriter().write(msg);
  }
  public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    doGet(request, response);
  }
}
public class SameSiteFilter implements javax.servlet.Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {}
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    chain.doFilter(request, response);
    addSameSiteAttribute((HttpServletResponse) response); // add SameSite=strict cookie attribute
  }
  private void addSameSiteAttribute(HttpServletResponse response) {
    Collection<String> headers = response.getHeaders("Set-Cookie");
    boolean firstHeader = true;
    for (String header : headers) {  
      if (firstHeader) {
        response.setHeader("Set-Cookie", String.format("%s; %s", header, "SameSite=Strict"));
        firstHeader = false;
        continue;
      }
      response.addHeader("Set-Cookie", String.format("%s; %s", header, "SameSite=Strict"));
      }
  }
  @Override
  public void destroy() {}
}
Cookie -- JavaScript 标准参考教程(alpha)
JSON Web Token 入门教程 - 阮一峰的网络日志
Auth0 | JWT Handbook
Ever wondered how JWT came to be and what problems it was designed to tackle?Are you curious about the plethora of algorithms available for signing...

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!