热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

聊一聊Asp.net过滤器Filter那一些事

这篇文章主要介绍了聊一聊Asp.net过滤器Filter那一些事,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

最近在整理优化.net代码时,发现几个很不友好的处理现象:登录判断、权限认证、日志记录、异常处理等通用操作,在项目中的action中到处都是。在代码优化上,这一点是很重要着力点。这时.net中的过滤器、拦截器(Filter)就派上用场了。现在根据这几天的实际工作,对其做了一个简单的梳理,分享出来,以供大家参考交流,如有写的不妥之处,多多指出,多多交流。

概述:

.net中的Filter中主要包括以下4大类:Authorize(授权),ActionFilter(自定义),HandleError(错误处理)。

过滤器

类名

实现接口

描述

授权

AuthorizeAttribute

IAuthorizationFilter

此类型(或过滤器)用于限制进入控制器或控制器的某个行为方法,比如:登录、权限、访问控制等等

异常

HandleErrorAttribute

IExceptionFilter

用于指定一个行为,这个被指定的行为处理某个行为方法或某个控制器里面抛出的异常,比如:全局异常统一处理。

自定义

ActionFilterAttribute

IActionFilter和IResultFilter

用于进入行为之前或之后的处理或返回结果的之前或之后的处理,比如:用户请求日志详情日志记录

AuthorizeAttribute:认证授权

认证授权主要是对所有action的访问第一入口认证,对用户的访问做第一道监管过滤拦截闸口。

实现方式:需要自定义一个类,继承AuthorizeAttribute并重写OnAuthorization,在OnAuthorization中能够获取到用户请求的所有Request信息,其实我们做的所有认证拦截操作,其所有数据支撑都是来自Request中。

具体验证流程设计:

IP白名单:这个主要针对的是API做IP限制,只有指定IP才可访问,非指定IP直接返回

请求频率控制:这个主要是控制用户的访问频率,主要是针对API做,超出请求频率直接返回。

登录认证:登录认证一般我们采用的是通过在请求的header中传递token的方式来进行验证,这样即使用与一般的MVC登录认证,也使用与API接口的Auth认证,并且也不依赖于用户前端js设置等。

授权认证:授权认证就简单了,主要是验证该用户是否具有该权限,如果不具有,直接做下相应的返回处理。

MVC和API异同:

  命名空间:MVC:System.Web.Http.Filters;API:System.Web.Mvc

  注入方式:在注入方式上,主要包括:全局->控制器Controller->行为Action

  全局注册:针对所有系统的所有Aciton都使用

  Controller:只针对该Controller下的Action起作用

  Action:只针对该Action起作用

其中全局注册,针对MVC和API还有一些差异:

  MVC在 FilterConfig.cs中注入

filters.Add(new XYHMVCAuthorizeAttribute());

     API 在 WebApiConfig.cs 中注入

config.Filters.Add(new XYHAPIAuthorizeAttribute());

  注意事项:在实际使用中,针对认证授权,我们一般都是添加全局认证,但是,有的action又不需要做认证,比如本来的登录Action等等,那么该如何排除呢?其实也很简单,我们只需要在自定定义一个Attribute集成Attribute,或者系统的AllowAnonymousAttribute,在不需要验证的action中只需要注册上对于的Attribute,并在验证前做一个过滤即可,比如:

// 有 AllowAnonymous 属性的接口直接开绿灯
   if (actionContext.ActionDescriptor.GetCustomAttributes().Any())
   {
    return;
   }

API AuthFilterAttribute实例代码

