`

Security Tutorials系列文章第十三章:Recovering and Changing Passwords

阅读更多

本文英文原版及代码下载:http://www.asp.net/learn/security/tutorial-13-cs.aspx


Security Tutorials系列文章第十三章:Recovering and Changing Passwords


导言:

我和大多数人一样,在不同的网站有不同的密码,当然很有可能会忘记密码,因此网站会提供一种途径让大家找回自己的密码,一般的方式的将一个随机生成的密码发给用户的邮箱,用户用该随机密码登陆后再改成自己容易记住的密码。

ASP.NET提供了2个用于找回和改变密码的控件;其中PasswordRecovery控件用于帮助用户找回密码,而ChangePassword控件用来改变密码。与其他与登陆相关的控件类似,这2个控件与Membership framework框架打交道,在后台重置或修改用户的密码。


本文我们考察这2个控件的用法,此外,我们再看看如何使用MembershipUser类的 ChangePassword 和 ResetPassword methods来改变和重置密码。


Step 1: Helping Users Recover Lost Passwords


所有提供了用户帐户的站点都要提供一个机制来帮助用户找回丢失的密码,不过对ASP.NET来说这简直就是小菜一碟,这都要归功于PasswordRecovery控件,它提供一个界面来供用户输入帐户名,必要的话还要输入密码提示问题的答案,然后将密码发到用户的邮箱。

注意:
由于邮件内容是以纯文本的形式进行传输的,因此,通过电子邮件发送用户的密码是要冒风险的。

PasswordRecovery控件由3部分构成:

.UserName---提示用户输入帐户名

.Question--显示一个安全提示问题,用一个TextBox控件来方便用户输入答案

.Success--显示一个消息提示用户其密码已经发送过去了。


不过PasswordRecovery显示出来的部分和执行的行为取决于如下的Membership配置的设定:

.RequiresQuestionAndAnswer
.EnablePasswordRetrieval
.EnablePasswordReset


Membership framework的RequiresQuestionAndAnswer设置指示用户在注册的时候是否必须要指定安全问题和密码;正如我们在文章《Creating User Accounts》里探讨的那样,如果RequiresQuestionAndAnswer 为 True(默认值),那么CreateUserWizard的界面就会包含TextBox以供用户输入新帐户的安全问题和答案;如果RequiresQuestionAndAnswer 为 False,那么就不会让用户输入这些信息;类似如果RequiresQuestionAndAnswer为True,那么PasswordRecovery控件也会显示Question部分;只有但用户正确的输入答案后才会找回密码。如果RequiresQuestionAndAnswer为False,那么PasswordRecovery控件将直接从UserName部分跳到Success部分。

在用户提供了用户名或用户名或密码提示问题的正确答案后--这取决于 RequiresQuestionAndAnswer是否为True;那么PasswordRecovery控件将把用户的密码发到用户的邮箱。如果EnablePasswordRetrieval为True,那么发送的是用户的当前密码;如果为False,且EnablePasswordReset为True,那么PasswordRecovery将生成一个新的随机生成的密码并发送到用户邮箱;如果EnablePasswordRetrieval 和 EnablePasswordReset都为False,PasswordRecovery将抛出一个异常。

注意:
我们知道SqlMembershipProvider存储用户密码的格式有三种:Clear, Hashed(默认),或Encrypted. 到底用哪种取决于Membership的配置设定情况;本文的示例代码用的是Hashed格式。但使用Hashed格式的时候,EnablePasswordRetrieval必须要设置为False,因为系统无法根据存储的hashed格式的密码来确定实际的密码是多少。


图1显示的是,不同的配置设定时,PasswordRecovery控件的不同界面。

图1


注意:
在文章《Creating the Membership Schema in SQL Server》里,我们将RequiresQuestionAndAnswer设为True, EnablePasswordRetrieval设为False,而 EnablePasswordReset设为True.

Using the PasswordRecovery Control

让我们看看如何在ASP.NET页面里使用PasswordRecovery控件。打开RecoverPassword.aspx页面,拖一个PasswordRecovery控件到页面,设其ID为RecoverPwd.和Login与CreateUserWizard控件一样,PasswordRecovery控件呈现的界面比较丰富,有Labels, TextBoxes, Buttons, 以及validation等控件;你可以通过style属性对界面进行定制,或将不同的部分转换为templates(模板).我将这作为一个练习留给有兴趣的读者。


在用户访问该页面时,他要输入帐户名,点Submit按钮。由于我们将RequiresQuestionAndAnswer设置为了True;因此PasswordRecovery控件将显示Question部分,用户正确输入答案,点Submit按钮后,PasswordRecovery将把用户的密码更新为一个随机生成的密码,并发到用户的邮箱,你甚至不需要写一行代码!

不过在测试页面之前,我们还要留意一项设置,我们需要在Web.config文件里指定邮件传输设置.因为PasswordRecovery控件依赖这些设置来发送邮件.


我们通过<system.net>元素里的<mailSettings>元素来指定邮件传输设置。用<smtp>元素来指定传输方式和默认的发件人地址(From address).下面的代码指定使用一个名为smtp.example.com的SMTP服务器,端口为25,用户名和密码分别为"username"和"password".

注意:
<system.net>元素为根元素<configuration>的子元素,而与<system.web>元素是平级的,因此不要把<system.net>放在了<system.web>元素的内部。

<configuration>
...
<system.net>
<mailSettings>
<smtp deliveryMethod="Network"
from="youraddress@example.com">
<network
host="smtp.example.com"
userName="username"
password="password"
port="25" />
</smtp>
</mailSettings>
</system.net>
</configuration>

一旦你配置好SMTP设置后,在浏览器里登陆RecoverPassword.aspx页面,首先输入一个无效的帐户,如图2所示,PasswordRecovery控件将显示一个消息指出无法访问该用户的信息,而显示的提示消息可通过控件的UserNameFailureText属性来指定.


图2


现在键入一个帐户,用系统里的一个拥有合法邮件地址的帐户做测试,输入帐户后点Submit按钮,那么PasswordRecovery控件将显示其Question部分,如果你输入一个错误的答案,那么PasswordRecovery将展示一个提示信息(如图3),使用QuestionFailureText属性可以对提示信息进行定制。

图3


最后,输入正确的答案,点击Submit。在后台,PasswordRecovery控件生成一个随机的密码,赋值给对应的用户帐户,再将新密码发送到用户邮箱(如图4),再显示Success部分。


图4


Customizing the Email

PasswordRecovery控件发送的邮件相当的“丑陋”(见图4),邮件的发送者由<smtp>元素的from属性指定的,而主题就是“Password”,而邮件主体为如下的纯文本:

Please return to the site and log in using the following information.
User Name: username
Password: password


我们可以通过PasswordRecovery控件的SendingMail event事件的事件处理器来"编程"对该信息进行定制,或者显式地通过MailDefinition属性来定制,接下来我们来探讨这2种方式。

该SendingMail事件是在电邮发出去之前触发的,我们正好可以在此时编程修改邮件内容。触发该事件时,将传入一个类型为MailMessageEventArgs的参数,该参数的Message属性引用的正是即将要发送的邮件。

为SendingMail事件创建一个事件处理器,添加如下的代码,它向CC list添加了一个webmaster@example.com,如下:

protected void RecoverPwd_SendingMail(object sender, MailMessageEventArgs e)
{
e.Message.CC.Add("webmaster@example.com");
}


我们也可以通过显式的方式对邮件内容进行配置。PasswordRecovery控件的MailDefinition属性是一个类型为MailDefinition的对象.该MailDefinition类提供了与邮件相关的host信息,比如From, CC, Priority, Subject, IsBodyHtml, BodyFileName等。我们可以将Subject属性设置为其他更适合的信息,比如“Your password has been reset...”,而不是默认的(“Password”)。

为了对邮件内容的body进行定制,我们要另外创建一个单独的邮件模板文件来保存body的内容。因此,新建一个名为EmailTemplates的文件夹,再在该文件夹里添加一个名为PasswordRecovery.txt的文件,添加如下的内容:

Your password has been reset, <%UserName%>!

According to our records, you have requested that your password be reset. Your new password is: <%Password%>

If you have any questions or trouble logging on please contact a site administrator.

Thank you!

注意占位符<%UserName%> 和 <%Password%>的使用。PasswordRecovery控件在放送电邮之前,将用户的username 和password信息自动的填充<%UserName%> 和 <%Password%>占位符。

最后,将MailDefinition的BodyFileName属性指向刚创建的模板文件(/EmailTemplates/PasswordRecovery.txt)

完毕后,再次访问RecoverPassword.aspx页面,输入你的帐户名和安全问题答案,你将收到一个类似图5里那样的邮件,只是主题和内容都改了,webmaster@example.com也添加到CC里了。


图5


如果要发送一个HTML格式的邮件的话,将IsBodyHtml属性设置为True (默认为False),再对邮件模板进行改动以包含HTML。

并不是只有PasswordRecovery类才有MailDefinition属性,正如第2步里看到的那样,ChangePassword控件也有MailDefinition属性,此外,CreateUserWizard控件也包含了这个属性,因此你可以设置以便自动向新用户发送欢迎邮件。

注意:
目前在左边的导航栏里没有导航到RecoverPassword.aspx页面的连接;只有当用户不能成功登陆站点的时候他们才有兴趣访问该页面;因此,对Login.aspx页面进行改动以包含到RecoverPassword.aspx页面的链接。


Programmatically Resetting a User’s Password

当重设用户密码时,PasswordRecovery控件将调用MembershipUser object对象的 ResetPassword方法,不过该方法有2个重载:

.ResetPassword----重设用户的密码,如果RequiresQuestionAndAnswer为False的话就使用该重载方法。

.ResetPassword(securityAnswer) ----只有当用户提供的安全问题答案正确时才重置用户密码。只有但RequiresQuestionAndAnswer为True时才使用该重载方法。

上面2种方法都返回新的,随机生成的密码。


和Membership framework里的其他方法一样,ResetPassword方法将委派一个配置好的provider.具体来说,SqlMembershipProvider调用aspnet_Membership_ResetPassword存储过程,传入用户的username,新的password,以及用户对安全问题所做的回答,以及其他的内容,存储过程在确定答案正确后就对用户的密码进行更新。

以下这些要注意:

.一个被冻结(locked out)的用户帐户不能重置其密码,而一个未经审核的用户却可以。我们将在后面的《Unlocking and Approving User》文章你详细的进行探讨。

.如果用户回答错误,那么回答错误的次数就增加1次。如果在指定的时间长度内,用户回答失败的次数达到了指定的次数,那么该帐户就会被冻结。


A Word on How the Random Passwords are Generated


在图4和图5里显示的随机生成的密码是由Membership class类的GeneratePassword方法生成的。该方法接受2个integer类型的输入参数——length(表示长度)和numberOfNonAlphanumericCharacters(表示非常规字符的个数),返回的字符串长度不低于指定的长度,包含的非常规字符也不会少有指定的数字。但调用该方法时——不管是Membership类调用还是由“登陆”相关的控件调用——这2个参数的值都通过Membership配置的MinRequiredPasswordLength 和 MinRequiredNonalphanumericCharacters选项决定。在此,我们分别指定为7和1.

GeneratePassword方法生成密码时使用"强密码",此外,GeneratePassword方法的修饰符为 public,这就意味着你可以直接在ASP.NET应用程序里使用。

注意:
SqlMembershipProvider类生成的随机密码至少是14个字符,如果为MinRequiredPasswordLength指定的值低于14,那么该指定值就会被忽略。


Step 2: Changing Passwords

这种随机生成的密码太难记忆了,看图4里的密码:WWGUZv(f2yM:Bd,毫无疑问,用户会考虑把它变成更容易记忆的密码。

ChangePassword控件可以让用户改变他们的密码,和PasswordRecovery控件类似,ChangePassw组成:Change Password 和 Success.其中Change Password界面让用户输入旧密码和新密码,只要旧密码吻合,且新密码的长度和非常规字符数合乎要求,那么ChangePassword控件就会更新用户的密码,并切换到Success界面。

注意:
ChangePassword控件是通过调用MembershipUser对象的ChangePassword方法来修改用户的密码的,而ChangePassword方法接受2个string类型的参数-oldPassword以及newPassword,只要oldPassword有效就将密码更新为newPassword.

打开ChangePassword.aspx页面,添加一个ChangePassword控件,设为ChangePwd;此时显示出来的是“Change Password”界面(如图6所示).就像PasswordRecovery控件那样,你可以通过控件的智能标签对这2个界面控制。此外,这2个界面可以通过众多的style属性进行定制,或将它们转换为一个template.

图6


ChangePassword控件可以对当前登陆用户的密码进行更新或对某个指定用户的密码进行更新,在图6里,默认的Change Password界面呈现为3个TextBox控件:一个供输入旧密码,2个供输入新密码。该默认的界面是对当前用户更改密码。


要对其他某个用户的密码进行更新,要将ChangePassword控件的DisplayUserName属性设置为True.这将在界面上添加第4个TextBox控件,输入要更改密码的用户帐户。

如果你允许一个下线的用户在未登陆的情况下更改自己的密码的话,将DisplayUserName属性设置为True是很有用的。不过我觉得让一个用户在更改自己密码之前先登陆系统再正常不过了,因此就让DisplayUserName属性为False(默认值),这样做实际上是不允许匿名用户访问该页面。对站点的URL授权规则进行更新,以禁止匿名用户访问ChangePassword.aspx页面。如果你想复习一下URL authorization rule语法,请参与前面的《User-Based Authorization》文章。

注意:
你可能以为将DisplayUserName属性设置为True时可以便于管理员更改某个用户的密码,其实即使你将DisplayUserName属性设置为True,管理员也要输入有效的旧密码的情况

下才能改动用户的密码,我们在第3步里探讨管理员更改用户密码的技术。

在浏览器里登陆ChangePassword.aspx页面,注意,如果你输入的新密码长度不够或非常规字符数不够那么就将显示一个错误消息(如图7所示)

图7


但旧密码吻合,新密码合乎要求的话,登陆用户的密码就会被更新,并显示"Success"界面.


Sending a Confirmation Email

ChangePassword控件在默认情况下是不会对用户发送邮件的,如果你希望发送的话,需要对MailDefinition属性进行配置。让我们对ChangePassword进行配置以向用户发送一个HTML格式的邮件,包含用户的新密码。

首先在EmailTemplates文件夹里新建一个名为ChangePassword.htm的文件,添加如下的标记:
<html>
<body>
<h2>Your Password Has Been Changed!</h2>
<p>
This email confirms that your password has been changed.
</p>
<p>
To log on to the site, use the following credentials:
</p>
<table>
<tr>
<td>
<b>Username:</b>
</td>
<td>
<%UserName%>
</td>
</tr>
<tr>
<td>
<b>Password:</b>
</td>
<td>
<%Password%>
</td>
</tr>
</table>
<p>
If you have any questions or encounter any problems logging in,
please contact a site administrator.
</p>
</body>
</html>

接下来将ChangePassword控件的MailDefinition属性的BodyFileName, IsBodyHtml,以及Subject分别设置为“~/EmailTemplates/ChangePassword.htm”,True,以及“Your password has changed!”。

完成后重新访问该页面,再次改变你的密码。这次,ChangePassword控件将发送一个自定义的,HTML格式的邮件给用户(如图8)。


图8


Step 3: Allowing Administrators to Change Users’ Passwords

提供用户帐户的应用程序的一个共同点是,管理员可以修改其他帐户的密码。不过使用PasswordRecovery 和 ChangePassword控件的话,用户可以修改自己的密码,而管理员却不能修改其他帐户的密码。

如果客户坚持要让管理员可以修改其他帐户的密码呢?不幸的是,这有点棘手,因为要改变一个用户的密码的话,我们必须向MembershipUser对象的ChangePassword方法提供旧密码和新密码,问题是管理员并不知道用户的密码是多少。

其中一个办法是先重新设置用户的密码,再用一个新的密码来进行修改,如下:
MembershipUser usr = Membership.GetUser(username);
string resetPwd = usr.ResetPassword();
usr.ChangePassword(resetPwd, newPassword);

代码首先通过username(也就是我们想修改密码的那个用户账号)检索出一个MembershipUser对象,再调用ResetPassword方法,将用户的密码改成一个随机生成的新密码,该随机新密码存储在变量resetPwd里,现在我们知道了用户的密码了(也就是那个新生成的随机密码),我们调用ChangePassword方法再次将该随机密码修改成newPassword(也就是管理员设置的新密码).

问题出来了,只有但Membership的RequiresQuestionAndAnswer配置项为"False"时,该方法才管用,如果RequiresQuestionAndAnswer配置项为"True",我们调用该方法的话,还要必须向ResetPassword方法传入密码提示问题的答案,否则要抛出一个异常。

如果Membership framework配置为需要密码提示问题和答案,且客户坚持要允许管理员可以改变某个用户的帐户,那么你有3个选择:

.告诉客户办不到

.将RequiresQuestionAndAnswer配置项设为False.但这样就导致了不太安全的应用程序

.绕开Membership framework为我们设置的"羁绊"而直接与数据库打交道。Membership构架包含了一个名为aspnet_Membership_SetPassword的存储过程,不需要知道用户的旧密码或密码安全问题的情况下就可以重新设置用户的密码。

以上任意一种都不是特别的好,这就是开发人员经常要遇到的情况。


我采用第三种方式,绕开Membership 和 MembershipUser类,直接操作SecurityTutorials数据库。

注意:
直接与数据库打交道的话,将会打乱Membership framework提供的封装好了的东西,这么做也会把我们与SqlMembershipProvider联系在一起,使我们的代码不那么"优雅";此外,在ASP.NET的后续版本里如果Membership的架构改变了的话,该方法不见得有用。该方法只是可选方案而已,不代表就是最佳解决办法。

代码并没有太出彩的地方,而且还有点长,因此我不打算在本文里做深入的探讨,如果你有兴趣的话,下载本文的代码,访问~/Administration/ManageUsers.aspx页面,我们是在前一篇文章里创建该页面的,它列出了每一个帐户。我对GridView控件做了改动以包含一个到UserInformation.aspx页面的链接,通过查询字符串来传递帐户的username,而

UserInformation.aspx页面则将显示该用户的信息,再用TextBoxe来供改变他们的密码(图9)。

输入新密码,并在第2个TextBox里确认后,点击Update User按钮,随后发生一个页面回传,调用aspnet_Membership_SetPassword存储过程,更新用户的密码。我建议那些对该功能感兴趣的读者对代码熟悉后进行一些扩展,向用户发送一个通知邮件。

图9


注意:
目前,只有当Membership framework框架被配置为以Clear或Hashed形式存储密码时,UserInformation.aspx页面才能起作用。现在还没有对密码进行加密的功能,我推荐使用一个诸如Reflector这样的反编译器(decompiler)来考察.NET Framework里method的源代码;我们首先要考察SqlMembershipProvider类的ChangePassword方法,我就是使用这种技术来对密码进行hash处理的。


结语:

ASP.NET提供了2个控件来帮助用户管理其密码,当用户忘记其密码时就要用到PasswordRecovery控件,根据Membership framework的配置情况,要么将用户的当前密码要么将一个新创建的随机密码通过电邮发送给用户,而ChangePassword密码可以使用户可以修改其密码。

和Login 和 CreateUserWizard控件一样,不用手写一行代码,PasswordRecovery 和 ChangePassword控件就可以呈现丰富的界面,如果默认的界面满足不了你的要求,那么你可以通过不同的style属性来进行定制,或者你可以将界面转换为一个templates,以进行高度的用户定制;在内部,这些控件使用Membership API,调用MembershipUser对象的

ResetPassword 和 ChangePassword方法。


祝编程愉快!

分享到:
评论

相关推荐

    spring java图片上传源码.rar

    源码实现了图片上传功能,可供相关功能开发的小伙伴参考学习使用。

    新入职员工工作总结范文大全(篇).docx

    工作总结,新年计划,岗位总结,工作汇报,个人总结,述职报告,范文下载,新年总结,新建计划。

    本项目内容为《SpringBoot 2.X 基础教程》配套源码.zip

    提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

    IMG_20240426_195457.jpg

    IMG_20240426_195457.jpg

    培训看版.xlsx

    Excel数据看板,Excel办公模板,Excel模板下载,Excel数据统计,数据展示

    A Confidence-Guided Automated System for Non-emergency Calls.pdf

    A Confidence-Guided Automated System for Non-emergency Calls.pdf

    用于快速反馈控制律优化的梯度丰富机器学习控制matlab代码.zip

    1.版本:matlab2014/2019a/2021a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    杭州电子科技大学数据结构数据结构讲义.pdf

    杭州电子科技大学,期末考试资料,计算机专业期末考试试卷,试卷及答案,数据结构。

    对保险业中人工智能的监管: 平衡消费者保护与创新.pdf

    对保险业中人工智能的监管: 平衡消费者保护与创新.pdf

    重庆大学电磁场原理10年考题(a卷)答案及评分标准.pdf

    重庆大学期末考试试卷,重大期末考试试题,试题及答案

    银行软件作业代码示例20240426

    震惊,师专男大竟然在夜深人静的夜晚写下了这些普通人都看不懂的东西,内容是...

    导航软件iApp源码V3+配置教程

    一款支持侧边导航栏的网页导航APP源码,风格简约为主,可以通过远程文档进行远程控制列表,浏览器拥有检测下载的功能。,配置较为简单,适合入门小白学习参考。 导航软件iApp源码V3+配置教程 配置教程在mian.iyu的载入事件里面

    基于CNN模型实现土壤湿度检测-数据集和完整代码.rar

    该数据集和完整代码主要实现《基于CNN模型实现土壤湿度检测》,适用于正在学习深度学习、神经网络以及计算机、农业自动化等相关专业的伙伴们。在现代农业和环境监测中,研究土壤湿度数据来预测未来的湿度趋势十分重要。资源中的CNN模型可能仍不够完善,大家可以继续修改完善,不断研究其他的内容。感谢大家的支持和交流,你们的支持也是我前进的十足动力!

    重庆大学数字电子技术试卷2007-2008(1)答案.pdf

    重庆大学期末考试试卷,重大期末考试试题,试题及答案

    mlab-upenn 研究小组的心脏模型模拟.zip

    1.版本:matlab2014/2019a/2021a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    【基于Springboot+Vue的Java毕业设计】银行账目账户管理系统项目实战(源码+录像演示+说明).rar

    【基于Springboot+Vue的Java毕业设计】银行账目账户管理系统项目实战(源码+录像演示+说明).rar 【项目技术】 开发语言:Java 框架:Spingboot+vue 架构:B/S 数据库:mysql 【演示视频-编号:305】 https://pan.quark.cn/s/8dea014f4d36 【实现功能】 用户信息管理,存取业务管理,公告信息管理,挂失信息管理,账户信息管理等

    年公司财务会计岗位工作总结(一).docx

    工作总结,新年计划,岗位总结,工作汇报,个人总结,述职报告,范文下载,新年总结,新建计划。

    智能机械装备制造信息化整体解决方案.pptx

    智能机械装备制造信息化整体解决方案.pptx

    杭州电子科技大学学生复习卷及部分答案.pdf

    杭州电子科技大学,期末考试资料,计算机专业期末考试试卷,试卷及答案,数据结构。

    Unity Asset Quantum Console v2.6.3

    Unity在打包后仍能看到控制台输出,甚至通过命令调用绑定好的函数,调试游戏的强大助手!

Global site tag (gtag.js) - Google Analytics