探討Model上客製化標籤如何被解析使用 (第20天)

Agenda

前言

上一篇有介紹ModelMetadata和參數Model之間的關係.

UML_Model

MVC提供我們一個IMetadataAware介面,讓我們可以對最終生成ModelMetadata進行自由設定.

IMetadataAware介面

IMetadataAware介面有一個OnMetadataCreated方法

1
2
3
4
public interface IMetadataAware
{
void OnMetadataCreated(ModelMetadata metadata);
}

MVC有預設兩個實現IMetadataAware介面的標籤.

  • AllowHtmlAttribute:標上的屬性可以攜帶Html資料.
  • AdditionalMetadataAttribute:對於當前屬性的modelmetadata資訊的AdditionalValues添加資料(添加資料可透過ViewData.ModelMetadata.AdditionalValues取得資料)

如果你想要對於modelmetadata資訊做修改或新增資料可以製作自己IMetadataAware介面標籤.

我有做一個可以針對於Asp.net MVC Debugger的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.

AllowHtmlAttribute標籤

為了防止(Cross-site scripting)XSS攻擊通過在針對某些輸入框中寫入或注入HTML來攻擊我們Web應用

針對HTML標記驗證通過ModelMetadataRequestValidationEnabled來控制,如下面程式碼顯示

這是一個布爾類型的可讀寫屬性。

1
2
3
4
public class ModelMetadata
{
public virtual bool RequestValidationEnabled { get; set; }
}

此屬性在默認情況下為True進行驗證防護

ASP.NET MVC有一個預設標籤AllowHtmlAttribute在進行Model綁定之前會對對應請求資料進行驗證,確保沒有任何HTML標記包含其中。

如果在Input tag輸入有關Html資料就會出現下面錯誤.(這是MVC貼心幫我們開啟防護XSS攻擊的機制)

具有潛在危險Request.Form 的值已從用戶端 (xxxxxx) 偵測到。

UML_Model

如果查看AllowHtmlAttribute原始碼就很簡單只是把metadata.RequestValidationEnabled設成false允許使用者上傳Html資料.

1
2
3
4
5
6
7
8
9
10
11
12
13
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class AllowHtmlAttribute : Attribute, IMetadataAware
{
public void OnMetadataCreated(ModelMetadata metadata)
{
if (metadata == null)
{
throw new ArgumentNullException("metadata");
}

metadata.RequestValidationEnabled = false;
}
}

我們就可以把Html資料傳送到AP端了

只是這個標籤請斟酌使用打開有一定風險.

為何可以透過實現IMetadataAware介面來擴充對於metadata操作

AssociatedMetadataProvider抽象類別中有個ApplyMetadataAwareAttributes方法.

參數物件上屬性進行反射取得使用到IMetadataAware的標籤,並呼叫他的OnMetadataCreated方法.

1
2
3
4
5
6
7
8
9
10
public abstract class AssociatedMetadataProvider : ModelMetadataProvider
{
private static void ApplyMetadataAwareAttributes(IEnumerable<Attribute> attributes, ModelMetadata result)
{
foreach (IMetadataAware awareAttribute in attributes.OfType<IMetadataAware>())
{
awareAttribute.OnMetadataCreated(result);
}
}
}

CachedDataAnnotationsModelMetadataProvider

MVC Action傳入參數上可以標示許多標籤例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class VerifyCodeViewModel
{
[Required]
public string Provider { get; set; }

[Required]
[Display(Name = "代碼")]
public string Code { get; set; }
public string ReturnUrl { get; set; }

[Display(Name = "記住此瀏覽器?")]
public bool RememberBrowser { get; set; }

public bool RememberMe { get; set; }
}

public class ForgotViewModel
{
[Required]
[Display(Name = "電子郵件")]
public string Email { get; set; }
}
  • RequiredAttribute
  • DisplayAttribute

還有其他一大堆,下面會跟大家介紹MVC是怎麼取得並使用這些標籤,ModelMetadataProviders這個類別會提供使用哪個ModelMetadataProvider

在原始碼建構子預設使用CachedDataAnnotationsModelMetadataProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ModelMetadataProviders
{
private static ModelMetadataProviders _instance = new ModelMetadataProviders();
private ModelMetadataProvider _currentProvider;
private IResolver<ModelMetadataProvider> _resolver;

internal ModelMetadataProviders(IResolver<ModelMetadataProvider> resolver = null)
{
_resolver = resolver ?? new SingleServiceResolver<ModelMetadataProvider>(
() => _currentProvider,
new CachedDataAnnotationsModelMetadataProvider(),
"ModelMetadataProviders.Current");
}
//....
}