/// 
 /// 授权认证过滤器
 /// 
 public class XYHAPIAuthFilterAttribute : AuthorizationFilterAttribute
 {
  /// 
  /// 认证授权验证
  /// 
  /// 请求上下文
  public override void OnAuthorization(HttpActionContext actionContext)
  {
   // 有 AllowAnonymous 属性的接口直接开绿灯
   if (actionContext.ActionDescriptor.GetCustomAttributes().Any())
   {
    return;
   }

   // 在请求前做一层拦截,主要验证token的有效性和验签
   HttpRequest httpRequest = HttpContext.Current.Request;

   // 获取apikey
   var apikey = httpRequest.QueryString["apikey"];

   // 首先做IP白名单校验 
   MBaseResult result = new AuthCheckService().CheckIpWhitelist(FilterAttributeHelp.GetIPAddress(actionContext.Request), apikey);

   // 检验时间搓
   string timestamp = httpRequest.QueryString["Timestamp"];
   if (result.Code == MResultCodeEnum.successCode)
   {
    // 检验时间搓 
    result = new AuthCheckService().CheckTimestamp(timestamp);
   }

   if (result.Code == MResultCodeEnum.successCode)
   {
    // 做请求频率验证 
    string acitOnName= actionContext.ActionDescriptor.ActionName;
    string cOntrollerName= actionContext.ActionDescriptor.ControllerDescriptor.ControllerName;
    result = new AuthCheckService().CheckRequestFrequency(apikey, $"api/{controllerName.ToLower()}/{acitonName.ToLower()}");
   }

   if (result.Code == MResultCodeEnum.successCode)
   {
    // 签名校验

    // 获取全部的请求参数
    Dictionary queryParameters = httpRequest.GetAllQueryParameters();

    result = new AuthCheckService().SignCheck(queryParameters, apikey);

    if (result.Code == MResultCodeEnum.successCode)
    {
     // 如果有NoChekokenFilterAttribute 标签 那么直接不做token认证
     if (actionContext.ActionDescriptor.GetCustomAttributes().Any())
     {
      return;
     }

     // 校验token的有效性
     // 获取一个 token
     string token = httpRequest.Headers.GetValues("Token") == null ? string.Empty :
      httpRequest.Headers.GetValues("Token")[0];

     result = new AuthCheckService().CheckToken(token, apikey, httpRequest.FilePath);
    }
   }

   // 输出
   if (result.Code != MResultCodeEnum.successCode)
   {
    // 一定要实例化一个response,是否最终还是会执行action中的代码
    actionContext.RespOnse= new HttpResponseMessage(HttpStatusCode.OK);
    //需要自己指定输出内容和类型
    HttpContext.Current.Response.COntentType= "text/html;charset=utf-8";
    HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result));
    HttpContext.Current.Response.End(); // 此处结束响应,就不会走路由系统
   }
  }
 }

 MVC AuthFilterAttribute实例代码

/// 
 /// MVC自定义授权
 /// 认证授权有两个重写方法
 /// 具体的认证逻辑实现:AuthorizeCore 这个里面写具体的认证逻辑,认证成功返回true,反之返回false
 /// 认证失败处理逻辑:HandleUnauthorizedRequest 前一步返回 false时,就会执行到该方法中
 /// 但是,我平时在应用过程中,一般都是在AuthorizeCore根据不同的认证结果,直接做认证后的逻辑处理
 /// 
 public class XYHMVCAuthorizeAttribute : AuthorizeAttribute
 {
  /// 
  /// 认证逻辑
  /// 
  /// 过滤器上下文
  public override void OnAuthorization(AuthorizationContext filterContext)
  {

   // 此处主要写认证授权的相关验证逻辑
   // 该部分的验证一般包括两个部分
   // 登录权限校验
   // --我们的一般处理方式是,通过header中传递一个token来进行逻辑验证
   // --当然不同的系统在设计上也不尽相同,有的也会采用session等方式来验证
   // --所以最终还是根据其项目本身的实际情况来进行对应的逻辑操作

   // 具体的页面权限校验
   // --该部分的验证是具体的到页面权限验证
   // --我看有得小伙伴没有做到这一个程度,直接将这一步放在前端js来验证,这样不是很安全,但是可以拦住小白用户
   // --当然有的系统根本就没有做权限控制,那就更不需要这一个逻辑了。
   // --所以最终还是根据其项目本身的实际情况来进行对应的逻辑操作

   // 现在用一个粗暴的方式来简单模拟实现过,用系统当前时间段秒厨艺3,取余数
   // 当余数为0:认证授权通过
   //   1:代表为登录,调整至登录页面
   //   2:代表无访问权限,调整至无权限提示页面

   // 当然,在这也还可以做一些IP白名单,IP黑名单验证 请求频率验证等等

   // 说到这而,还有一点需要注意,如果我们选择的是全局注册该过滤器,那么如果有的页面根本不需要权限认证,比如登录页面,那么我们可以给不需要权限的认证的控制器或者action添加一个特殊的注解 AllowAnonymous ,来排除

   // 获取Request的几个关键信息
   HttpRequest httpRequest = HttpContext.Current.Request;
   string acitOnName= filterContext.ActionDescriptor.ActionName;
   string cOntrollerName= filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;

   // 注意:如果认证不通过,需要设置filterContext.Result的值,否则还是会执行action中的逻辑

   filterContext.Result = null;
   int thisSecOnd= System.DateTime.Now.Second;
   switch (thisSecond % 3)
   {
    case 0:
     // 认证授权通过
     break;
    case 1:
     // 代表为登录,调整至登录页面
     // 只有设置了Result才会终结操作
     filterContext.Result = new RedirectResult("/html/Login.html");
     break;
    case 2:
     // 代表无访问权限,调整至无权限提示页面
     filterContext.Result = new RedirectResult("/html/NoAuth.html");
     break;
   }
  }
 }

ActionFilter:自定义过滤器

自定义过滤器,主要是监控action请求前后,处理结果返回前后的事件。其中API只有请求前后的两个方法。

重新方法

方法功能描述

使用于

OnActionExecuting

一个请求在进入到aciton逻辑前执行

MVC、API

OnActionExecuted

一个请求aciton逻辑执行后执行

MVC、API

OnResultExecuting

对应的view视图渲染前执行

MVC

OnResultExecuted

对应的view视图渲染后执行

MVC

在这几个方法中,我们一般主要用来记录交互日志,记录每一个步骤的耗时情况,以便后续系统优化使用。具体的使用,根据自身的业务场景使用。

其中MVC和API的异同点,和上面说的认证授权的异同类似,不在详细说明。

下面的一个实例代码:

API定义过滤器实例DEMO代码

/// 
 /// Action过滤器
 /// 
 public class XYHAPICustomActionFilterAttribute : ActionFilterAttribute
 {
  /// 
  /// Action执行开始
  /// 
  /// 
  public override void OnActionExecuting(HttpActionContext actionContext)
  {

  }

  /// 
  /// action执行以后
  /// 
  /// 
  public override void OnActionExecuted(HttpActionExecutedContext actionContext)
  {
   try
   {
    // 构建一个日志数据模型
    MApiRequestLogs apiRequestLogsM = new MApiRequestLogs();

    // API名称
    apiRequestLogsM.API = actionContext.Request.RequestUri.AbsolutePath;

    // apiKey
    apiRequestLogsM.API_KEY = HttpContext.Current.Request.QueryString["ApiKey"];

    // IP地址
    apiRequestLogsM.IP = FilterAttributeHelp.GetIPAddress(actionContext.Request);

    // 获取token
    string token = HttpContext.Current.Request.Headers.GetValues("Token") == null ? string.Empty :
        HttpContext.Current.Request.Headers.GetValues("Token")[0];
    apiRequestLogsM.TOKEN = token;

    // URL
    apiRequestLogsM.URL = actionContext.Request.RequestUri.AbsoluteUri;

    // 返回信息
    var objectCOntent= actionContext.Response.Content as ObjectContent;
    var returnValue = objectContent.Value;
    apiRequestLogsM.RESPONSE_INFOR = returnValue.ToString();

    // 由于数据库中最大只能存储4000字符串,所以对返回值做一个截取
    if (!string.IsNullOrEmpty(apiRequestLogsM.RESPONSE_INFOR) &&
     apiRequestLogsM.RESPONSE_INFOR.Length > 4000)
    {
     apiRequestLogsM.RESPONSE_INFOR = apiRequestLogsM.RESPONSE_INFOR.Substring(0, 2000);
    }

    // 请求参数
    apiRequestLogsM.REQUEST_INFOR = actionContext.Request.RequestUri.Query;

    // 定义一个异步委托 ,异步记录日志
    // Func action = AddApiRequestLogs;//声明一个委托
    // IAsyncResult ret = action.BeginInvoke(apiRequestLogsM, null, null);

   }
   catch (Exception ex)
   {

   }
  }
 }

