【转】WebApi 身份认证解决方案:Basic基础认证

参考路径:https://www.cnblogs.com/landeanfen/p/5287064.html

 

前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题。也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想想都恐怖。经过一番折腾,总算是加上了接口的身份认证,在此记录下,也给需要做身份认证的园友们提供参考。

一、为什么需要身份认证

在前言里面,我们说了,如果没有启用身份认证,那么任何匿名用户只要知道了我们服务的url,就能随意访问我们的服务接口,从而访问或修改数据库。

1、我们不加身份认证,匿名用户可以直接通过url随意访问接口:

 

可以看到,匿名用户直接通过url就能访问我们的数据接口,最终会发生什么事,大家可以随意畅想。

2、增加了身份认证之后,只有带了我们访问票据的请求才能访问我们的接口。

例如我们直接通过url访问,会返回401

 

如果是正常流程的请求,带了票据,就OK了。

可以看到,正常流程的请求,会在请求报文的头里面增加Authorization这一项,它的值就是我们的Ticket票据信息。

二、Basic基础认证的原理解析

1、常见的认证方式

我们知道,asp.net的认证机制有很多种。对于WebApi也不例外,常见的认证方式有

  • FORM身份验证
  • 集成WINDOWS验证
  • Basic基础认证
  • Digest摘要认证

园子里很多关于WebApi认证的文章,各种认证方式都会涉及到,但感觉都不够细。这里也并不想去研究哪种验证方式适用哪种使用场景,因为博主还是觉得“贪多嚼不烂”,也可能是博主能力所限。对于认证机制,弄懂其中一种,其他的都能融会贯通。此篇就使用Basic基础认证来详细讲解下整个的过程。

1、常见的认证方式

我们知道,asp.net的认证机制有很多种。对于WebApi也不例外,常见的认证方式有

  • FORM身份验证
  • 集成WINDOWS验证
  • Basic基础认证
  • Digest摘要认证

园子里很多关于WebApi认证的文章,各种认证方式都会涉及到,但感觉都不够细。这里也并不想去研究哪种验证方式适用哪种使用场景,因为博主还是觉得“贪多嚼不烂”,也可能是博主能力所限。对于认证机制,弄懂其中一种,其他的都能融会贯通。此篇就使用Basic基础认证来详细讲解下整个的过程。

2、Basic基础认证原理

我们知道,认证的目的在于安全,那么如何能保证安全呢?常用的手段自然是加密。Basic认证也不例外,主要原理就是加密用户信息,生成票据,每次请求的时候将票据带过来验证。这样说可能有点抽象,我们详细分解每个步骤:

  1. 首先登陆的时候验证用户名、密码,如果登陆成功,则将用户名、密码按照一定的规则生成加密的票据信息Ticket,将票据信息返回到前端。
  2. 如果登陆成功,前端会收到票据信息,然后跳转到主界面,并且将票据信息也带到主界面的ActionResult里面(例如跳转的url可以这样写:/Home/Index?Ticket=Ticket)
  3. 在主界面的ActionResult里面通过参数得到票据信息Ticket,然后将Ticket信息保存到ViewBag里面传到前端。
  4. 在主界面的前端,发送Ajax请求的时候将票据信息加入到请求的Head里面,将票据信息随着请求一起发送到服务端去。
  5. 在WebApi服务里面定义一个类,继承AuthorizeAttribute类,然后重写父类的OnAuthorization方法,在OnAuthorization方法里面取到当前http请求的Head,从Head里面取到我们前端传过来的票据信息。解密票据信息,从解密的信息里面得到用户名和密码,然后验证用户名和密码是否正确。如果正确,表示验证通过,否则返回未验证的请求401。

 这个基本的原理。下面就按照这个原理来看看每一步的代码如何实现。

三、Basic基础认证的代码示例

首先说下我们的示例场景,我们在一个解决方案里面放了两个项目Web和WebApiCORS,我们这次还是以这个为例来说明。