CachedDataAnnotationsModelMetadataProvider類別有一個CreateMetadataPrototype方法返回一個CachedDataAnnotationsModelMetadata物件,這個物件存放參數上屬性欄位使用標籤資訊.

1
2
3
4
5
6
7
8
9
10
11
12
public class CachedDataAnnotationsModelMetadataProvider : CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>
{
protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
{
return new CachedDataAnnotationsModelMetadata(this, containerType, modelType, propertyName, attributes);
}

protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
{
return new CachedDataAnnotationsModelMetadata(prototype, modelAccessor);
}
}

CachedDataAnnotationsModelMetadata

CachedDataAnnotationsModelMetadata類別上有許多屬性,主要是方便日後來判斷使用MVC使用標籤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>
{
private bool _isEditFormatStringFromCache;

public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
: base(prototype, modelAccessor)
{
}

public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, IEnumerable<Attribute> attributes)
: base(provider, containerType, modelType, propertyName, new CachedDataAnnotationsMetadataAttributes(attributes.ToArray()))
{
}

protected override bool ComputeConvertEmptyStringToNull()
{
return PrototypeCache.DisplayFormat != null
? PrototypeCache.DisplayFormat.ConvertEmptyStringToNull
: base.ComputeConvertEmptyStringToNull();
}

protected override string ComputeDataTypeName()
{
if (PrototypeCache.DataType != null)
{
return PrototypeCache.DataType.ToDataTypeName();
}

if (PrototypeCache.DisplayFormat != null && !PrototypeCache.DisplayFormat.HtmlEncode)
{
return DataTypeUtil.HtmlTypeName;
}

return base.ComputeDataTypeName();
}
//...
}

有一個蠻特別事情是CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>他繼承一個泛型類別CachedDataAnnotationsMetadataAttributes存放取得物件標籤的資訊.

CachedDataAnnotationsMetadataAttributes

CachedDataAnnotationsMetadataAttributes類別主要把屬性上的某些標籤給值到類別的屬性上,方便CachedDataAnnotationsModelMetadata來操作使用.

這也是為什麼只有某些標籤掛在屬性上可以被使用.預設只有CachedDataAnnotationsMetadataAttributes才會被反射取得.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CachedDataAnnotationsMetadataAttributes
{
public CachedDataAnnotationsMetadataAttributes(Attribute[] attributes)
{
DataType = attributes.OfType<DataTypeAttribute>().FirstOrDefault();
Display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
DisplayColumn = attributes.OfType<DisplayColumnAttribute>().FirstOrDefault();
DisplayFormat = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault();
DisplayName = attributes.OfType<DisplayNameAttribute>().FirstOrDefault();
Editable = attributes.OfType<EditableAttribute>().FirstOrDefault();
HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault();
ReadOnly = attributes.OfType<ReadOnlyAttribute>().FirstOrDefault();
Required = attributes.OfType<RequiredAttribute>().FirstOrDefault();
ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
//.....
}

public DataTypeAttribute DataType { get; protected set; }

public DisplayAttribute Display { get; protected set; }

public DisplayColumnAttribute DisplayColumn { get; protected set; }

public DisplayFormatAttribute DisplayFormat { get; protected set; }

public DisplayNameAttribute DisplayName { get; protected set; }

public EditableAttribute Editable { get; protected set; }

public HiddenInputAttribute HiddenInput { get; protected set; }
//.....
}

小結:

ModelMetaData是一個Model Binding很重要物件,裡面存放許多調用參數的資訊.

MVC提供一個IMetadataAware介面可以改變ModelMetaData中資訊,提高更高的彈性.

這篇也介紹了IMetadataAware介面是在哪邊做攔截.

另外也分享常掛在屬性上標籤取得的類別跟機制

  • DisplayNameAttribute
  • RequiredAttribute
  • DisplayAttribute

透過CachedDataAnnotationsModelMetadataProvider這個類別來取得以上標籤,並在日後做判斷.

下篇會和大家分享另一種屬性標籤ValidationAttribute的取得和呼叫過程.

此文作者:Daniel Shih(石頭)
此文地址https://isdaniel.github.io/ithelp-day20/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!


如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^