CSRF驗證,搭配Angular Interceptor
CSRF驗證會使用到兩個Token
- Cookie Token
- Form Token
透過AJAX參數方式傳遞Token
如果你是透過AJAX呼叫取得資料的話,由於不會有form表單。所以我們先自己在cshtml裡面插入一個空的form表單,並加上Razor語法@Html.AntiForgeryToken()
<!--index.cshtml-->
<form id="myForm">
@Html.AntiForgeryToken()
</form>
執行網頁後,AntiForgeryToken()
會自動幫你產生一個hidden input存放Form Token
接著把Token存放到LocalStorage(要存在Session也可以)。
var antiForgeryToken = document.querySelector('input[name="__RequestVerificationToken"]').value;
localStorage.setItem('antiForgeryToken', antiForgeryToken);//由form #myForm取得後端產出的token
在AJAX呼叫時,在data參數裡面加上 __RequestVerificationToken: localStorage.getItem('antiForgeryToken')把Form Token傳給後端API
$.ajax({
url: url,
data: { mID: id, __RequestVerificationToken: localStorage.getItem('antiForgeryToken') },
type: "POST",
dataType: 'json'
})
後端Controller記得加上ValidateAntiForgeryToken Attribute
[ValidateAntiForgeryToken]
public ActionResult Test(string mID)
{
...
}
先前有提到,後端驗證時需要兩個Token(Form Token. Cookie Token),Form Token是我們自己透過參數傳入的,Cookie Token則是由@Html.AntiForgeryToken()
幫我們產出並加入至Cookie。有了這兩個Token,後端通過CSRF驗證後,就能成功取得回應了.
透過Header方式傳遞Token
上面方法的Token是透過AJAX的參數來傳遞,也是可以把Token存放在header裡面,並在MVC後端新增一個繼承ActionFilterAttribute的AntiForgeryTokenHeaderFilter
的類別來取得Token並執行驗證。
清除Cookie裡的__RequestVerificationToken
首先,先清除之前做測試時所產生的Cookie Token,避免後續一直取到舊的Cookie Token,導致驗證失敗
產生Token
<!--index.cshtml-->
@functions{
//產出cookieToken & formToken
public string TokenHeaderValue()
{
AntiForgery.GetTokens(null, out var cookieToken, out var formToken);
return $"{cookieToken}:{formToken}";
}
}
//把token存放到hidden input裡
@Html.Hidden("__RequestVerificationToken", TokenHeaderValue(), new {id= "request_verification_token" })
var antiForgeryToken = document.querySelector('input[name="__RequestVerificationToken"]').value;
//由form #myForm取得後端產出的token
localStorage.setItem('antiForgeryToken', antiForgeryToken);
把token加到Request的header中
$.ajax({
url: url,
data: { mID: id },
type: "POST",
dataType: 'json',
beforeSend: (function (xhr) {
xhr.setRequestHeader('__RequestVerificationToken', localStorage.getItem('antiForgeryToken')); //add csrf token to header.
})
})
Angular add interceptor
新增AntiforgeryTokenHeaderFilter
/*
* Ref:
* https://learn.microsoft.com/zh-tw/aspnet/web-api/overview/security/preventing-cross-site-request-forgery-csrf-attacks
* https://kevintsengtw.blogspot.com/2013/09/aspnet-mvc-csrf-ajax-antiforgerytoken.html
*
*直接從request header取得CSRF Token。不必在前端api加上參數[__RequestVerificationToken]傳遞token
*/
public class AntiForgeryTokenHeaderFilter: ActionFilterAttribute
{
private readonly bool _requireToken;
public AntiForgeryTokenHeaderFilter(bool requireToken = true)
{
_requireToken = requireToken;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (_requireToken)
{
var request = filterContext.HttpContext.Request;
var tokenHeaders = request.Headers.GetValues("__RequestVerificationToken").FirstOrDefault();
var cookieToken = "";
var formToken = "";
var aryAntiForgeryToken = string.IsNullOrEmpty(tokenHeaders) ? new string[] {} : tokenHeaders.Split(':');
if (aryAntiForgeryToken.Length == 2)
{
cookieToken = aryAntiForgeryToken[0];
formToken = aryAntiForgeryToken[1];
}
else
{
if (string.IsNullOrEmpty(cookieToken))
{
throw new HttpAntiForgeryException("Anti-forgery cookie token not found in headers.");
}
if (string.IsNullOrEmpty(formToken))
{
throw new HttpAntiForgeryException("Anti-forgery formToken token not found in headers.");
}
}
AntiForgery.Validate(cookieToken, formToken);
}
base.OnActionExecuting(filterContext);
}
}
替MVC Actione掛上Attribute [AntiForgeryTokenHeaderFilter
]
Request在進入Action之前,透過此Filter取得Token並執行驗證
[AntiForgeryTokenHeaderFilter]
public ActionResult TokenTest(string mID)
{
...
}
搭配 Angular Interceptor攔截器
上面把token加到header裡的做法跟第一種存到參數的做法差不多,但還要自己多寫一個AntiforgeryTokenHeaderFilter.cs來取得Token並執行驗證,你可能會覺得這樣不是多次一舉嗎? 單如果跟Angular的攔截器(Interceptor)搭配的話,就能比較能體會第二種做法的好處了。
新增Angular Interceptor
此攔截器會幫我們在每個Request的header加上Token。
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
/**
* 所有請求都加上csfr token
*/
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor() {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const antiForgeryToken = localStorage.getItem('antiForgeryToken');
if (antiForgeryToken) {
request = request.clone({
setHeaders: {
'__RequestVerificationToken': antiForgeryToken //header name會變全小寫,但大小寫不影響
},
withCredentials: true
});
}
return next.handle(request);
}
}
新增測試API
//CSRF TEST
const csrfTest = await lastValueFrom(this._apiService.TokenTest("test"));
TokenTest(params: string): Observable<string>
{
const url = `${environment.apiBaseUrl}/TokenTest`;
let data = {
test: params
}
return this._httpClient.post(url, data, { responseType:'text' })
}
API測試
攔截器幫我們把Token加到Header裡,在透過MVC的AntiforgeryTokenHeaderFilter幫我們把Token拆解出Cookie Token跟Form Token後,在透過AntiForgery.Validate(cookieToken, formToken)
進行CSRF Token驗證。通過驗證後,就能成功呼叫API了。
Ref: