CSRF驗證,搭配Angular Interceptor

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: