目录

🌗 实现 XSRF 防御

# 需求分析

XSRF 又名 CSRF,跨站请求伪造,它是前端常见的一种攻击方式。

XSRF 防御的手段有很多。

比如验证请求的 referer,但是 referer 也是可以伪造的;所以杜绝此类攻击的一种方式是服务器端要求每次请求都包含一个 token ,这个 token 不在前端生成,而是在我们每次访问站点的时候生成,并通过 set-cookie 的方式种到客户端,然后客户端发送请求的时候,从 cookie 中对应的字段读取出 token ,然后添加到请求 headers 中。这样服务端就可以从请求 headers 中读取这个 token 并验证,由于这个 token 是很难伪造的,所以就能区分这个请求是否是用户正常发起的。

要自动实现完成这个操作,每次发送请求的时候,从 cookie 中读取相应的 token 值,然后添加到请求 headers 中。允许用户配置 xsrfCookieNamexsrfHeaderName ,其中 xsrfCookieName 表示存储 tokencookie 的名称; xsrfHeaderName 表示请求 headerstoken 对应的 header 名称。

axios.get('/more/get', {
  xsrfCookieName: 'XSRF-TOKEN', // default
  xsrfHeaderName: 'X-XSRF-TOKEN' // default
}).then(res => {
  console.log(res)
})
1
2
3
4
5
6

可以提供 xsrfCookieNamexsrfHeaderName 的默认值,当然用户也可以根据自己的需求在请求中去配置 xsrfCookieNamexsrfHeaderName

# 代码实现

首先修改 AxiosRequestConfig 的类型定义, src/types/index.ts

export interface AxiosRequestConfig {
  url?: string
  method?: Method
  data?: any
  params?: any
  headers?: any
  responseType?: XMLHttpRequestResponseType
  timeout?: number
  transformRequest?: AxiosTransformer | AxiosTransformer[]
  transformResponse?: AxiosTransformer | AxiosTransformer[]

  [propName: string]: any

  cancelToken?: CancelToken
  withCredentials?: boolean
  xsrfCookieName?: string
  xsrfHeaderName?: string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

接着修改默认配置, src/defaults.ts

const defaults: AxiosRequestConfig = {
  // ...
  
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN'
}
1
2
3
4
5
6

接着要完成三件事:

  • 首先判断如果是 withCredentialstrue ,或者同域请求,才会在请求 headers 中添加 xsrf 相关的字段;
  • 如果判断成功,尝试从 cookie 中读取 xsrftoken 值;
  • 如果能读取到,则把它添加到请求 headers 中的 xsrf 字段中;

首先实现同域请求的判断,在 src/helpers/url.ts

interface URLOrigin {
  protocol: string
  host: string
}

const urlParsingNode = document.createElement('a')
const currentOrigin = resolveURL(window.location.href)

function resolveURL(url: string): URLOrigin {
  urlParsingNode.setAttribute('href', url)
  const { protocol, host } = urlParsingNode

  return {
    protocol,
    host
  }
}

export function isURLSameOrigin(requestURL: string): boolean {
  const parsedOrigin = resolveURL(requestURL)

  return parsedOrigin.protocol === currentOrigin.protocol && parsedOrigin.host === currentOrigin.host
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

先创建一个 a 标签的 DOM,然后设置 href 属性为传入的 URL ,然后可以获取该 DOM 的 protocolhost 。当前页面的 url 和请求的 url 都通过这种方式获取,然后对比它们的 protocolhost 即可判断是否同域。

然后实现 cookie 的读取, src/helpers/cookie.ts

const cookie = {
  read(name: string): string | null {
    const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'))
    return match ? decodeURIComponent(match[3]) : null
  }
}

export default cookie
1
2
3
4
5
6
7
8

实现 cookie 的读取,利用了 正则表达式 可以解析到 name 对应的 值。

最后实现完整的逻辑, src/core/xhr.ts

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve, reject) => {
    const {
      data = null,
      url,
      method = 'get',
      headers,
      responseType,
      timeout,
      cancelToken,
      withCredentials,
      xsrfCookieName,
      xsrfHeaderName
    } = config

  	// ...

    if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) {
      const xsrfValue = cookie.read(xsrfCookieName)
      if (xsrfValue) headers[xsrfHeaderName!] = xsrfValue
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

要同时检测是否携带 withCredentials 以及是否同域,是否存在 xsrfCookieName

# 编写测试 DEMO

examples/server.js 中添加:

app.use(express.static(__dirname, {
  setHeaders(res) {
    res.cookie('XSRF-TOKEN-D', '1234abc')
  }
}))
1
2
3
4
5

然后在 exmample/more/app.ts 中添加:

const instance = axios.create({
  xsrfCookieName: 'XSRF-TOKEN-D',
  xsrfHeaderName: 'X-XSRF-TOKEN-D'
})

instance.get('/more/get').then(res => {
  console.log(res)
})
1
2
3
4
5
6
7
8

在访问页面的时候,服务端通过 set-cookie 往客户端种了 keyXSRF-TOKEN ,值为 1234abccookie ,作为 xsrftoken 值。

然后在前端发送请求的时候,就能从 cookie 中读出 keyXSRF-TOKEN 的值,然后把它添加到 keyX-XSRF-TOKEN 的请求 headers 中。

📢 上次更新: 2022/09/02, 10:18:16