一、ng-bing-html指令问题
需求:我需要将一个变量$scope.x = '<a href="http://www.cnblogs.com/web2-developer/">王大鹏</a>'绑定到angular的视图上,希望视图上显示的一个链接.
1.如果,我采用ng-bind="x",或者{{x}},我在视图看到的结果就是上面那个字符串,就说里面的<和>都被转义了.
2.如果,我在用ng-bind-html,视图上什么都没有,并且会抛出一个错误:"Attempting to use an unsafe value in a safe context."
问题来了,该怎么解决呢?
二、SCE
针对上面的问题,官方文档给出了解决方法:方法1,引入ngSanitize模块,方法而利用$sce.trustAsHtml将要绑定的值变成一个可信任的值。
那么,问题来了:$sce到底是什么鬼?
SCE是Strict Contextual Escaping的缩写,不知道怎么翻译,从$sce干的事情来看就是将语境中存在的跨站攻击的风险给干掉.SCE是一种模式,用于满足angular在某些情况下需要绑定一个上下文被标记为安全上下文的值.其中一个例子就是"ng-bind-html"这个指令,要绑定任意的html,我们参考上下文特权和SCE的上下文.(原文,Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context. One example of such a context is binding arbitrary html controlled by the user via ng-bind-html. We refer to these contexts as privileged or SCE contexts.)
$sce提供了一种将可能存在跨站风险的内容(包括html,url,css,js,resourceUrl)标记为被信任的内容。这是为什么呢?因为,在angular中,默认的这些内容是不被信任,所以,在绑定数据的时候,这些内容会被认为不安全。但是如果我们的确有这样的需求,就需要用$sce来做标记处理。
三、$sce如何使用
1.$sce提供的方法:
$sce.getTrustedXXX,获取被信任的数据值。其中的XXX代表Hmtl,Css,Js,Url,ResourceUrl。
$sce.trustXXX,让绑定内容,成为受信任的XXX
2.将$sce用于指令编写
在指令值,一般需要操作dom,在添加元素时,如果要将传入的变量直接作为dom元素进行添加,就会可能会带来跨站风险,这时就需要,用$sce.getTrustedXXX从变量中获取受信任的数据。
3.在controller中使用。
使用$sce.trustXXX来将确实需要被信任的数据标记为信任数据。
请慎用!
四、$sce的代码实现
1.$sce是依赖于$sceDelegate,$sce的实现就是调用$sceDelegate来完成,$sceDelegate是$sce的代理者,这里的设计采用了代理模式,所以我们可以通过修改$sceDelegate来完成对$sce的功能增强。
sce.trustAs = $sceDelegate.trustAs; sce.getTrusted = $sceDelegate.getTrusted; sce.valueOf = $sceDelegate.valueOf;
2.$sce的基础方法就只有trustAs
,getTrusted
,valueOf
,其他的方法都是这三个方法的"快捷方式"
// Shorthand delegations. var parse = sce.parseAs,getTrusted = sce.getTrusted,trustAs = sce.trustAs; forEach(SCE_CONTEXTS,function(enumValue,name) { var lName = lowercase(name); sce[camelCase("parse_as_" + lName)] = function(expr) { return parse(enumValue,expr); }; sce[camelCase("get_trusted_" + lName)] = function(value) { return getTrusted(enumValue,value); }; sce[camelCase("trust_as_" + lName)] = function(value) { return trustAs(enumValue,value); }; });
3.$sce.parse
sce.parseAs = function sceParseAs(type,expr) { var parsed = $parse(expr); if (parsed.literal && parsed.constant) { return parsed; } else { return $parse(expr,function(value) { return sce.getTrusted(type,value); }); } };
五、$sceDelegate的实现
$sceDelegate实现三个函数:trustAs,getTrusted,valueOf,如果想实现一些自定义的安全策略,可以修改$sceDelegate或对这三个方法进行重载。
1.资源地址的白名单和黑名单
在资源的处理上,$sceDelegate引入了白黑名单机制,可以允许用户编写不同的安全策略来控制不同域名的不同权限。
function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; } else if (isString(matcher)) { // Strings match exactly except for 2 wildcards - '*' and '**'. // '*' matches any character except those from the set ':/.?&'. // '**' matches any character (like .* in a RegExp). // More than 2 *'s raises an error as it's ill defined. if (matcher.indexOf('***') > -1) { throw $sceMinErr('iwcard','Illegal sequence *** in string matcher. String: {0}',matcher); } matcher = escapeForRegexp(matcher). replace('\\*\\*','.*').//两个*号,将匹配任意打印字符 replace('\\*','[^:/.?&;]*');//一个*,只能匹配url中的分隔符间的内容 return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. // Match entire URL / disallow partial matches. // Flags are reset (i.e. no global,ignoreCase or multiline) return new RegExp('^' + matcher.source + '$');//转正则式 } else { throw $sceMinErr('imatcher','Matchers may only be "self",string patterns or RegExp objects'); } } function adjustMatchers(matchers) {//工具函数,将配置转换成正则表达式数组 var adjustedMatchers = []; if (isDefined(matchers)) { forEach(matchers,function(matcher) { adjustedMatchers.push(adjustMatcher(matcher));//调用上面的工具函数,将使用通配符方式的配置转成正则表达式 }); } return adjustedMatchers; } this.resourceUrlWhitelist = function(value) {//提供$sceDelegate.resourceUrlWhitelist 配置白名单 if (arguments.length) { resourceUrlWhitelist = adjustMatchers(value);//调用上面的工具函数 } return resourceUrlWhitelist; }; this.resourceUrlBlacklist = function(value) {//提供$sceDelegate.resourceUrlBlacklist 配置黑名单 if (arguments.length) { resourceUrlBlacklist = adjustMatchers(value);//调用上面的工具函数 } return resourceUrlBlacklist; };
function matchUrl(matcher,parsedUrl) {//url匹配函数 if (matcher === 'self') { return urlIsSameOrigin(parsedUrl); } else { // definitely a regex. See adjustMatchers() return !!matcher.exec(parsedUrl.href);//双!限制,返回的只能是bool值 } } function isResourceUrlAllowedByPolicy(url) {//执行白黑名单策略:只允许在白名单中且不再黑名单中的内容 var parsedUrl = urlResolve(url.toString()); var i,n,allowed = false; // Ensure that at least one item from the whitelist allows this url. for (i = 0,n = resourceUrlWhitelist.length; i < n; i++) {//先判断白名单 if (matchUrl(resourceUrlWhitelist[i],parsedUrl)) { allowed = true; break; } } if (allowed) { // Ensure that no item from the blacklist blocked this url. for (i = 0,n = resourceUrlBlacklist.length; i < n; i++) {//后处理黑名单 if (matchUrl(resourceUrlBlacklist[i],parsedUrl)) { allowed = false; break; } } } return allowed; }
2.下面byType将是什么?
function generateHolderType(Base) { var holderType = function TrustedValueHolderType(trustedValue) { this.$$unwrapTrustedValue = function() { return trustedValue; }; }; if (Base) { holderType.prototype = new Base(); } holderType.prototype.valueOf = function sceValueOf() { return this.$$unwrapTrustedValue(); }; holderType.prototype.toString = function sceToString() { return this.$$unwrapTrustedValue().toString(); }; return holderType; } var trustedValueHolderBase = generateHolderType(),//这里trustedValueHolderBase 将是构造函数TrustedValueHolderType,且没有绑定原型 byType = {}; //下面的都是函数 byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]);
上面的代码执行后的结果是:
3.trustAs,valueOf和getTrusted
var htmlSanitizer = function htmlSanitizer(html) { throw $sceMinErr('unsafe','Attempting to use an unsafe value in a safe context.'); }; if ($injector.has('$sanitize')) {//这里检查是否有$sanitize htmlSanitizer = $injector.get('$sanitize'); } function trustAs(type,trustedValue) { var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); if (!Constructor) { throw $sceMinErr('icontext','Attempted to trust a value in invalid context. Context: {0}; Value: {1}',type,trustedValue); } if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') { return trustedValue; } // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting // mutable objects,we ensure here that the value passed in is actually a string. if (typeof trustedValue !== 'string') { throw $sceMinErr('itype','Attempted to trust a non-string value in a content requiring a string: Context: {0}',type); } return new Constructor(trustedValue);//将一个值标记为可信,就是用相应的构造函数进行包装 } function valueOf(maybeTrusted) { if (maybeTrusted instanceof trustedValueHolderBase) { return maybeTrusted.$$unwrapTrustedValue(); } else { return maybeTrusted; } } function getTrusted(type,maybeTrusted) { if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } // If we get here,then we may only take one of two actions. // 1. sanitize the value for the requested type,or // 2. throw an exception. if (type === SCE_CONTEXTS.RESOURCE_URL) { if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { throw $sceMinErr('insecurl','Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}',maybeTrusted.toString()); } } else if (type === SCE_CONTEXTS.HTML) { return htmlSanitizer(maybeTrusted);//如果htmlSanitizer = $injector.get('$sanitize');,这里就调用了$sanitize } throw $sceMinErr('unsafe','Attempting to use an unsafe value in a safe context.'); } return { trustAs: trustAs,getTrusted: getTrusted,valueOf: valueOf }; }];