初学asp.net,一般会用session结合cookie来做登录后的安全认证,或者用Forms身份认证。但这两者用在WebApi中就不太合适了,新建一个默认的WebApi项目,会自带用Owin作为安全认证,但里面的一些结构和类有些难理解,也不怎么会用,不太习惯里面的一些model用法,看了很多概念,但这个默认的项目如何用呢?怎么获取token访问呢?用户密码又是如何传递去验证的呢?
下面是试着研究一下,这里只讲一下怎么会用,至于一些OWin、OAth2.0的概念就自己百度一下吧。
1.新建一个asp.net Web应用程序,然后选择WebApi项目
基本的目录结构如下
2.在新建一个默认的WebApi项目的同时,会自动生成一个数据库,数据库的文件就存放在APP_Data中,查看Web.config可以找到数据库连接字符串找到生成的数据库名称,类似下面
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-WebApiTest-20170715031110.mdf;Initial Catalog=aspnet-WebApiTest-20170715031110;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
登录本地数据库可以看到里面的表结构,这里可以看到AspNetUsers
应该就是保存用户名和密码的表了。
3.下面来看看一些主要的方法,首先打开/App_Start/Startup.Auth.cs
,这个是安全认证入口的配置方法,看下代码
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Google;
using Microsoft.Owin.Security.OAuth;
using Owin;
using WebApiTest.Providers;
using WebApiTest.Models;
namespace WebApiTest
{
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static string PublicClientId { get; private set; }
// 有关配置身份验证的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// 将数据库上下文和用户管理器配置为对每个请求使用单个实例
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// 使应用程序可以使用 Cookie 来存储已登录用户的信息
// 并使用 Cookie 来临时存储有关使用第三方登录提供程序登录的用户的信息
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// 针对基于 OAuth 的流配置应用程序
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
//在生产模式下设 AllowInsecureHttp = false
AllowInsecureHttp = true
};
// 使应用程序可以使用不记名令牌来验证用户身份
app.UseOAuthBearerTokens(OAuthOptions);
// 取消注释以下行可允许使用第三方登录提供程序登录
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
//app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
//{
// ClientId = "",
// ClientSecret = ""
//});
}
}
}
TokenEndpointPath = new PathString("/Token")
,这里的/Token是获取token的路径,也就是说通过访问这个地址会返回access_token给客户端(当然要带上所需参数),地址比如:http:localhost/Token
4.先别急,我们先来试试api的访问,Ctrl+F5运行一下项目,项目自带有一个ValuesController这个控制器,我们访问一下,我这里的路径是http://localhost:31926/api/values
,但是提示“已拒绝为此请求授权”。
为什么呢?我们打开ValuesController看看代码,可以看到这个控制器上面有一个[Authorize]
标识,说明这个控制器是需要登录后才能访问的。如果把[Authorize]
去掉,重新生成一下访问,就可以看到返回数据了。
那么问题来了,我们怎么登录?账号密码又是什么?
- 其实,我们需要先注册用户,然后才能通过用户名和密码来登录,获得token后,然后访问
http://localhost:31926/api/values
时带上access_token,就可以了。
5.打开/Controllers/AccountController.cs
,顾名思义,这个控制器是是登录验证所用的控制器了,注意看里面的方法,下面是这个文件的代码
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using WebApiTest.Models;
using WebApiTest.Providers;
using WebApiTest.Results;
namespace WebApiTest.Controllers
{
[Authorize]
[RoutePrefix("api/Account")]
public class AccountController : ApiController
{
private const string LocalLoginProvider = "Local";
private ApplicationUserManager _userManager;
public AccountController()
{
}
public AccountController(ApplicationUserManager userManager,
ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
{
UserManager = userManager;
AccessTokenFormat = accessTokenFormat;
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; }
// GET api/Account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("UserInfo")]
public UserInfoViewModel GetUserInfo()
{
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null
};
}
// POST api/Account/Logout
[Route("Logout")]
public IHttpActionResult Logout()
{
Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return Ok();
}
// GET api/Account/ManageInfo?returnUrl=%2F&generateState=true
[Route("ManageInfo")]
public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)
{
IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null)
{
return null;
}
List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
foreach (IdentityUserLogin linkedAccount in user.Logins)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = linkedAccount.LoginProvider,
ProviderKey = linkedAccount.ProviderKey
});
}
if (user.PasswordHash != null)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = LocalLoginProvider,
ProviderKey = user.UserName,
});
}
return new ManageInfoViewModel
{
LocalLoginProvider = LocalLoginProvider,
Email = user.UserName,
Logins = logins,
ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
};
}
// POST api/Account/ChangePassword
[Route("ChangePassword")]
public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword,
model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST api/Account/SetPassword
[Route("SetPassword")]
public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST api/Account/AddExternalLogin
[Route("AddExternalLogin")]
public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken);
if (ticket == null || ticket.Identity == null || (ticket.Properties != null
&& ticket.Properties.ExpiresUtc.HasValue
&& ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow))
{
return BadRequest("外部登录失败。");
}
ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity);
if (externalData == null)
{
return BadRequest("外部登录已与某个帐户关联。");
}
IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey));
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST api/Account/RemoveLogin
[Route("RemoveLogin")]
public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result;
if (model.LoginProvider == LocalLoginProvider)
{
result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId());
}
else
{
result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(model.LoginProvider, model.ProviderKey));
}
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// GET api/Account/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if (error != null)
{
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
}
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(provider, this);
}
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.LoginProvider != provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
}
// GET api/Account/ExternalLogins?returnUrl=%2F&generateState=true
[AllowAnonymous]
[Route("ExternalLogins")]
public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false)
{
IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes();
List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>();
string state;
if (generateState)
{
const int strengthInBits = 256;
state = RandomOAuthStateGenerator.Generate(strengthInBits);
}
else
{
state = null;
}
foreach (AuthenticationDescription description in descriptions)
{
ExternalLoginViewModel login = new ExternalLoginViewModel
{
Name = description.Caption,
Url = Url.Route("ExternalLogin", new
{
provider = description.AuthenticationType,
response_type = "token",
// client_id = Startup.PublicClientId,
redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri,
state = state
}),
State = state
};
logins.Add(login);
}
return logins;
}
// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST api/Account/RegisterExternal
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("RegisterExternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
protected override void Dispose(bool disposing)
{
if (disposing && _userManager != null)
{
_userManager.Dispose();
_userManager = null;
}
base.Dispose(disposing);
}
#region 帮助程序
private IAuthenticationManager Authentication
{
get { return Request.GetOwinContext().Authentication; }
}
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
if (ModelState.IsValid)
{
// 没有可发送的 ModelState 错误,因此仅返回空 BadRequest。
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
private class ExternalLoginData
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
public string UserName { get; set; }
public IList<Claim> GetClaims()
{
IList<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider));
if (UserName != null)
{
claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider));
}
return claims;
}
public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
{
if (identity == null)
{
return null;
}
Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer)
|| String.IsNullOrEmpty(providerKeyClaim.Value))
{
return null;
}
if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
{
return null;
}
return new ExternalLoginData
{
LoginProvider = providerKeyClaim.Issuer,
ProviderKey = providerKeyClaim.Value,
UserName = identity.FindFirstValue(ClaimTypes.Name)
};
}
}
private static class RandomOAuthStateGenerator
{
private static RandomNumberGenerator _random = new RNGCryptoServiceProvider();
public static string Generate(int strengthInBits)
{
const int bitsPerByte = 8;
if (strengthInBits % bitsPerByte != 0)
{
throw new ArgumentException("strengthInBits 必须能被 8 整除。", "strengthInBits");
}
int strengthInBytes = strengthInBits / bitsPerByte;
byte[] data = new byte[strengthInBytes];
_random.GetBytes(data);
return HttpServerUtility.UrlTokenEncode(data);
}
}
#endregion
}
}
- 第一步:首先注册一个用户。每个方法上面都注释了访问的路径,我们先看
Register
方法,这个方法就是注册用户的方法,需要post的方式,参数F12看一下,是
public class RegisterBindingModel
{
[Required]
[Display(Name = "电子邮件")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "{0} 必须至少包含 {2} 个字符。", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "密码")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "确认密码")]
[Compare("Password", ErrorMessage = "密码和确认密码不匹配。")]
public string ConfirmPassword { get; set; }
}
也就是说,我们要用post方法访问http://localhost:31926/api/Account/Register
,并带上Email,Password,ConfirmPassword 三个参数,这样就可以注册一个账号密码了。
下面是做法:
在Home控制器下新建一个控制器Login,并添加一个视图,然后我们打开/Views/Home/Login.cshtml
,添加代码:
<h2>Login</h2>
<input type="button" name="" id="sub" value="提交" />
<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script>
var data = {
username: "123@qq.com",
password: "123Aa!",
ConfirmPassword: "123Aa!"
}
$("#sub").click(function () {
$.post("/api/Account/Register", data, function (d) {
})
return false;
})
</script>
这里只放一个按钮,然后ajax访问api,打开http://localhost:31926/home/Login
,并点击按钮。然后我们可以在数据库中看到真的添加了一个123@qq.com的用户了。第一步完成
- 第二步:登录。访问
http://localhost:31926/Token
,并带上参数,把放回的参数存放在cookie里面,或者打印出来
<script>
var data = {
username: "123@qq.com",
password: "123Aa!",
grant_type:"password"
}
$("#sub").click(function () {
$.post("/token", data, function (d) {
if (d) {
setcookie('access_token', d.access_token, 86400);
setcookie('token_type', d.token_type, 86400);
setcookie('token', JSON.stringify(d), 86400);
}
})
return false;
});
function setcookie(name, value, seconds) {
seconds = seconds || 0; //seconds有值就直接赋值,没有为0,这个根php不一样。
var expires = "";
if (seconds != 0) { //设置cookie生存时间
var date = new Date();
var expires_time = new Date(date.getTime() + seconds * 1000);
expires = "; expires=" + expires_time.toGMTString();
console.log(name, '====', expires);
}
console.log("cookie")
document.cookie = name + "=" + value + expires;
}
<script>
点击按钮访问,这里就是通过ajax访问,然后server返回access_token了,我这里是把token保存起来,在生产环境中,一般都是保存在cookie,或者sessionStorge,或者localStorge里面。然后访问需要登录授权的接口时,带上token就行。
- 第三步:带上token访问需要授权的接口。只要在http请求头中加上Authorization:bearer Token就可以成功访问API就成功了,如:
GET http://localhost:31926/api/values
Authorization : bearer T5jF97t5n-rBkWcwpiVDAlhzXtOvV7Jw2NnN1Aldc--xtDrvWtqLAN9hxJN3Fy7piIqNWeLMNm2IKVOqmmC0X5_s8MwQ6zufUDbvF4Bg5OHoHTKHX6NmZGNrU4mjpCuPLtSbT5bh_gFOZHoIXXIKmqD3Wu1MyyKKNhj9XPEIkd9bl4E9AZ1wAt4dyUxmPVA_VKuN7UvYJ97TkO04XyGqmXGtfVWKfM75mNVYNhySWTg
代码可以这样写(这里需要先做上一步,把token存在cookie中):
$("#get").click(function () {
$.ajax({
url: '/api/values',
beforeSend: function(request) {
request.setRequestHeader("Authorization", "bearer "+getCookie('access_token'));
},
dataType: 'JSON',
async: false,//请求是否异步,默认为异步
type: 'GET',
success: function (d) {
alert(JSON.stringify(d));
},
error: function () {
}
});
})
function getCookie(name) {
var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
if (arr = document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}
这样就能有权限访问需授权的接口了。
至于把用户模型改为时候自己业务的用法这些应该就比较简单了,在/Providers/ApplicationOAuthProvider.cs
里,下面这个方法可以改写验证用户名和密码是否正确的逻辑 if (user==null)
这里就可以更换为查询数据库的用户名作对比(原方法应该是用了EF直接与数据库打交道)。
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user==null)
{
context.SetError("invalid_grant", "用户名或密码不正确。");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
「一键投喂 软糖/蛋糕/布丁/牛奶/冰阔乐!」
(๑>ڡ<)☆谢谢老板~
使用微信扫描二维码完成支付