1、登录过程

1.1、Web前端

1

2

3

4

5

6

7

<body>

    <div style="text-align:center;">

        <div>用户名:<input type="text" id="txt_username" /></div>

        <div>密  码:<input type="password" id="txt_password"  /></div>

        <div><input type="button" value="登录" id="btn_login" class="btn-default" /></div>

    </div>

</body>

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

$(function () {

    $("#btn_login").click(function () {

        $.ajax({

            type: "get",

            url: "http://localhost:27221/api/User/Login",

            data: { strUser: $("#txt_username").val(), strPwd: $("#txt_password").val() },

            success: function (data, status) {

                if (status == "success") {

                    if (!data.bRes){

                        alert("登录失败");

                        return;

                    }

                    alert("登录成功");

            //登录成功之后将用户名和用户票据带到主界面

                    window.location = "/Home/Index?UserName=" + data.UserName + "&Ticket=" + data.Ticket;

                }

            },

            error: function (e) {

            },

            complete: function () {

 

            }

 

        });

    });

});

1.2、登录的API接口

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

38

39

40

41

42

43

44

45

46

47

48

49

public class UserController : ApiController

    {

        /// <summary>

        /// 用户登录

        /// </summary>

        /// <param name="strUser"></param>

        /// <param name="strPwd"></param>

        /// <returns></returns>

        [HttpGet]

        public object Login(string strUser, string strPwd)

        {

            if (!ValidateUser(strUser, strPwd))

            {

                return new { bRes = false };

            }

            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(0, strUser, DateTime.Now,

                            DateTime.Now.AddHours(1), truestring.Format("{0}&{1}", strUser, strPwd),

                            FormsAuthentication.FormsCookiePath);

            //返回登录结果、用户信息、用户验证票据信息

            var oUser = new UserInfo { bRes = true, UserName = strUser, Password = strPwd, Ticket = FormsAuthentication.Encrypt(ticket) };

            //将身份信息保存在session中,验证当前请求是否是有效请求

            HttpContext.Current.Session[strUser] = oUser;

            return oUser;

        }

 

        //校验用户名密码(正式环境中应该是数据库校验)

        private bool ValidateUser(string strUser, string strPwd)

        {

            if (strUser == "admin" && strPwd == "123456")

            {

                return true;

            }

            else

            {

                return false;

            }

        }

    }

 

    public class UserInfo

    {

        public bool bRes { getset; }

 

        public string UserName { getset; }

 

        public string Password { getset; }

 

        public string Ticket { getset; }

    }

这里有一点需要注意的是,因为WebApi默认是没有开启Session的,所以需要我们作一下配置,手动去启用session。如何开启WebApi里面的Session,请参考:http://www.cnblogs.com/tinya/p/4563641.html

正如上面的原理部分说的,登录如果失败,则直接返回;如果成功,则将生成的票据Ticket带到前端,传到主界面/Home/Index,下面,我们就来看看主界面Home/Index。

2、/Home/Index主界面

1

2

3

4

5

6

7

8

9

10

public class HomeController : Controller

    {

        // GET: Home

        public ActionResult Index(string UserName, string Ticket)

        {

            ViewBag.UserName = UserName;

            ViewBag.Ticket = Ticket;

            return View();

        }

    }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

<html>

<head>

    <meta name="viewport" content="width=device-width" />

    <title>Index</title>

    <script src="~/Content/jquery-1.9.1.js"></script>

    <link href="~/Content/bootstrap/css/bootstrap.css" rel="stylesheet" />

    <script src="~/Content/bootstrap/js/bootstrap.js"></script>

    <script src="~/Scripts/Home/Index.js"></script>

    <script type="text/javascript">

        //打开页面的时候保存票据信息

        var UserName = '@ViewBag.UserName';

        var Ticket = '@ViewBag.Ticket';

    </script>

</head>

<body>

    <div>当前登录用户:'@ViewBag.UserName'</div>

 

    <div id="div_test">

 

    </div>

</body>

</html>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

$(function () {

    $.ajax({

        type: "get",

        url: "http://localhost:27221/api/Charging/GetAllChargingData",

        data: {},

        beforeSend: function (XHR) {

            //发送ajax请求之前向http的head里面加入验证信息

            XHR.setRequestHeader('Authorization''BasicAuth ' + Ticket);

        },

        success: function (data, status) {

            if (status == "success") {

                $("#div_test").html(data);

            }

        },

        error: function (e) {

            $("#div_test").html("Error");

        },

        complete: function () {

 

        }

 

    });

});

这里需要说明的是,我们在发送ajax请求之前,通过 XHR.setRequestHeader('Authorization', 'BasicAuth ' + Ticket); 这一句向请求的报文头里面增加票据信息。就是因为这里加了这一句,所以才有我们下图中的红线部分:  

 

3、WebApiCORS验证部分(重点)

我们看到,上面的/Home/Index页面里面发送了ajax请求去访问服务的 http://localhost:27221/api/Charging/GetAllChargingData 这个接口,那么我们在WebApi里面怎么去验证这个请求和合法的请求呢?接下来我们重点看看验证的这个过程。

3.1、在WebApiCORS项目里面自定义一个类RequestAuthorizeAttribute,去继承我们的AuthorizeAttribute这个类。然后重写OnAuthorization方法,在这个方法里面取到请求头的Ticket信息,然后校验用户名密码是否合理。

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

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

/// <summary>

    /// 自定义此特性用于接口的身份验证

    /// </summary>

    public class RequestAuthorizeAttribute : AuthorizeAttribute

    {

        //重写基类的验证方式,加入我们自定义的Ticket验证

        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)

        {

            //从http请求的头里面获取身份验证信息,验证是否是请求发起方的ticket

            var authorization = actionContext.Request.Headers.Authorization;

            if ((authorization != null) && (authorization.Parameter != null))

            {

                //解密用户ticket,并校验用户名密码是否匹配

                var encryptTicket = authorization.Parameter;

                if (ValidateTicket(encryptTicket))

                {

                    base.IsAuthorized(actionContext);

                }

                else

                {

                    HandleUnauthorizedRequest(actionContext);

                }

            }

            //如果取不到身份验证信息,并且不允许匿名访问,则返回未验证401

            else

            {

                var attributes = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>();

                bool isAnonymous = attributes.Any(a => a is AllowAnonymousAttribute);

                if (isAnonymous) base.OnAuthorization(actionContext);

                else HandleUnauthorizedRequest(actionContext);

            }

        }

 

        //校验用户名密码(正式环境中应该是数据库校验)

        private bool ValidateTicket(string encryptTicket)

        {

            //解密Ticket

            var strTicket = FormsAuthentication.Decrypt(encryptTicket).UserData;

 

            //从Ticket里面获取用户名和密码

            var index = strTicket.IndexOf("&");

            string strUser = strTicket.Substring(0, index);

            string strPwd = strTicket.Substring(index + 1);

 

            if (strUser == "admin" && strPwd == "123456")

            {

                return true;

            }

            else

            {

                return false;

            }

        }

    }

3.2、在具体的Api接口增加我们上面自定义类的特性

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

[RequestAuthorize]

    public class ChargingController : ApiController

    {

        /// <summary>

        /// 得到所有数据

        /// </summary>

        /// <returns>返回数据</returns>

        [HttpGet]

        public string GetAllChargingData()

        {

            return "Success";

        }

 

        /// <summary>

        /// 得到当前Id的所有数据

        /// </summary>

        /// <param name="id">参数Id</param>

        /// <returns>返回数据</returns>

        [HttpGet]

        public string GetAllChargingData(string id)

        {

            return "ChargingData" + id;

        }

 

    }

增加了特性标注之后,每次请求这个API里面的接口之前,程序会先进入到我们override过的 OnAuthorization() 方法里面,验证通过之后,才会进到相应的方法里面去执行,否则返回401。  

四、优化

 通过上面的几步,基本就能达到我们想要的身份认证的效果,但是总是感觉不太方便,主要不太方便的点有以下几个。

  1. 每次新建一个API,对应的接口上面都要标注 [RequestAuthorize] 这个一个东西,感觉好麻烦。
  2. 每次发送ajax请求,都要在beforeSend事件里面加 XHR.setRequestHeader('Authorization', 'BasicAuth ' + Ticket); 这个,感觉也麻烦。
  3. 如果有些WebApi服务的某些方法,我们不想使用这个验证,让它可以匿名用户验证(比如我们的登录方法Login)。该怎么处理呢。

关于以上两点,我们优化下

1、解决API的问题

在API里面加一个公共的父类,在父类上面标注 [RequestAuthorize] 即可。

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

namespace WebApiCORS.Controllers

{

    [RequestAuthorize]

    [EnableCors(origins: "*", headers: "*", methods: "*")]

    public class BaseApiController : ApiController

    {

    }

}

namespace WebApiCORS.Controllers

{

    public class ChargingController : BaseApiController

    {

        /// <summary>

        /// 得到所有数据

        /// </summary>

        /// <returns>返回数据</returns>

        [HttpGet]

        public string GetAllChargingData()

        {

            return "Success";

        }

 

        /// <summary>

        /// 得到当前Id的所有数据

        /// </summary>

        /// <param name="id">参数Id</param>

        /// <returns>返回数据</returns>

        [HttpGet]

        public string GetAllChargingData(string id)

        {

            return "ChargingData" + id;

        }

  }

}

注意:我们登录的请求是不需要验证的,因为登录的时候还没有产生票据,所以登录的API不能够继承 BaseApiController   

2、解决ajax的问题

新建一个文件Jquery_ajax_extention.js

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

(function ($) {

    //1.得到$.ajax的对象

    var _ajax = $.ajax;

    $.ajax = function (options) {

        //2.每次调用发送ajax请求的时候定义默认的error处理方法

        var fn = {

            error: function (XMLHttpRequest, textStatus, errorThrown) {

                toastr.error(XMLHttpRequest.responseText, '错误消息', { closeButton: true, timeOut: 0, positionClass: 'toast-top-full-width' });

            },

            success: function (data, textStatus) { },

            beforeSend: function (XHR) { },

            complete: function (XHR, TS) { }

        }

        //3.扩展原生的$.ajax方法,返回最新的参数

        var _options = $.extend({}, {

            error: function (XMLHttpRequest, textStatus, errorThrown) {

                fn.error(XMLHttpRequest, textStatus, errorThrown);

            },

            success: function (data, textStatus) {

                fn.success(data, textStatus);

            },

            beforeSend: function (XHR) {

                XHR.setRequestHeader('Authorization''BasicAuth ' + Ticket);

                fn.beforeSend(XHR);

            },

            complete: function (XHR, TS) {

                fn.complete(XHR, TS);

            }

        }, options);

        //4.将最新的参数传回ajax对象

        _ajax(_options);

    };

})(jQuery);

引用这个js后再发送ajax不必在每个请求的beforeSend里面写了。  

3、解决特殊不想使用验证的方法

如果我们某些方法不想使用验证,使得它可以让匿名用户访问,我们可以在方法的上面加特性标注 [AllowAnonymous] ,申明该方法运行匿名访问。比如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public class ChargingController : BaseApiController

    {

        /// <summary>

        /// 得到所有数据

        /// </summary>

        /// <returns>返回数据</returns>

        [HttpGet]

        public string GetAllChargingData()

        {

            return "Success";

        }

 

        /// <summary>

        /// 得到当前Id的所有数据

        /// </summary>

        /// <param name="id">参数Id</param>

        /// <returns>返回数据</returns>

        [HttpGet]

        [AllowAnonymous]

        public string GetAllChargingData(string id)

        {

            return "ChargingData" + id;

        }

  }

  

  

  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/437401.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode每日打卡 - 汉明距离

位异或运算&#xff08;^&#xff09; 运算规则是&#xff1a;两个数转为二进制&#xff0c;然后从高位开始比较&#xff0c;如果相同则为0&#xff0c;不相同则为1。 比如&#xff1a;8^11. 8转为二进制是1000&#xff0c;11转为二进制是1011.从高位开始比较得到的是&#xff…

【转】Task和async/await详解

一、什么是异步 同步和异步主要用于修饰方法。当一个方法被调用时&#xff0c;调用者需要等待该方法执行完毕并返回才能继续执行&#xff0c;我们称这个方法是同步方法&#xff1b;当一个方法被调用时立即返回&#xff0c;并获取一个线程执行该方法内部的业务&#xff0c;调用者…

LeetCode每日打卡 - 反转每对括号间的子串

题解中有个更好的思路&#xff0c;stack存入的不是(的坐标&#xff0c;存入每次到左括号的字符串&#xff0c;拿到右括号就对其进行反转&#xff0c;更妙一些。 import java.util.Stack; class Solution {public String reverseParentheses(String s) {Stack<Integer> s…

iar stm32_树莓派玩转STM32开发(一)——介绍篇

01—树莓派树莓派(Raspberry Pi)听起来让人流口水&#xff0c;但它的确不是吃的(身为非吃货的我第一次也以为它是食物……)。树莓派是基于ARM架构的Linux卡片电脑&#xff0c;由英国树莓派基金会开发&#xff0c;目的是以低价硬件以及自由软件来促进学校的基本电脑科学教育。树…

JavaScript面向对象的理解

前言 1. 本文默认阅读者已有面向对象的开发思想&#xff0c;最好是使用过c、java&#xff0c;本人Java不太熟悉&#xff0c;所以例子都是用C来写的。 2. 本人不是专业网站开发人员&#xff0c;接触javascript一年多&#xff0c;自己也编写调试了一些代码&#xff0c;本文完全根…

【转】C# 彻底搞懂async/await

关键&#xff1a; 异步方法&#xff1a;在执行完成前立即返回调用方法&#xff0c;在调用方法继续执行的过程中完成任务。 async/await 结构可分成三部分&#xff1a; &#xff08;1&#xff09;调用方法&#xff1a;该方法调用异步方法&#xff0c;然后在异步方法执行其任务的…

LeetCode每日打卡 - 汉明距离总和

有点慢&#xff0c;两层循环也可以完成&#xff0c;就是换个方向&#xff0c;外层遍历32大小的bits数组&#xff0c;里层遍历nums的数字每次右移一位&#xff0c;计算方法类似。 class Solution {public int totalHammingDistance(int[] nums) {//int 是4byte &#xff0c; 每…

python编写代码_用 Python 编写干净、可测试、高质量的代码

用 Python 编写干净、可测试、高质量的代码Noah Gift 2010 年 12 月 20 日发布简介 编写软件是人所承担的最复杂的任务之一。AWK 编程语言和 "K and R C" 的作者之一 Brian Kernigan 在 Software Tools 一书中总结了软件开发的真实性质&#xff0c;他说&#xff0c;“…

阿里云服务器被[kthreaddi]挖矿病毒攻击

首先我根本https://blog.csdn.net/weixin_41599103/article/details/115403332这个博客试了下并没有成功&#xff0c;所以应该是被侵入的程序不一样 先去阿里云里看一下详情 明确告诉了是通过docker被攻击了&#xff0c;先将wordpress容器停止并删除容器和镜像 kill掉进行&am…

python canvas画弧度_python画一朵玫瑰给你

听说 python 还能画画&#xff1f;是的&#xff0c;今天亲手画一朵玫瑰给你。turtle 是 python 中一个强大的绘制图像的库&#xff0c;可以用来绘制各种图像&#xff0c;使用起来很方便。但是需要计算各种角度、距离等&#xff0c;所以要想完成一幅复杂的图像还是有一定的难度的…

通俗彻底解读批处理的延迟变量

先说环境变量 一个批处理可以看成是一个程序&#xff0c;它的环境变量就是自己的全局变量。操作系统可以看成是一个大程序&#xff0c;它的环境变量同样也是全局变量&#xff0c;只不过为了和程序的环境变量区别&#xff0c;就叫做全局环境变量了&#xff0c;既然为了区别&…

LeetCode每日打卡 - 4的幂

不使用循环来今天解题 public boolean isPowerOfFour(int n) {return n > 0 && (n & (n-1)) 0 && n % 31;}首先去做了2的幂这道题&#xff0c;并了解了n&(n-1)的过程&#xff0c;然后思考一下这道题:求二进制中1的个数&#xff0c;理解了n&(n-…

【转】探索c#之Async、Await剖析

阅读目录&#xff1a; 基本介绍基本原理剖析内部实现剖析重点注意的地方总结 基本介绍 Async、Await是net4.x新增的异步编程方式&#xff0c;其目的是为了简化异步程序编写&#xff0c;和之前APM方式简单对比如下。 APM方式&#xff0c;BeginGetRequestStream需要传入回调函数…

时间计算题100道_小学数学专项练习:计算题200道,趁早打印给孩子,期末考试拿100分!...

点击上方「升学的秘诀」关注我们&#xff01;获取更多教育经验、方法、学习资料等&#xff0c;每天中午12点与您相约&#xff01;▼说到我们数学学习过程中最重要的是什么&#xff1f;毋庸置疑那就是我们的一个计算能力&#xff0c;计算可以说是贯穿了我们整个数学学习体系&…

幂等性实现 -接口幂等性

接口幂等性 1.什么是幂等性 对于同一笔业务操作&#xff0c;不管调用多少次&#xff0c;得到的结果都是一样的。 也就是方法调用一次和调用多次产生的额外效果是相同的&#xff0c;他就具有幂等性 2.为什么需要幂等性 在系统高并发的环境下&#xff0c;很有可能因为网络&#…

C 怎么读取Cpp文件_python之调用C加速计算(一)

一、前言python语言是目前比较火的语言&#xff0c;很容易上手&#xff0c;对数据处理也比较友好&#xff0c;可以用几行代码就能进行一些简单的数据处理工作。但是对于稍微大型的数值计算&#xff0c;或者一些涉及到大量循环的数值计算python的计算速度有点让人失望。即使是使…

【转】刨根究底字符编码【2.0版】(3):字符编码的由来、演变与ASCII码

为什么需要字符编码 1. 计算机一开始发明出来时是用来解决数字计算问题的&#xff0c;后来人们发现&#xff0c;计算机还可以做更多的事&#xff0c;例如文本处理。 但计算机其实挺“笨”的&#xff0c;它只“认识”010110111000…这样由0和1两个数字组成的二进制数字&#…

JS创建对象的模式介绍

转自http://www.cnblogs.com/asqq/archive/2013/02/01/3194993.html

matplotlib的优点_超详细matplotlib基础介绍!!!

(给Python开发者加星标&#xff0c;提升Python技能)来源&#xff1a;逐梦erhttps://zhumenger.blog.csdn.net/article/details/106530281【导语】&#xff1a;出色的数据可视化&#xff0c;会让你的数据分析等工作锦上添花&#xff0c;让人印(升)象(职)深(加)刻(薪)。matplotli…

【转】WPF PRISM开发入门一( 初始化PRISM WPF程序)

这篇博客将介绍在WPF项目中引入PRISM框架进行开发的一些基础知识。目前最新的PRISM的版本是Prism 6.1.0&#xff0c;可以在Github上获取PRISM的源码。这个系列的博客将选择PRISM 4.1版本来讲解。可以从微软官网上下载到PRISM 4.1相关内容。将下载下来的文件解压开&#xff1a; …