ASP.NET Identity 2集成到MVC5项目--笔记02
ASP.NET Identity 2集成到MVC5项目--笔记01
ASP.NET Identity 2集成到MVC5项目--笔记02
继上一篇,本篇主要是实现邮件、用户名登陆和登陆前邮件认证。
1. 登陆之前
到现在为止现在,涉及到身份认证的解决方案大致完成了。需要我们在Identity2Study项目下面按照运行前面的Nuget命令。下面才是真正的用到项目中去。我们演示一个简单的登录。
在我们建立登录控制器之前,我们需要为项目添加一个Startup类和简单配置一下web.config文件。
在项目的App_Start文件夹下新建一个分部类文件命名为:Startup.Auth.cs
namespace Identity2Study
{
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
}
}
需要注意的是这个类的命名空间是Identity2Study
然后在项目根文件夹建立一个分部类名为Startup.cs
namespace Identity2Study
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
打开Web.config在configuration节点下增加一下节点(实际上是配置EF的数据库连接字符串)
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=Identity2Study;Integrated Security=SSPI" providerName="System.Data.SqlClient" />
</connectionStrings>
2. 简单登陆
在Identity2Study项目下增加一个控制器名为:AccountController
[Authorize]
public class AccountController : Controller
{
public AccountController()
{
}
public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
{
UserManager = userManager;
SignInManager = signInManager;
}
private ApplicationUserManager _userManager;
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
private ApplicationSignInManager _signInManager;
public ApplicationSignInManager SignInManager
{
get
{
return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
}
private set { _signInManager = value; }
}
[HttpGet]
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut();
return RedirectToAction("Index", "Home");
}
//以下为辅助方法
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
}
为Login添加一个视图
@model Identity2Study.Models.LoginViewModel
@{
ViewBag.Title = "登录";
}
<h2>登录</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.RememberMe, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<div class="checkbox">
@Html.EditorFor(model => model.RememberMe)
@Html.ValidationMessageFor(model => model.RememberMe, "", new { @class = "text-danger" })
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="登录" class="btn btn-default" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
这里我们不添加注册账号的功能了,下面懒得改。这里我们直接使用上述添加的默认管理员登陆即可,如果不出意外,我们可以使用默认管理账号登陆
3. 使用邮箱或者用户名登陆
我们默认使用登陆的时候会提示我们输入邮箱登陆。这造成一个错觉,Identity2是使用邮箱登陆的,然后我们要改成其它登陆方式比如用户名登陆会需要重写方法什么的。但是!!这只是一个错觉,Identity默认用的就是用户名登陆,来看一眼我们的数据库:
单独看数据库是看不出什么来的。回到我们的登录控制器里面的代码
我下载了Identity2的源代码之后找到这个方法。
明明是用户名呀,联合前面的数据库。相信诸位看官已经明白是怎么一回事了。当然,这只能用用户名登陆了么?其实不是的,当然还有方法FindByEmailAsync。(图中那个user1是我自己写了,是为了打印出FindByEmailAsync())
其实除了用户名、邮箱登陆之外,还可以用手机号登陆。诸位看官接着看下去就会明白。
如果要验证我们的猜想是不是正确,我这里用了一个最笨的办法,直接修改数据库里面的UserName。
UserName字段值改成字符串001say
更改LoginViewModel
更改视图登陆代码
控制器这里只需改动点点
把原来的Email改成Name
运行登陆成功
验证了我们猜想是正确的。
这里是被Identity2给的那个例子挖了个坑,其实也怪我没仔细看源代码,回去看看我们建立默认账号的代码
if (user == null)
{
user = new ApplicationUser { UserName = name, Email = name };
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
name同时被创建成UserName和Email了。剩下的事情就好办了
建立一个最简单的RegisterViewModel
public class RegisterViewModel
{
[Required(ErrorMessage="用户名不能为空")]
[Display(Name="用户名")]
public string Name { get; set; }
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
控制器下添加如下代码
[HttpGet]
[AllowAnonymous]
public ActionResult Register()
{
return View();
}
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Name, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
AddErrors(result);
}
return View(model);
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
还有对应的视图代码,这里不贴出来了。动作选择Create模型选择RegisterViewModel即可。
查看数据库的AspNetUsers表多了一条记录
说了半天下面才是重点
重写ApplicationSignInManager类下面的PasswordSignInAsync方法。
找到Mvc.Identity解决方案的BLL文件夹下的类ApplicationSignInManager。需要我们添加一个辅助用的方法和重写PasswordSignInAsync方法。
private async Task<SignInStatus> SignInOrTwoFactor(ApplicationUser user, bool isPersistent)
{
var id = Convert.ToString(user.Id);
if (await UserManager.GetTwoFactorEnabledAsync(user.Id)
&& (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0
&& !await AuthenticationManager.TwoFactorBrowserRememberedAsync(id))
{
var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
AuthenticationManager.SignIn(identity);
return SignInStatus.RequiresVerification;
}
await SignInAsync(user, isPersistent, false);
return SignInStatus.Success;
}
public override async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
{
if (UserManager == null)
{
return SignInStatus.Failure;
}
ApplicationUser user;
string strRegex = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";
Regex re = new Regex(strRegex);
if(re.IsMatch(userName))
{
user = await UserManager.FindByEmailAsync(userName);
}
else{
user = await UserManager.FindByNameAsync(userName);
}
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
if (await UserManager.CheckPasswordAsync(user, password))
{
await UserManager.ResetAccessFailedCountAsync(user.Id);
return await SignInOrTwoFactor(user, isPersistent);
}
if (shouldLockout)
{
// If lockout is requested, increment access failed count which might lock out the user
await UserManager.AccessFailedAsync(user.Id);
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
}
return SignInStatus.Failure;
}
改一下LoginViewModel
public class LoginViewModel
{
[Required(ErrorMessage="用户名或者邮箱不能为空")]
[Display(Name = "用户名或邮箱")]
public string Name { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "密码")]
public string Password { get; set; }
[Display(Name = "记住我?")]
public bool RememberMe { get; set; }
}
剩下都不用改,直接运行。会发现我们使用用户名或者邮箱均可登陆!
3. 用户注册必须邮件验证后登陆
这里我个人理解为两个意思
- 注册后是可以登陆,但是会给没有用邮件确认的用户分配一个权限最小的角色
- 注册后必须通过邮件确认后才允许登陆,否则登陆不成功
第一种比较好办,就是在默认注册的控制器里面给新建的用户分配一个最小的角色,然后在邮件确认的方法里面重新分配一个角色即可。我这里想用的是第二种,没有确定邮件之前不允许登录。
由于Identity2的SignInStatus枚举类型里面并没有邮件是否确定的项,所以我们需要自己另外定义一个枚举类型(可能是我没发现,如果有知道的希望能指点我)
如图,打开Mvc.Identity根目录新建立一个Common的文件夹,新建一个类AppSignInStatus.cs添加如下代码
namespace Mvc.Identity.Common
{
/// <summary>
/// Possible results from a sign in attempt
/// </summary>
public enum AppSignInStatus
{
/// <summary>
/// Sign in was successful
/// </summary>
Success,
/// <summary>
/// User is locked out
/// </summary>
LockedOut,
/// <summary>
/// Sign in requires addition verification (i.e. two factor)
/// </summary>
RequiresVerification,
/// <summary>
/// Sign in failed
/// </summary>
Failure,
/// <summary>
/// make sure email
/// </summary>
NotSureEmail
}
}
我们只在SignInStatus基础上加了最后一个NotSureEmail
重新修改ApplicationUserManager类里面的两个方法如下:
private async Task<AppSignInStatus> SignInOrTwoFactor(ApplicationUser user, bool isPersistent)
{
var id = Convert.ToString(user.Id);
if (await UserManager.GetTwoFactorEnabledAsync(user.Id)
&& (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0
&& !await AuthenticationManager.TwoFactorBrowserRememberedAsync(id))
{
var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
AuthenticationManager.SignIn(identity);
return AppSignInStatus.RequiresVerification;
}
await SignInAsync(user, isPersistent, false);
return AppSignInStatus.Success;
}
public new async Task<AppSignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout,bool markSureEmail)
{
if (UserManager == null)
{
return AppSignInStatus.Failure;
}
ApplicationUser user;
string strRegex = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";
Regex re = new Regex(strRegex);
if(re.IsMatch(userName))
{
user = await UserManager.FindByEmailAsync(userName);
}
else{
user = await UserManager.FindByNameAsync(userName);
}
if (user == null)
{
return AppSignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return AppSignInStatus.LockedOut;
}
if (!await UserManager.IsEmailConfirmedAsync(user.Id) && markSureEmail )
{
return AppSignInStatus.NotSureEmail;
}
if (await UserManager.CheckPasswordAsync(user, password))
{
await UserManager.ResetAccessFailedCountAsync(user.Id);
return await SignInOrTwoFactor(user, isPersistent);
}
if (shouldLockout)
{
// If lockout is requested, increment access failed count which might lock out the user
await UserManager.AccessFailedAsync(user.Id);
if (await UserManager.IsLockedOutAsync(user.Id))
{
return AppSignInStatus.LockedOut;
}
}
return AppSignInStatus.Failure;
}
注意此时的PasswordSignInAsync方法不再是重写父类的了,而是显示的覆盖掉了父类里面的PasswordSignInAsync方法
回到Account控制器,添加一下方法:
//该方法为辅助方法
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
//用户邮件确认
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "SureEmail" : "Error");
}
修改Register方法为以下代码
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Name, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>");
return View("ConfirmeEmail");
}
AddErrors(result);
}
return View(model);
}
修改Login方法为以下代码
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await SignInManager.PasswordSignInAsync(model.Name, model.Password, model.RememberMe, shouldLockout: false,markSureEmail: true);
switch (result)
{
case AppSignInStatus.Success:
return RedirectToLocal(returnUrl);
case AppSignInStatus.LockedOut:
return View("Lockout");
case AppSignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case AppSignInStatus.NotSureEmail:
return View("ConfirmeEmail");
case AppSignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
并且添加两个视图文件位于View => Account文件夹下
ConfirmeEmail.cshtml
@{
ViewBag.Title = "登录";
}
<h3>请登录你的邮箱并确认!</h3>
SureEmail.cshtml
@{
ViewBag.Title = "确认邮件";
}
<h3>邮件已确认,现在您登录本网站了</h3>
完成后,我们使用默认建立的账号登陆试试。
因为我们这个默认账号在建立的时候,并未指定它已经验证过了。所以登陆时会提示我们没有确认邮件
怎么让我们建立的默认账号从一开始就不需要邮件验证呢?
修改一下建立默认账号的那段代码,把EmailConfirmed为true即可。
接下来我们建立一个新的账号并且测试一下。
注册没有验证过后登录会需要我们确认注册。
假如我们不需要注册的用户邮件验证而是直接可以登陆怎么办?还需要改代码么?回到刚刚说的是覆盖不是重写的PasswordSignInAsync方法,仔细看一下我们最后一个参数。如果需要则传一个true进去,这里需要显示指定是哪个参数
var result = await SignInManager.PasswordSignInAsync(model.Name, model.Password, model.RememberMe, shouldLockout: false,markSureEmail: true);
至于到底在注册登陆的时候需不需要验证邮箱,可以在数据库里面存。也可以在web.config文件里面添加节点存。自由发挥!
至此,我们可以使用邮箱或者用户名登陆,并且登陆之前必须确认邮件有效
ASP.NET Identity 2集成到MVC5项目--笔记02的更多相关文章
- ASP.NET Identity 2集成到MVC5项目--笔记01
Identiry2是微软推出的Identity的升级版本,较之上一个版本更加易于扩展,总之更好用.如果需要具体细节.网上具体参考Identity2源代码下载 参考文章 在项目中,是不太想直接把这一堆堆 ...
- [Solution] ASP.NET Identity(2) 空的项目使用
在本节中,我将说明将ASP.NET Identity添加到现有的项目或者一个空项目.我将介绍你需要添加的Nuget和Class.此示例中,会使用LocalDB. 本节目录: 注册用户 登入登出 注册用 ...
- ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的授权,中级篇
在前一篇文章中,我介绍了ASP.NET Identity 基本API的运用并创建了若干用户账号.那么在本篇文章中,我将继续ASP.NET Identity 之旅,向您展示如何运用ASP.NET Ide ...
- ASP.NET Identity 身份验证和基于角色的授权
ASP.NET Identity 身份验证和基于角色的授权 阅读目录 探索身份验证与授权 使用ASP.NET Identity 身份验证 使用角色进行授权 初始化数据,Seeding 数据库 小结 在 ...
- MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN
在Membership系列的最后一篇引入了ASP.NET Identity,看到大家对它还是挺感兴趣的,于是来一篇详解登录原理的文章.本文会涉及到Claims-based(基于声明)的认证,我们会详细 ...
- [转]MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN
本文转自:http://www.cnblogs.com/jesse2013/p/aspnet-identity-claims-based-authentication-and-owin.html 在M ...
- MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN -摘自网络
在Membership系列的最后一篇引入了ASP.NET Identity,看到大家对它还是挺感兴趣的,于是来一篇详解登录原理的文章.本文会涉及到Claims-based(基于声明)的认证,我们会详细 ...
- VS2013中web项目中自动生成的ASP.NET Identity代码思考
vs2013没有再分webform.mvc.api项目,使用vs2013创建一个web项目模板选MVC,身份验证选个人用户账户.项目会生成ASP.NET Identity的一些代码.这些代码主要在Ac ...
- 向空项目添加 ASP.NET Identity
安装 AspNet.Identity 程序包 Microsoft.AspNet.Identity.Core 包含 ASP.NET Identity 核心接口Microsoft.AspNet.Ident ...
随机推荐
- Atitit.100% 多个子元素自适应布局属性
Atitit.100% 多个子元素自适应布局属性 1.1. 原理1 1.2. Table布局1 1.3. Css布局1 1.4. 判断amazui加载完毕2 1.1. 原理 每个子元素平均分配,但是有 ...
- Atitit.软件开发的几大规则,法则,与原则。。。attilax总结
Atitit.软件开发的几大规则,法则,与原则... 1. 设计模式六大原则 2 1.1. 设计模式六大原则(1):单一职责原则 2 1.2. 设计模式六大原则(2):里氏替换原则 2 1.3. 设计 ...
- JQ集合
获取所有id以a开头的div$("div[id^='a']") $('div').last() $('#item1') <div id='select'>点击这里< ...
- 495. Implement Stack【easy】
Implement a stack. You can use any data structure inside a stack except stack itself to implement it ...
- 关于UNIX/Linux下安装《UNIX环境高级编程》源代码的问题
<UNIX环境高级编程(第三版)>是一本广为人知的unix系统编程书籍. 但是,书中的代码示例,要想正确的编译运行,要先做好准备工作: 1.下载源代码 传送门:http://apueboo ...
- linux学习笔记20--命令df和dh,fdisk
df和dh是用来查看磁盘空间使用情况的. linux中df命令的功能是用来检查linux服务器的文件系统的磁盘空间占用情况.可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间等信息. 1.命 ...
- 在GOOGLE浏览器中模拟移动浏览器 调试Web app
在此记录下,以便在今后的工作中用到. 首先通过F12 or Ctrl+Shift+i,打开开发者工具,点击开发者工具面板的 (show drawer)按钮,出现如下图所示的面板: 切换至Emulat ...
- linux web.py spawn-fcgi web.py 配置
本来要用uwsgi,但是...介于以前说过...这台服务器略老...redhat 3的系统...确实很老,没法用yum,没法安装很多东西,打算自己编译uwsgi,但是编译各种错误...花了快一天,最后 ...
- Tuning 12 manage statistics
这个 stattistics 对解析 sql 时的优化器有很重要的作用, 优化器是基于 statistics 来进行优化的. desc dbms_stats 包也可以 desc (早期使用 analy ...
- zend studio 10.6.2 设置默认编码为UTF-8
如果汉化的:窗体-->常规-->工作空间 然后再选择编码格式 如果未汉化:Window->Preferences->General->wookspace 然后再选 ...