angular源码分析:angular中入境检察官$sce

一、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 };
  }];

上一期:angular源码分析:angular中脏活累活承担者之$parse
下期预告:angular源码分析:angular中脏活累活的承担者之$interpolate

posted @ 2015-11-19 01:49  王大鹏  阅读(4207)  评论(0编辑  收藏