HandleError:错误处理

异常处理对于我们来说很常用,很好的利用异常处理,可以很好的避免全篇的try/catch。异常处理箱单很简单,值需要自定义集成:ExceptionFilterAttribute,并自定义实现:OnException方法即可。

在OnException我们可以根据自身需要,做一些相应的逻辑处理,比如记录异常日志,便于后续问题分析跟进。

OnException还有一个很重要的处理,那就是对异常结果的统一包装,返回一个很友好的结果给用户,避免把一些不必要的信息返回给用户。比如:针对MVC,那么跟进不同异常,统一调整至友好的提示页面等等;针对API,那么我们可以一个统一的返回几个封装,便于用户统一处理结果。

MVC 的异常处理实例代码:

 /// 
 /// MVC自定义异常处理机制
 /// 说道异常处理,其实我们脑海中的第一反应,也该是try/cache操作
 /// 但是在实际开发中,很有可能地址错误根本就进入不到try中,又或者没有被try处理到异常
 /// 该类就发挥了作用,能够很好的未经捕获的异常,并做相应的逻辑处理
 /// 自定义异常机制,主要集成HandleErrorAttribute 重写其OnException方法
 /// 
 public class XYHMVCHandleError : HandleErrorAttribute
 {
  /// 
  /// 处理异常
  /// 
  /// 异常上下文
  public override void OnException(ExceptionContext filterContext)
  {
   // 我们在平时的项目中,异常处理一般有两个作用
   // 1:记录异常的详细日志,便于事后分析日志
   // 2:对异常的统一友好处理,比如根据异常类型重定向到友好提示页面

   // 在这里面既能获取到未经处理的异常信息,也能获取到请求信息
   // 在此可以根据实际项目需要做相应的逻辑处理
   // 下面简单的列举了几个关键信息获取方式

   // 控制器名称 注意,这样获取出来的是一个文件的全路径 
   string cOntropath= filterContext.Controller.ToString();

   // 访问目录的相对路径
   string filePath = filterContext.HttpContext.Request.FilePath;

   // url完整地址
   string url = (filterContext.HttpContext.Request.Url.AbsoluteUri).ExUrlDeCode();

   // 请求方式 post get
   string httpMethod = filterContext.HttpContext.Request.HttpMethod;

   // 请求IP地址
   string ip = filterContext.HttpContext.Request.GetIPAddress();

   // 获取全部的请求参数
   HttpRequest httpRequest = HttpContext.Current.Request;
   Dictionary queryParameters = httpRequest.GetAllQueryParameters();

   // 获取异常对象
   Exception ex = filterContext.Exception;

   // 异常描述信息
   string exMessage = ex.Message;

   // 异常堆栈信息
   string stackTrace = ex.StackTrace;

   // 根据实际情况记录日志(文本日志、数据库日志,建议具体步骤采用异步方式来完成)


   filterContext.ExceptiOnHandled= true;

   // 模拟根据不同的做对应的逻辑处理
   int statusCode = filterContext.HttpContext.Response.StatusCode;

   if (statusCode>=400 && statusCode<500)
   {
    filterContext.Result = new RedirectResult("/html/404.html");
   }
   else 
   {
    filterContext.Result = new RedirectResult("/html/500.html");
   }
  }
 }

