🌗 实现 XSRF 防御
# 需求分析
XSRF 又名 CSRF,跨站请求伪造,它是前端常见的一种攻击方式。
XSRF 防御的手段有很多。
比如验证请求的 referer,但是
referer
也是可以伪造的;所以杜绝此类攻击的一种方式是服务器端要求每次请求都包含一个token
,这个token
不在前端生成,而是在我们每次访问站点的时候生成,并通过set-cookie
的方式种到客户端,然后客户端发送请求的时候,从cookie
中对应的字段读取出token
,然后添加到请求headers
中。这样服务端就可以从请求headers
中读取这个token
并验证,由于这个token
是很难伪造的,所以就能区分这个请求是否是用户正常发起的。
要自动实现完成这个操作,每次发送请求的时候,从 cookie
中读取相应的 token
值,然后添加到请求 headers
中。允许用户配置 xsrfCookieName
和 xsrfHeaderName
,其中 xsrfCookieName
表示存储 token
的 cookie
的名称; xsrfHeaderName
表示请求 headers
中 token
对应的 header
名称。
axios.get('/more/get', {
xsrfCookieName: 'XSRF-TOKEN', // default
xsrfHeaderName: 'X-XSRF-TOKEN' // default
}).then(res => {
console.log(res)
})
2
3
4
5
6
可以提供
xsrfCookieName
和xsrfHeaderName
的默认值,当然用户也可以根据自己的需求在请求中去配置xsrfCookieName
和xsrfHeaderName
。
# 代码实现
首先修改 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
}
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'
}
2
3
4
5
6
接着要完成三件事:
- 首先判断如果是
withCredentials
为true
,或者同域请求,才会在请求headers
中添加xsrf
相关的字段; - 如果判断成功,尝试从
cookie
中读取xsrf
的token
值; - 如果能读取到,则把它添加到请求
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
}
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 的protocol
、host
。当前页面的url
和请求的url
都通过这种方式获取,然后对比它们的protocol
和host
即可判断是否同域。
然后实现 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
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
}
}
}
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')
}
}))
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)
})
2
3
4
5
6
7
8
在访问页面的时候,服务端通过
set-cookie
往客户端种了key
为XSRF-TOKEN
,值为1234abc
的cookie
,作为xsrf
的token
值。然后在前端发送请求的时候,就能从 cookie 中读出
key
为XSRF-TOKEN
的值,然后把它添加到key
为X-XSRF-TOKEN
的请求headers
中。