将最小的OWIN身份验证添加到现有的ASP.NET MVC应用程序
从ASP.NET 4开始,ASP.NET提供了一个相当有用的身份系统。如果您创建一个新项目并选择一个MVC项目并选择添加内部和外部身份验证,那么在您的应用程序中获得合理的身份实现是相当直接的。
但是,如果您有现有的应用程序,或者基于完整实体框架的身份结构不适合您,那么连接使用您自己的域/业务模型和类的最小和自定义实现的过程并不完全如此直截了当。您必须从完整的模板安装中删除不需要的部分,或者添加必要的部分。在这篇文章中,我希望我能告诉你如何做后者,只显示你需要的部分。
这个过程并不一定很难 - 但它没有很好的记录。很难找到创建必要处理程序所需的信息,这些处理程序可以处理将帐户链接到外部oAuth提供程序,如Google,Facebook,GitHub等。所以在这篇文章中,我将讨论这个场景。
真实世界的用例
我在上周末自己走下了这条路,因为我的旧网站之一 - CodePaste.net - 需要更新我在该网站上运行的旧OpenID身份验证。Codepaste是一个非常古老的网站; 事实上它是我建造的第一个MVC应用程序😃。但它在过去的7年里一直没有受到阻碍。直到谷歌决定拔掉旧的OpenId实施。我很快就收到了大量的电子邮件,并决定我必须重新实施外部提供商。
旧的实现使用了DotNetOpenAuth和几年前我写博客的 FormsAuthentication ,但多年来DotnetOpenAuth似乎已经失宠了,所以我决定咬紧牙关并转而使用更新的,内置的基于OWIN的Identity功能。 ASP.NET 4。
当你第一次开始查看身份时,那里的信息量相当庞大。很多介绍文章谈论如何使用'原样'没有定制。但是,如果没有完整的UserManager和Entity Framework数据存储,就没有很多关于使用核心Identity片段的信息,以便仅使用身份验证/授权并将它们与我自己的业务对象/域模型集成。
只是本地和外部登录的核心身份功能
这篇文章的目的是展示OWIN Identity系统的最小部分,以处理本地和外部帐户登录并将它们挂钩到自定义域模型,而不是使用基于Entity Framework的UserManager。简而言之,重点关注管理身份验证的系统组件,并将用户管理留给应用程序。
只要告诉我我需要什么!
本文的主要内容很长,因为除了核心登录功能之外,它还涵盖了与讨论相关的CodePaste.NET登录和用户管理功能。对于那些只需要螺母和螺栓的人来说,最后还有一个摘要部分,它只显示骨架格式的相关代码,您可以将其插入控制器。我还链接到我在这里讨论的帐户控制器的完整源代码,如果你想看到完整的代码,而不是一口大小的片段。
有关详细信息,请继续阅读。
ASP.NET MVC 5中基于OWIN的标识
如果您还没有在ASP.NET MVC 5中使用过新的Identity功能,我建议您首先通过创建一个新的MVC Web站点并让它创建一个启用了单个用户身份验证的默认Web站点来检查它。这将让您了解默认实现的工作原理。如果您正在查看特定于代码的实现细节,可以查看处理用户管理界面的AccountController和ManageController。AccountController处理登录本地和外部帐户,而ManageController处理设置新帐户,电子邮件确认等。这是身份感觉非常压倒性的部分原因 - 当您查看代码时,会发生大量事情并且它与基于特定实体框架的实现紧密混合。
不过,我鼓励你至少简单地看一下它,也许会逐步了解登录处理的总体流程。
只提取必要的碎片
为了在您自己的应用程序中使用不使用基于EF的UserManager的OWIN Idenity片段,您必须做一些事情。同样,作为本文的一部分,我的目标是:
- 支持基于Cookie的用户登录(用户名/密码)
- 支持外部登录(Google,Github,Twitter)
- 支持使用本地或外部帐户创建帐户的功能
- 支持使用本地或外部帐户登录
为了使用现有应用程序实现此目的,我必须:
- 关闭应用程序的IIS身份验证
- 添加适当的NuGet包
- 实施本地和外部帐户的帐户注册
- 实施登录本地和外部帐户
- 允许使用基于OWIN的登录机制登录
我们来看看每个步骤的样子。
关闭应用程序的IIS身份验证
ASP.NET Identity使用OWIN平台,该平台是一个不依赖于标准IIS安全性的自定义子系统。因为OWIN可以自托管,所以在这个系统中不依赖于IIS。在IIS上,OWIN使用一些动态注入模块插入IIS管道,但它基本上完全接管了应用程序的身份验证/授权过程。因此,为了使用ASP.NET Identity,您首先要做的是在web.config中关闭应用程序的标准IIS身份验证。
<system.web>
<authentication mode="None" />
<system.web>
我最初没有这样做,我花了一些时间来弄清楚为什么我的应用程序不断浏览浏览器身份验证对话框而不是导航到我的登录页面或将我发送到外部提供商登录。确保标准身份验证已关闭!
添加NuGet包
如果您的现有项目没有安装任何身份功能,则需要将正确的程序集放入项目中。如果你看一个新创建的MVC项目,并在那里组装的一连串不是很明显什么实际需要得到公正的基本特征。
幸运的是,由于NuGet的强大功能,为了获得核心身份功能,您只需添加以下软件包即可获得核心身份功能所需的所有引用以及所需的一些外部提供程序。
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Cookies
前两个包足以点亮OWIN身份框架。如果您没有在IIS上运行,请使用Microsoft.Owin.Host.SelfHost而不是SystemWeb主机。
您还需要安装特定的外部提供程序包:
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.Twitter
- Owin.Security.Providers
提供程序包添加了对您可以登录的特定外部提供程序的支持。该Owin.Security.Providers 包是一个第三方库,其中包括一吨,你可以用集成附加供应商,这就是我用来支持GitHub上登录,因为这是一个开发人员为主的网站。
启动类:提供程序配置
下一步是配置OWIN管道以实际处理各种登录解决方案。为此,您必须创建一个配置各种身份验证机制的Startup类。就我而言,我支持本地用户身份验证(Cookie Auth)以及Google,Twitter和GitHub的外部提供商。
这是配置代码:
namespace CodePasteMvc
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/LogOn")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// App.Secrets is application specific and holds values in CodePasteKeys.json
// Values are NOT included in repro – auto-created on first load
if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId))
{
app.UseGoogleAuthentication(
clientId: App.Secrets.GoogleClientId,
clientSecret: App.Secrets.GoogleClientSecret);
}
if (!string.IsNullOrEmpty(App.Secrets.TwitterConsumerKey))
{
app.UseTwitterAuthentication(
consumerKey: App.Secrets.TwitterConsumerKey,
consumerSecret: App.Secrets.TwitterConsumerSecret);
}
if (!string.IsNullOrEmpty(App.Secrets.GitHubClientId))
{
app.UseGitHubAuthentication(
clientId: App.Secrets.GitHubClientId,
clientSecret: App.Secrets.GitHubClientSecret);
}
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
}
}
这是OWIN启动类的实现。当OWIN运行时启动时,它通过Reflection查找具有Configuration(IAppBuilder app)方法的名为Startup的类,并在发现它执行该方法时。
然后,ConfigureAuth()为本地登录配置Cookie身份验证,为Google,Twitter和Github配置外部提供程序。我使用自定义ApplicationConfiguration类使用我的ApplicationConfiguration类将我的秘密值保存在JSON文件中(主要是为了使它们远离GitHub仓库),但您可以对这些值进行硬编码或从配置设置中读取它们。
每个提供程序都需要相应的应用程序ID和必须配置的密钥。为了使用外部提供程序,您必须在提供程序开发人员Web站点为每个提供程序创建一个应用程序,然后选择这些应用程序生成的密钥。以下是每个网站的链接(假设您已登录到每个网站):
请注意,您可以选择提供商。您只能在此站点上使用Cookie身份验证,或仅使用外部或两者组合。
自定义AccountController
我将讨论的与这些登录相关的所有代码都位于MVC控制器中 - 特别是AccountController。实际上需要实现3个主要功能集 - 本地登录,外部登录和实际登录/注销操作。以下是AccountController类中的大致内容:
还有一些更传统的Controller操作可以处理我的实现的一些支持功能 - 密码恢复和帐户激活。这是我的整个实现,因此您可以看到它比完全(矫枉过正?)身份实现相当简单 - 而且功能较少 - 您可以在默认的完整功能MVC站点中获得,这对于这篇文章来说是完美的。
登录和退出
我将首先登录和退出,因为它是所有其他操作都将使用的核心功能。只要本地或外部登录成功,用户就会登录,并且此过程实际上会创建标识用户的身份验证Cookie,并允许Identity框架确定用户是否已登录并为每个请求设置User Principal对象。
如果您在ASP.NET中使用了FormsAuthentication,则您知道有一个全局对象可以处理用户跟踪cookie的管理,该cookie将用户与帐户相关联。OWIN在IAuthenticationManager接口中有自己的身份验证管理器版本,该接口附加到HttpContext对象。要获得对它的引用,您可以使用:
HttpContext.GetOwinContext().Authentication;
此对象处理用于通过站点跟踪用户的安全cookie的创建和删除。身份cookie用于跟踪所有登录用户,无论他们是使用用户名和密码在本地登录还是使用Google等外部提供商。一旦用户通过身份验证,就会调用SignIn方法来创建cookie。在随后的请求中,基于OWIN的Identity子系统然后选择Cookie并在用户访问您的站点时向用户授权基于用户的适当IPrinciple(ClaimsPrinciple with ClaimsIdentity)。
身份登录使用ClaimsIdentity对象,其中包含存储在声明中的用户信息。声明提供了用户ID和名称以及要与经过身份验证的用户一起存储为缓存状态的任何其他信息。
为了简化登录,我使用了几个如下所示的辅助函数:
public void IdentitySignin(AppUserState appUserState, string providerKey = null, bool isPersistent = false)
{
var claims = new List<Claim>();
// create required claims
claims.Add(new Claim(ClaimTypes.NameIdentifier, appUserState.UserId));
claims.Add(new Claim(ClaimTypes.Name, appUserState.Name));
// custom – my serialized AppUserState object
claims.Add(new Claim("userState", appUserState.ToString()));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = isPersistent,
ExpiresUtc = DateTime.UtcNow.AddDays(7)
}, identity);
}
public void IdentitySignout()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie,
DefaultAuthenticationTypes.ExternalCookie);
}
private IAuthenticationManager AuthenticationManager
{
get { return HttpContext.GetOwinContext().Authentication; }
}
登入/ SignOut
关键方法是AuthenticationManager上的SignIn()和SignOut(),它们在执行请求上创建或删除应用程序cookie。SignIn()接受一个Identity对象,其中包含您为其分配的任何声明。一旦用户登录,您就会收到此身份,稍后您会查看Context.User.Identity以检查授权。
关于AppUserState的一个词
Note I’m using an application specific AppUserState class to represent the logged in user’s state which is then added to the Identity’s claims! This is a custom object in my application that basically holds basic User that are the bare minimum needed by the application to display user info like name, admin status, theme etc. This object is persisted and cached inside of the ClaimsIdentity claims and therefore in the cookie, so that the data is available without having to look up a user in the database for each request.
Above I show how the AppUserState is persisted in the identity cookie. For retrieval and storage in a property on the controller, I have a base controller that has an internal AppUserState property that is loaded up from a valid ClaimsPrincipal when a request comes in in BaseController.Initialize():
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
// Grab the user's login information from Identity
AppUserState appUserState = new AppUserState();
if (User is ClaimsPrincipal)
{
var user = User as ClaimsPrincipal;
var claims = user.Claims.ToList();
var userStateString = GetClaim(claims, "userState");
//var name = GetClaim(claims, ClaimTypes.Name);
//var id = GetClaim(claims, ClaimTypes.NameIdentifier);
if (!string.IsNullOrEmpty(userStateString))
appUserState.FromString(userStateString);
}
AppUserState = appUserState;
ViewData["UserState"] = AppUserState;
ViewData["ErrorDisplay"] = ErrorDisplay;
}
The net effect is that anywhere within Controller and most Views this AppUserState object is available for user info display or visual display options.
This approach made great sense when I was using FormsAuthentication because you could effectively only store a single string value which was the serialized AppUserState value. But now ClaimsIdentity can contain multiple values as claims explicitly as a dictionary so it may be much cleaner to simply store any values as claims on the ClaimsIdentity. In the future the AppUserState code could probably be abstracted away, using a custom ClaimsIdentity instead that knows how to persist and retrieve its state from the attached claims instead. I’ll leave that exercise for another day though because AppUserState is used widely in this application.
Either way I want to make it clear that AppUserState is a custom implementation and an application specific implementation detail for my application.
使用Cookie进行本地登录
每种登录类型有两个步骤:最初创建用户和帐户,然后实际登录用户。因此,让我们从本地登录的注册过程开始。
以下是用户注册表单的内容:
表单顶部包含本地登录,而底部包含外部提供程序登录。在此表单中,您可以选择使用外部登录进行注册,该登录使用可能从外部提供商处接收的任何数据填充本地用户注册表单。Google提供电子邮件地址,GitHub包括电子邮件和姓名,而Twitter仅提供名称。在任何一种情况下,都会在应用程序中创建新用户。
响应“ 注册”按钮的代码实际上创建了一个新帐户(如果没有验证错误),并对用户进行签名。
以下是本地用户登录代码的外观(请记住此特定于应用程序):
AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult Register(FormCollection formVars)
{
string id = formVars["Id"];
string confirmPassword = formVars["confirmPassword"];
bool isNew = false;
User user = null;
if (string.IsNullOrEmpty(id) || busUser.Load(id) == null)
{
user = busUser.NewEntity();
user.InActive = true;
isNew = true;
}
else
user = busUser.Entity;
UpdateModel<User>(busUser.Entity,
new string[] { "Name", "Email", "Password", "Theme" });
if (ModelState.Count > 0)
ErrorDisplay.AddMessages(ModelState);
if (ErrorDisplay.DisplayErrors.Count > 0)
return View("Register", ViewModel);
if (!busUser.Validate())
{
ErrorDisplay.Message = "Please correct the following:";
ErrorDisplay.AddMessages(busUser.ValidationErrors);
return View("Register", ViewModel);
}
if (!busUser.Save())
{
ErrorDisplay.ShowError("Unable to save User: " + busUser.ErrorMessage);
return View("Register", ViewModel);
}
AppUserState appUserState = new AppUserState();
appUserState.FromUser(user);
IdentitySignin(appUserState, appUserState.UserId);
if (isNew)
{
SetAccountForEmailValidation();
ErrorDisplay.HtmlEncodeMessage = false;
ErrorDisplay.ShowMessage(@"Thank you for creating an account...");
return View("Register", ViewModel);
}
return RedirectToAction("New","Snippet");
}
这段代码与我们以前的工作方式并没有太大的不同。您检查保存是否是我们在系统中已有的用户,如果是,请更新她,或者创建一个新用户并更新新用户。
这里的关键项不同之处仅在于我如前所述设置AppUserState,然后创建调用IdentitySignIn()来验证该用户。在随后的点击中,我然后获得一个Context.User,其中包含该用户的ClaimsIdentity。
登录本地帐户
注册帐户后,您实际上可以登录。以下是登录表单的外观:
处理登录的代码如下所示:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string email, string password, bool rememberMe, string returnUrl, bool emailPassword)
{
if (emailPassword)
return EmailPassword(email);
var user = busUser.ValidateUserAndLoad(email, password);
if (user == null)
{
ErrorDisplay.ShowError(busUser.ErrorMessage);
return View(ViewModel);
}
AppUserState appUserState = new AppUserState()
{
Email = user.Email,
Name = user.Name,
UserId = user.Id,
Theme = user.Theme,
IsAdmin = user.IsAdmin
};
IdentitySignin(appUserState, user.OpenId, rememberMe);
if (!string.IsNullOrEmpty(returnUrl))
return Redirect(returnUrl);
return RedirectToAction("New", "Snippet", null);
}
您可能会再次猜测,这里的代码只是查找用户名和密码,如果有效,则更新AppUserState对象,然后调用IdentitySignin()来记录用户。
这些工作流程与我以前使用FormsAuthentication所做的完全不同。唯一真正的区别是我正在调用 IdentitySignin()而不是FormsAuthentication.Authenticate()。
现在开始你以前不容易做的有趣的东西 - 外部登录。
链接到外部登录
正如您在上面的屏幕截图中看到的那样,注册和登录表单都支持使用外部提供程序来处理帐户的身份验证。对于注册表单,可以在注册开始时执行外部登录,以从外部提供者预填充注册信息,或者将外部登录附加到现有本地帐户。
对于外部登录,当您单击任何提供程序按钮时,您将被重定向到提供程序站点(Google,Twitter,GitHub),该站点将检查您的帐户是否已登录。如果不是,您将被移至提供程序登录他们的服务器上的页面,您可以登录和/或指定您希望为请求登录的应用程序(即我的站点)提供哪种权限。单击“接受”后,服务器将在您的服务器上触发回调请求,并提供提供程序可用的声明。通常,这是提供者密钥(用户登录的标识符)以及名称或电子邮件或两者。
外部登录通过OAuth2流程处理,该流程由ASP.NET中的OWIN身份验证管道内部管理。当请求被触发时,它们包括一个首先触发到OWIN处理程序的回调URL。回调网址为/ signin-google或/ signin-twitter或/ signin-github。
在本地帐户和外部帐户之间创建初始帐户链接以及日志记录都有两个端点请求流:一个是通过质询操作(实际上是重定向)实际启动远程身份验证过程,另一个是接收身份验证完成后的回调。
这是第一种通过挑战和重定向到提供者来启动外部帐户链接的方法:
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLinkLogin(string provider)
{
// Request a redirect to the external login provider to link a login for the current user
return new ChallengeResult(provider, Url.Action("ExternalLinkLoginCallback"), AppUserState.UserId);
}
ChallengeResult是一个帮助器类,它是ASP.NET MVC默认控制器实现的一部分,我从中简单地复制它:
private const string XsrfKey = "CodePaste_$31!.2*#";
public class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUri)
: this(provider, redirectUri, null)
{ }
public ChallengeResult(string provider, string redirectUri, string userId)
{
LoginProvider = provider;
RedirectUri = redirectUri;
UserId = userId;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
properties.Dictionary[XsrfKey] = UserId;
var owin = context.HttpContext.GetOwinContext();
owin.Authentication.Challenge(properties, LoginProvider);
}
}
此类的关键是OWIN Authentication.Challenge()方法,该方法向提供程序发出302重定向,以使用包含重定向URL和某些状态信息的URL处理登录。在这种情况下,状态是一个用户标识符(在这种情况下是我们的用户ID),它允许我们检查并确保结果是我们感兴趣的结果。
当提供程序验证(或无法验证)用户时,它会使用特定URL回调您的服务器。回调的URL是〜/ signin-google或〜/ signin-github或〜/ signin-twitter。OWIN管道在内部为您处理此回调,并在验证重定向到您的实际端点之后,以便您可以处理经过身份验证的请求。
为了说明在登录我的Google帐户时查看此注册链接请求的Fiddler跟踪:
注意所有302个请求。第一个请求由您的代码使用ChallengeResult启动,后者会重定向到Google。然后,Google会重定向回OWIN内部端点以处理提供程序OAuth解析,最后使用ExternalLinkLoginCallback()将OWIN管道调用到您的代码中。
这是链接过程完成时调用的Callback方法:
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult> ExternalLinkLoginCallback()
{
// Handle external Login Callback
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, AppUserState.UserId);
if (loginInfo == null)
{
IdentitySignout(); // to be safe we log out
return RedirectToAction("Register", new {message = "Unable to authenticate with external login."});
}
// Authenticated!
string providerKey = loginInfo.Login.ProviderKey;
string providerName = loginInfo.Login.LoginProvider;
// Now load, create or update our custom user
// normalize email and username if available
if (string.IsNullOrEmpty(AppUserState.Email))
AppUserState.Email = loginInfo.Email;
if (string.IsNullOrEmpty(AppUserState.Name))
AppUserState.Name = loginInfo.DefaultUserName;
var userBus = new busUser();
User user = null;
if (!string.IsNullOrEmpty(AppUserState.UserId))
user = userBus.Load(AppUserState.UserId);
if (user == null && !string.IsNullOrEmpty(providerKey))
user = userBus.LoadUserByProviderKey(providerKey);
if (user == null && !string.IsNullOrEmpty(loginInfo.Email))
user = userBus.LoadUserByEmail(loginInfo.Email);
if (user == null)
{
user = userBus.NewEntity();
userBus.SetUserForEmailValidation(user);
}
if (string.IsNullOrEmpty(user.Email))
user.Email = AppUserState.Email;
if (string.IsNullOrEmpty(user.Name))
user.Name = AppUserState.Name ?? "Unknown (" + providerName + ")";
if (loginInfo.Login != null)
{
user.OpenIdClaim = loginInfo.Login.ProviderKey;
user.OpenId = loginInfo.Login.LoginProvider;
}
else
{
user.OpenId = null;
user.OpenIdClaim = null;
}
// finally save user inf
bool result = userBus.Save(user);
// update the actual identity cookie
AppUserState.FromUser(user);
IdentitySignin(AppUserState, loginInfo.Login.ProviderKey);
return RedirectToAction("Register");
}
这段代码中有相当多的代码,但是......大多数代码都是特定于应用程序的。代码中最重要的部分是第一行,它负责返回包含providerKey的LoginInfo对象以及提供程序提供的任何其他声明。通常这些是名称和/或电子邮件。然后,其余代码将检查用户是否已存在并更新它,或者是否创建新用户并保存。如果全部通过在后续请求中创建cookie和授权的ClaimsIdentity来调用IdenitySignin()来有效地登录用户。
完成后,用户已登录,但我想重新显示注册表单以显示外部帐户注册:
使用外部登录登录
最后,我们仍然需要连接逻辑以使用外部提供程序登录。与链接提供程序一样,这是一个两步过程 - 触发初始身份验证请求。与链接操作一样,质询请求处理此问题:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider)
{
string returnUrl = Url.Action("New","Snippet",null);
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback",
"Account", new { ReturnUrl = returnUrl }));
}
同样发出相同的302个请求以最终将结果带回到OWIN管道,而OWIN管道又重定向到ExternalLoginCallback()方法:
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
return RedirectToAction("LogOn");
// AUTHENTICATED!
var providerKey = loginInfo.Login.ProviderKey;
// Aplication specific code goes here.
var userBus = new busUser();
var user = userBus.ValidateUserWithExternalLogin(providerKey);
if (user == null)
{
return RedirectToAction("LogOn", new
{
message = "Unable to log in with " + loginInfo.Login.LoginProvider +
". " + userBus.ErrorMessage
});
}
// store on AppUser
AppUserState appUserState = new AppUserState();
appUserState.FromUser(user);
IdentitySignin(appUserState, providerKey, isPersistent: true);
return Redirect(returnUrl);
}
该过程类似于前一个示例中的链接操作,除了登录代码只是检查用户是否有效,如果是,则使用现在熟悉的IdentySignin()方法将其登录。
此时用户已登录,因此只需重定向我们想去的地方。
取消帐户关联
最后,如果您要取消帐户关联,我所要做的就是删除域模型中的链接。在我的情况下,我删除OpenId和OpenIdClaim的字段值,以便使用外部帐户的任何后续登录都将失败,因为我们将不匹配外部提供程序提供的提供程序密钥。
这是unlink操作的代码:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ExternalUnlinkLogin()
{
var userId = AppUserState.UserId;
var user = busUser.Load(userId);
if (user == null)
{
ErrorDisplay.ShowError("Couldn't find associated User: " + busUser.ErrorMessage);
return RedirectToAction("Register", new { id = userId });
}
user.OpenId = string.Empty;
user.OpenIdClaim = string.Empty;
if (busUser.Save())
return RedirectToAction("Register", new { id = userId });
return RedirectToAction("Register", new { message = "Unable to unlink OpenId. " + busUser.ErrorMessage });
}
执行此代码后,外部帐户链接消失。我再次显示注册页面,这次外部链接帐户不再显示由三个外部提供者再次替换,其中一个可以连接。
这就是所有的基础操作!
快速回顾
您可能认为这是很多代码,您必须编写一些非常简单的代码。但请记住,此代码包含我在此提供的一些特定于应用程序的逻辑,以便提供一些有点逼真的上下文。虽然实际的底层身份代码非常小(并且我以粗体突出显示了代码段中的核心要求),但此处显示的代码可能是您可能希望在其中运行的基本自我管理用户管理实现所需的最低限度真正的应用。
如果您想查看完整的控制器代码,可以在Github上查看。
总之,你基本上处理:
IdentitySignIn,IdentitySignOut,用于标准LogIn / LogOut函数
实现ExternalLinkLogin()和ExternalLinkLoginCallback()
实现ExternalLogin()和ExternalLoginCallback()
各种ExternalXXXX方法遵循一个简单的样板,使用ChallengeResult作为初始请求,并调用GetExternalLoginInfoAsync()来获取结果数据。只要你知道实际需要实现的部分,它就非常直接。
最小代码摘要
因为上面的代码非常冗长,所以这里只是基本部分实现的相关部分的摘要:
启动配置类
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/LogOn")
});
// these values are stored in CodePasteKeys.json
// and are NOT included in repro - autocreated on first load
if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId))
{
app.UseGoogleAuthentication(
clientId: App.Secrets.GoogleClientId,
clientSecret: App.Secrets.GoogleClientSecret);
}
if (!string.IsNullOrEmpty(App.Secrets.TwitterConsumerKey))
{
app.UseTwitterAuthentication(
consumerKey: App.Secrets.TwitterConsumerKey,
consumerSecret: App.Secrets.TwitterConsumerSecret);
}
if (!string.IsNullOrEmpty(App.Secrets.GitHubClientId))
{
app.UseGitHubAuthentication(
clientId: App.Secrets.GitHubClientId,
clientSecret: App.Secrets.GitHubClientSecret);
}
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
}
IdentitySignIn / SignOut
public void IdentitySignin(string userId, string name, string providerKey = null, bool isPersistent = false)
{
var claims = new List<Claim>();
// create *required* claims
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
claims.Add(new Claim(ClaimTypes.Name, name));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
// add to user here!
AuthenticationManager.SignIn(new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = isPersistent,
ExpiresUtc = DateTime.UtcNow.AddDays(7)
}, identity);
}
public void IdentitySignout()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie,
DefaultAuthenticationTypes.ExternalCookie);
}
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
外部链接登录
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLinkLogin(string provider) //Google,Twitter etc.
{
return new ChallengeResult(provider, Url.Action("ExternalLinkLoginCallback"), userId);
}
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult> ExternalLinkLoginCallback()
{
// Handle external Login Callback
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey,userId);
if (loginInfo == null)
{
IdentitySignout(); // to be safe we log out
return RedirectToAction("Register", new {message = "Unable to authenticate with external login."});
}
// Authenticated!
string providerKey = loginInfo.Login.ProviderKey;
string providerName = loginInfo.Login.LoginProvider;
// Your code here…
// when all good make sure to sign in user
IdentitySignin(userId, name, providerKey, isPersistent: true);
return RedirectToAction("Register");
}
此代码和外部登录还需要ChallengeResult帮助程序类:
// Used for XSRF protection when adding external logins
private const string XsrfKey = "CodePaste_$31!.2*#";
public class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUri)
: this(provider, redirectUri, null)
{ }
public ChallengeResult(string provider, string redirectUri, string userId)
{
LoginProvider = provider;
RedirectUri = redirectUri;
UserId = userId;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
properties.Dictionary[XsrfKey] = UserId;
var owin = context.HttpContext.GetOwinContext();
owin.Authentication.Challenge(properties, LoginProvider);
}
}
外部登录
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider)
{
string returnUrl = Url.Action("New","Snippet",null);
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback",
"Account", new { ReturnUrl = returnUrl }));
}
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
if ( string.IsNullOrEmpty(returnUrl) )
returnUrl = "~/";
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
return RedirectToAction("LogOn");
// AUTHENTICATED!
var providerKey = loginInfo.Login.ProviderKey;
// Your code goes here.
// when all good make sure to sign in user
IdentitySignin(userId, name, providerKey, isPersistent: true);
return Redirect(returnUrl);
}
本地帐户注册
[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult Register(FormCollection formVars)
{
// Capture User Data and Create/Update account
// when all good make sure to sign in user
IdentitySignin(userId, name, appUserState.UserId);
return RedirectToAction("New","Snippet",null);
}cs
本地帐户登录
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string email, string password, bool rememberMe, string returnUrl, bool emailPassword)
{
// validate your user
// if all OK sign in
IdentitySignin(userId, name, user.OpenId, rememberMe);
return RedirectToAction("New", "Snippet", null);
}
public ActionResult LogOff()
{
IdentitySignout();
return RedirectToAction("LogOn");
}
摘要
对于新项目而言,无论是想要自己动手,还是坚持使用股票身份实施,都可能需要长时间的思考。就个人而言,我不是开箱即用的基于EF的实现的粉丝。虽然可以自定义,但仍需要大量调整UI以使其适合您的应用程序,并可能添加和删除Identity模型中的字段。最糟糕的是,默认的EF依赖关系不容易集成到另一个EF模型中。就个人而言,我更喜欢将用户管理集成为我自己的应用程序域模型的一部分,而不是将用户链接到客户。
使用自己的另一个好处是,你不会坚持微软的想法。我们经历了太多的身份验证框架,微软似乎在每个主要的发布周期都在改变模型。虽然新身份可能是他们曾经拥有的最接近实际可用的东西,但是我仍然需要依赖微软在这方面所做的任何事情,因为他们害怕将它从下面拉出来我在下一个版本。使用我自己的,我不必担心至少可以随我的应用程序一起旅行的用户管理功能。使用我自己的实现,我可能会有更多的设置,但至少我有一个标准的方法,我可以轻松地通过任何版本的ASP.NET的应用程序继续。
最后我不得不说,虽然我花了很长时间才能完全理解我需要实施的东西,但并不是很困难。困难的部分只是通过挖掘生成代码并删除相关部分来找到您需要实现的内容的正确信息。一旦您知道需要什么,实际代码片段的实现就相对简单了。
我希望这篇文章能够很好地总结所需内容,尤其是最小代码摘要,创建将自己的域驱动用户管理插入核心身份框架所需的框架代码会更容易。
资源
理性之声
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
我热衷于将.NET应用程序与Identity Server集成,后者又包含多个服务和身份提供程序,但是没有太多材料可以解释如何在.NET中执行此操作。
好的帖子顺便说一下!
谢谢。
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
http://stackoverflow.com/questions/28704700/updating-owin-from-2-1-to-3-0-1-breaks-external-auth
我注意到你正在从AuthenticationManager调用GetExternalLoginInfoAsync。我正在使用GetAuthenticationResult。知道有什么区别吗?
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
我没有密切关注较低级别的AuthenticationManager方法,看看代码是否需要使用ExternalLinkLoginInfoAsync()。你有没有使用它?另请注意使用app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie)而不是app.SetDefaultSignInAsAuthenticationType()。
尽管如此,听到这件事打破了你真的很糟糕 - 真的不应该发生与这个不可或缺的组件。
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
IAuthenticationManager AuthenticationManager
{
get {return HttpContext.GetOwinContext()。}
}
此外,如果你没有使用的System.Web这是常见的在MVC项目中,你将有一个更加困难的时间。告诉你的母亲。
弗洛伊德
2015年9月2日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
很难找到这样好的和有用的文章。与MVC5身份验证相关的每个论坛或问题都与默认的OWIN内容有关。这是我为自定义Authentication Mechanizm找到的最佳解决方案。
我玩了代码,现在为我工作%100。你肯定应该写更多关于这个设置的文章。可能是您可以在单个用户中添加授权,角色,多个角色,如何在视图中向不同的用户或角色显示不同的内容,或者像这样思考。
你是英雄!
干杯。
山药
2015年9月11日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
问候!
Neil Moss
2015年9月12日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
我删除了所有的Microsoft.AspNet.Identity包 - 我走得太远了吗?如果我只是注册你建议的nuGet包,编译器根本不喜欢Startup部分。
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
IAppBuilder下不存在UseExternalSignInCookie属性
//使应用程序能够使用cookie存储已登录用户
app的信息 .UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(“/ Account / LogOn”)
});
在类Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions中,AuthenticateType是一个字符串而不是枚举。
等等..!
你能澄清一下你正在使用的nuGet包和版本以及你正在使用的命名空间吗?
谢谢
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
如果你这样做,最好找出程序集是创建一个新项目添加nuget引用,然后复制包列表。
Neil Moss
2015年9月13日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
根据帖子中的说明进行了双重检查,发现没有遗漏,我去了CodePaste.NET GitHub,看看那里到底发生了什么。(在打扰你之前应该这样做,但是......好吧,下次)。
我遗失的包裹如下:
< package id =“Microsoft.AspNet.Identity.Core” version =“2.2.1” targetFramework =“net451” /> < package id =“Microsoft.AspNet.Identity.Owin” version =“2.2.1” targetFramework =“net451” />
从帖子的标题和大致方向来看,我认为我们不需要Microsoft.AspNet.Identity包,因为我们只使用最小的OWIN类。
不过,现在起来,继续前行,对你们来说,这是非常重要的。
问候。
史蒂夫
2015年10月7日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
Constantinos Haskas
2015年10月07日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
约翰A戴维斯
2015年10月25日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
但是你的代码和微软的疯狂科学家一样疯狂。他们肯定会让人们远离使用MS编程语言。
if(!string .IsNullOrEmpty(App.Secrets.GoogleClientId))
{
app.UseGoogleAuthentication(
clientId:App.Secrets.GoogleClientId,
clientSecret:App.Secrets.GoogleClientSecret);
}
我的项目中没有任何地方可以通过红色下划线获得“App.Secrets.GoogleClientId”。“在当前的背景下,App名称不存在。”
我今天醒来以为我终于可以继续在我的应用程序中实际处理我的CRUD内容了。
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
Rafe Kemmis
2015年11月15日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
珍妮
2016年1月21日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
Uday
2016年2月4日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
如果想深入了解OWIN以及如何将它与.NET一起使用,这是最好的教程或参考书。
可以请一些人建议。
veysel
2016年2月26日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
我想要另一篇关于使用新的asp.net身份模型进行自定义角色身份验证的文章。
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
我是从Asp.net论坛导演来的。
我有一个现有的ASP.Net Web应用程序。它不是* MVC!
将(应该)这里的步骤使我的ASP.Net Web应用程序允许我的成员使用OWin登录或者我是否会遇到其他问题?
如果可以,在成员使用OWin进程登录后,我需要做什么才能将该登录标识与我的应用程序的SQL数据库中的成员相匹配,在该数据库中我按照他们的电子邮件地址存储我的所有成员?
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
该帖子描述了一个不使用默认EF身份实现的场景 - 它仅使用oWin组件,但没有任何授权或用户管理部分延迟在应用程序中存储。
ecesari
2016年5月21日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
你对如何将MVC网站与app mobile整合了吗?
我的意思是......现在我可以在网络上进行外部登录了mvc,我将一些功能移到了web api和I0m triyng,也使用自定义(xamarin)APP进行连接。什么是正确的方法?我想让我的APP登录到我的mvc后端,使用谷歌凭据
你有什么想法吗?
非常感谢
拉古
2016年6月21日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
dpmragu
2016年6月21日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
感谢您阅读本文,并帮助我在MVC5中实现OWIN身份验证。您能否指导我在申请退出后如何使claimIndentity无效?
Afshin
2016年9月16日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
我有一个MVC4网站,目前使用简单的会员资格和SQL服务器进行用户管理。我的一些用户已通过Facebook验证。我想升级到OWIN身份以使谷歌身份验证工作。我想知道OWIN是否可以使用sql server中的现有用户信息,或者它有不同的模式?
谢谢
Dwayne
2016年9月23日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
Danny Scheelings
2016年9月25日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
只是想对此博客条目表示衷心的感谢。它涵盖了很多像StackOverflow和其他博客这样的小东西错过了。
让我的生活更轻松!
谢谢。
山姆。
安吉拉
2016年11月21日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
只需为第三方提供商登录的新用户添加评论...在其所述的代码中
// these values are stored in CodePasteKeys.json
// and are NOT included in repro - autocreated on first load
if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId))
{
app.UseGoogleAuthentication(
clientId: App.Secrets.GoogleClientId,
clientSecret: App.Secrets.GoogleClientSecret);
}
该应用程序未解决。它可能是一个将他的json文件读入的类。
无论哪种方式,我都不熟悉第三方提供商。所以我在网上搜索了clientId和Google身份验证,我发现你必须向网站应用程序注册这些提供商,这样你才能与他们互动。在谷歌的情况下,他们会给你一个clientid和客户秘密,你将在联系他们验证用户时使用。
只是想在这里放一个注释,以防其他不熟悉使用第三方登录的人不明白。我会说如果你不使用第三方登录,你可以注释掉那段代码。这就是我现在正在做的事情。
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
真棒文章你有我的理智!!!! 这是关于这个主题的最好的文章之一。我整个周末都在研究以下解决方案:对LDAP和Windows Azure Active Directory使用本地身份验证。只是给每个人的一个注释我必须将OpenIdConnectAuthentication置于被动模式,以便它不会立即尝试向WAAD进行身份验证。
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Passive,
肥皂盒 - 像OWIN这样的框架非常棒,当它成为常见问题但是当你需要走出人迹罕至的道路时却是一场噩梦!
马克H
2017年3月5日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
谢谢你这篇文章。在我意识到编写自己的库更容易,更快之前,我开始实现Owin。
OAuth不是一个难以实现的协议。但是当需要这么少的代码来实现一个库时,它对其他不必要的库有很深的依赖性,需要很多入口点和外部代码,你就知道出了什么问题。应用程序启动配置代码也不适用于多租户应用程序。这个中间件只是一个过度设计的混乱。
我实施了几个提供商。每个人在OAuth上都有自己的轻微扭曲/怪癖,但我只需要为我的库中的每个提供者覆盖3个属性和基类的方法,这些方法比不同的更相似。我只是传入HttpRequest,提供者,并向Web应用程序添加一个新路由,然后查找ProfileInfo类最终返回(或错误)。几行可读代码。
Owin对于监视请求和响应URL非常有用,但我建议您自己编写代码。否则它的大锤就会破解坚果。
JP
2017年5月10日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
嗨,非常感谢这篇文章,很难找到这个,而不是整个Identity + EF实现。我有一个问题,关于cookie超时并将其呈现给用户。
我一直在遵循这种方法:http: //stackoverflow.com/questions/23090706/how-to-know-when-owin-cookie-will-expire
基本上我用SlidingExpiration = true设置我的cookie,然后,在用户登录后,我通过ajax请求获取当前剩余的cookie有效时间,但通过执行该请求(定期运行1秒左右)然后cookie本身会更新并永不过期,您是否有任何建议向用户提供该信息,而不是将ajax请求与用户的请求混淆?
提前致谢!!
詹姆斯法尔
2017年6月28日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
这篇文章很棒,正是我需要的!我花了几个小时阅读许多不同网站上的碎片信息,根本没有显示结果。
阅读完本文后,只需不到15分钟的时间即可登录!
非常感谢您花时间写这篇文章,非常感谢
詹姆士
Marcel Slats
2017年8月15日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
最后发现了一篇文章,并没有指导我到新的项目向导来启动一个完整的项目,而没有告诉我什么做了什么。
通过本文,我了解了正在发生的事情以及如何在不使用EF的情况下处理结果。
优秀的文章,谢谢。
鲍勃
2017年11月6日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
嗨里克,你必须一直听到这个,但是这篇文章让我的生活变得轻松!这是第一篇展示从头开始实现所需部件的文章及其原因。我搜索了几个小时,大多数搜索结果已经进入了杂草!再次感谢!并继续撰写文章。这不是我读过的第一个,也不是最后一个!问候。
Vinod
,2018年3月12日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
您好,非常好,关于主题文章,正如您在文章开头提到的那样“很多关于如何使用这些东西的介绍文章'没有自定义'。但是没有很多关于使用核心的信息没有完整的UserManager和Entity Framework数据存储的标识片段,以便仅使用身份验证/授权并将它们与我自己的业务对象/域模型集成。“ 然后它真的让我觉得我并不孤单。
终于得到了我想要的东西,但现在我有几个问题
- 过期时间设置在两个位置(在启动类中,另一个在Signin方法中的AuthenticationProperties中),两者之间有什么区别,哪一个优先?
- 当另一件事先于其他事物在上述问题中过期时会发生什么事情。
- AllowRefresh = true如何在SignIn方法中起作用
- 您在上面提到过“身份cookie用于跟踪所有已登录的用户”,请您解释一下这个问题。我们可以基于它通知其他用户有关用户的在线/离线,如果是,那么如何使用SlidingExpiration用于此目的?
非常感谢这篇精彩的文章和再见那些庞大而充满EF和基于默认模板的代码解释主题😃
Stephen Grattan
,2018年5月22日
# 重:添加最小OWIN身份认证到现有的ASP.NET MVC应用程序
嗨瑞克,很棒的文章,但我有一个问题,并想知道你是否仍在监视这篇文章。我甚至在登录后都知道为什么:
HttpContext.GetOwinContext()。Authentication.SignIn(new AuthenticationProperties(),identity);
任何后续请求(甚至重定向)都未经过身份验证/授权。
我误解了什么吗?任何帮助将非常感谢。
本博客Android APP 下载 |
![]() |
支持我们就给我们点打赏 |
![]() |
支付宝打赏 支付宝扫一扫二维码 |
![]() |
微信打赏 微信扫一扫二维码 |
![]() |
如果想下次快速找到我,记得点下面的关注哦!
Jonathas Morais
2015年4月29日