API 的异常处理实例代码:

 /// 
 /// API自定义异常处理机制
 /// 说道异常处理,其实我们脑海中的第一反应,也该是try/cache操作
 /// 但是在实际开发中,很有可能地址错误根本就进入不到try中,又或者没有被try处理到异常
 /// 该类就发挥了作用,能够很好的未经捕获的异常,并做相应的逻辑处理
 /// 自定义异常机制,主要集成ExceptionFilterAttribute 重写其OnException方法
 /// 
 public class XYHAPIHandleError : ExceptionFilterAttribute
 {
  /// 
  /// 处理异常
  /// 
  /// 异常上下文
  public override void OnException(HttpActionExecutedContext actionExecutedContext)
  {
   // 我们在平时的项目中,异常处理一般有两个作用
   // 1:记录异常的详细日志,便于事后分析日志
   // 2:对异常的统一友好处理,比如根据异常类型重定向到友好提示页面

   // 在这里面既能获取到未经处理的异常信息,也能获取到请求信息
   // 在此可以根据实际项目需要做相应的逻辑处理
   // 下面简单的列举了几个关键信息获取方式

   // action名称 
   string actiOnName= actionExecutedContext.ActionContext.ActionDescriptor.ActionName;

   // 控制器名称 
   string cOntrollerName=actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName;

   // url完整地址
   string url = (actionExecutedContext.Request.RequestUri.AbsoluteUri).ExUrlDeCode();

   // 请求方式 post get
   string httpMethod = actionExecutedContext.Request.Method.Method;

   // 请求IP地址
   string ip = actionExecutedContext.Request.GetIPAddress();

   // 获取全部的请求参数
   HttpRequest httpRequest = HttpContext.Current.Request;
   Dictionary queryParameters = httpRequest.GetAllQueryParameters();

   // 获取异常对象
   Exception ex = actionExecutedContext.Exception;

   // 异常描述信息
   string exMessage = ex.Message;

   // 异常堆栈信息
   string stackTrace = ex.StackTrace;

   // 根据实际情况记录日志(文本日志、数据库日志,建议具体步骤采用异步方式来完成)
   // 自己的记录日志落地逻辑略 ......

   // 构建统一的内部异常处理机制,相当于对异常做一层统一包装暴露
   MBaseResult result = new MBaseResult()
   {
    Code = MResultCodeEnum.systemErrorCode,
    Message = MResultCodeEnum.systemError
   };

   actionExecutedContext.RespOnse= new HttpResponseMessage(HttpStatusCode.OK);
   //需要自己指定输出内容和类型
   HttpContext.Current.Response.COntentType= "text/html;charset=utf-8";
   HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result));
   HttpContext.Current.Response.End(); // 此处结束响应,就不会走路由系统
  }
 }

总结

.net过滤器,我个人的一句话理解就是:对action的各个阶段进行统一的监控处理等操作。.net过滤器中,其中每一个种过滤器的执行先后顺序为:Authorize(授权)-->ActionFilter(自定义)-->HandleError(错误处理)

好了,就先聊到这而,如果什么地方说的不对之处,多多指点和多多包涵。我自己写了一个练习DEMO,里面会有每一种情况的处理说明。有兴趣的可以取下载下来看一看,谢谢。

DEMO在GitHub地址为:https://github.com/xuyuanhong0902/XYH.FilterTest.git

到此这篇关于聊一聊Asp.net过滤器Filter那一些事的文章就介绍到这了,更多相关Asp.net过滤器Filter内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • 使用正则表达式爬取36Kr网站首页新闻的操作步骤和代码示例
    本文介绍了使用正则表达式来爬取36Kr网站首页所有新闻的操作步骤和代码示例。通过访问网站、查找关键词、编写代码等步骤,可以获取到网站首页的新闻数据。代码示例使用Python编写,并使用正则表达式来提取所需的数据。详细的操作步骤和代码示例可以参考本文内容。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • MySQL中的MVVC多版本并发控制机制的应用及实现
    本文介绍了MySQL中MVCC的应用及实现机制。MVCC是一种提高并发性能的技术,通过对事务内读取的内存进行处理,避免写操作堵塞读操作的并发问题。与其他数据库系统的MVCC实现机制不尽相同,MySQL的MVCC是在undolog中实现的。通过undolog可以找回数据的历史版本,提供给用户读取或在回滚时覆盖数据页上的数据。MySQL的大多数事务型存储引擎都实现了MVCC,但各自的实现机制有所不同。 ... [详细]
  • 本文介绍了Sencha Touch的学习使用心得,主要包括搭建项目框架的过程。作者强调了使用MVC模式的重要性,并提供了一个干净的引用示例。文章还介绍了Index.html页面的作用,以及如何通过链接样式表来改变全局风格。 ... [详细]
author-avatar
徘徊在堕落边缘的魔鬼
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有