购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证

时间:2021-12-23 14:09:20

原文:购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证

chsakell分享了前端使用AngularJS,后端使用ASP.NET Web API的购物车案例,非常精彩,这里这里记录下对此项目的理解。

文章:
http://chsakell.com/2015/01/31/angularjs-feat-web-api/
http://chsakell.com/2015/03/07/angularjs-feat-web-api-enable-session-state/

源码:
https://github.com/chsakell/webapiangularjssecurity

本系列共三篇,本篇是第三篇。

购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(1)--后端
购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session
购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证

这里会涉及到三方面的内容:

1、ASP.NET Identity & Entity Framework

● Identity User
● User Mnager

2、OWIN Middleware

● Authorization Server
● Bearer Auhentication

3、AngularJS

● Generate Tokens
● Creae authorized requests

1、ASP.NET Identity & Entity Framework

首先安装Microsoft ASP.NET Identity EntityFramework。

添加一个有关用户的领域模型,继承IdentityUser。

public class AppStoreUser : IdentityUser
{
...
}

配置用户,继承EntityTypeConfiguration<T>

public class AppStoreUserConfiguraiton : EntityTypeConfiguration<AppStoreUser>
{
public AppStoreUserConfiguration()
{
ToTable("Users");
}
}

然后让上下文继承Identity特有的上下文类。

public class StoreContext : IdentityDbContext<AppStoreUser>
{
public StoreContext() : base("StoreContext", thrwoIfVISchema: false)
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUserLogin>().HasKey<string>(l => l.UserId);
modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id);
modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId }); modelBuilder.Configurations.Add(new AppStoreUserConfiguration());
modelBuilder.Configurations.Add(new CategoryConfiguration());
modelBuilder.Configurations.Add(new OrderConfiguration());
}
}
}

继承Identity的UserManager类:

public class AppStoreUserManager : UserManager<AppStoreUser>
{
public AppStoreUserManager(IUserStore<AppStoreUser> store) : base(store)
{}
}

2、OWIN Middleware

在NuGet中输入owin,确保已经安装如下组件:

Microsoft.Owin.Host.SystemWeb
Microsoft.Owin
Microsoft ASP.NET Web API 2.2 OWIN
Microsoft.Owin.Security
Microsoft.Owin.Security.OAth
Microsoft.Owin.Security.Cookies (optional)
Microsoft ASP.NET Identity Owin
OWIN

在项目根下创建Startup.cs部分类。

[assembly: OwinStartup(typeof(Store.Startup))]
namespace Store
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureStoreAuthentication(app);
}
}
}

在App_Start中创建Startup.cs部分类。

//启用OWIN的Bearer Token Authentication
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; } public static string PublicClientId { get; private set; } public void ConfigureStoreAuthentication(IAppBuilder app)
{
// User a single instance of StoreContext and AppStoreUserManager per request
app.CreatePerOwinContext(StoreContext.Create);
app.CreatePerOwinContext<AppStoreUserManager>(AppStoreUserManager.Create); // Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(),
AllowInsecureHttp = true
}; app.UseOAuthBearerTokens(OAuthOptions);
}
}

在Identity用户管理类中添加如下代码:

public class AppStoreUserManager : UserManager<AppStoreUser>
{
public AppStoreUserManager(IUserStore<AppStoreUser> store)
: base(store)
{
} public static AppStoreUserManager Create(IdentityFactoryOptions<AppStoreUserManager> options, IOwinContext context)
{
var manager = new AppStoreUserManager(new UserStore<AppStoreUser>(context.Get<StoreContext>())); // Configure validation logic for usernames
manager.UserValidator = new UserValidator<AppStoreUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
}; // Password Validations
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = ,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = true,
RequireUppercase = true,
}; var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<AppStoreUser>(dataProtectionProvider.Create("ASP.NET Identity"));
} return manager;
} public async Task<ClaimsIdentity> GenerateUserIdentityAsync(AppStoreUser user, string authenticationType)
{
var userIdentity = await CreateIdentityAsync(user, authenticationType); return userIdentity;
}
}

当在API中需要获取用户的时候,就会调用以上的代码,比如:

Request.GetOwinContext().GetUserManager<AppStoreUserManager>();

为了能够使用OWIN的功能,还需要实现一个OAuthAuthorizationServerProvider。

public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId; public ApplicationOAuthProvider(string publicClientId)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
} _publicClientId = publicClientId;
} public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<AppStoreUserManager>(); AppStoreUser user = await userManager.FindAsync(context.UserName, context.Password); if (user == null)
{
context.SetError("invalid_grant", "Invalid username or password.");
return;
} ClaimsIdentity oAuthIdentity = await userManager.GenerateUserIdentityAsync(user, OAuthDefaults.AuthenticationType);
AuthenticationProperties properties = new AuthenticationProperties();
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
} public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (context.ClientId == null)
{
context.Validated();
} return Task.FromResult<object>(null);
}
}

OWIN这个中间件的工作原理大致是:

→对Token的请求过来
→OWIN调用以上的GrantResourceOwnerCredentials方法
→OAuthAuthorizationServerProvider获取UerManager的实例
→OAuthAuthorizationServerProvider创建access token
→OAuthAuthorizationServerProvider创建access token给响应
→Identity的UserManager检查用户的credentials是否有效
→Identity的UserManager创建ClaimsIdentity

接着,在WebApiConfig中配置,让API只接受bearer token authentication。

public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType)); // Web API routes
config.MapHttpAttributeRoutes(); }
}

在需要验证的控制器上加上Authorize特性。

[Authorize]
public class OrdersController : ApiController
{}

AccountController用来处理用户的相关事宜。

[Authorize]
[RoutePrefix("api/Account")]
public class AccountController : ApiController
{
//private const string LocalLoginProvider = "Local";
private AppStoreUserManager _userManager; public AccountController()
{
} public AccountController(AppStoreUserManager userManager,
ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
{
UserManager = userManager;
AccessTokenFormat = accessTokenFormat;
} public AppStoreUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext().GetUserManager<AppStoreUserManager>();
}
private set
{
_userManager = value;
}
} public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; } // POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegistrationModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} var user = new AppStoreUser() { UserName = model.Email, Email = model.Email }; IdentityResult result = await UserManager.CreateAsync(user, model.Password); 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 Helpers 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)
{
// No ModelState errors are available to send, so just return an empty BadRequest.
return BadRequest();
} return BadRequest(ModelState);
} return null;
}
#endregion
}

3、AngularJS

在前端,把token相关的常量放到主module中去。

angular.module('gadgetsStore')
.constant('gadgetsUrl', 'http://localhost:61691/api/gadgets')
.constant('ordersUrl', 'http://localhost:61691/api/orders')
.constant('categoriesUrl', 'http://localhost:61691/api/categories')
.constant('tempOrdersUrl', 'http://localhost:61691/api/sessions/temporders')
.constant('registerUrl', '/api/Account/Register')
.constant('tokenUrl', '/Token')
.constant('tokenKey', 'accessToken')
.controller('gadgetStoreCtrl', function ($scope, $http, $location, gadgetsUrl, categoriesUrl, ordersUrl, tempOrdersUrl, cart, tokenKey) {

提交订单的时候需要把token写到headers的Authorization属性中去。

$scope.sendOrder = function (shippingDetails) {
var token = sessionStorage.getItem(tokenKey);
console.log(token); var headers = {};
if (token) {
headers.Authorization = 'Bearer ' + token;
} var order = angular.copy(shippingDetails);
order.gadgets = cart.getProducts();
$http.post(ordersUrl, order, { headers: { 'Authorization': 'Bearer ' + token } })
.success(function (data, status, headers, config) {
$scope.data.OrderLocation = headers('Location');
$scope.data.OrderID = data.OrderID;
cart.getProducts().length = 0;
$scope.saveOrder();
$location.path("/complete");
})
.error(function (data, status, headers, config) {
if (status != 401)
$scope.data.orderError = data.Message;
else {
$location.path("/login");
}
}).finally(function () {
});
}

在主module中增加登出和注册用户的功能。

$scope.logout = function () {
sessionStorage.removeItem(tokenKey);
}
$scope.createAccount = function () {
$location.path("/register");
}

当然还需要添加对应的路由:

 $routeProvider.when("/login", {
templateUrl: "app/views/login.html"
});
$routeProvider.when("/register", {
templateUrl: "app/views/register.html"
});

再往主module中添加一个controller,用来处理用户账户相关事宜。

angular.module("gadgetsStore")
.controller('accountController', function ($scope, $http, $location, registerUrl, tokenUrl, tokenKey) { $scope.hasLoginError = false;
$scope.hasRegistrationError = false; // Registration
$scope.register = function () { $scope.hasRegistrationError = false;
$scope.result = ''; var data = {
Email: $scope.registerEmail,
Password: $scope.registerPassword,
ConfirmPassword: $scope.registerPassword2
}; $http.post(registerUrl, JSON.stringify(data))
.success(function (data, status, headers, config) {
$location.path("/login");
}).error(function (data, status, headers, config) {
$scope.hasRegistrationError = true;
var errorMessage = data.Message;
console.log(data);
$scope.registrationErrorDescription = errorMessage; if (data.ModelState['model.Email'])
$scope.registrationErrorDescription += data.ModelState['model.Email']; if (data.ModelState['model.Password'])
$scope.registrationErrorDescription += data.ModelState['model.Password']; if (data.ModelState['model.ConfirmPassword'])
$scope.registrationErrorDescription += data.ModelState['model.ConfirmPassword']; if (data.ModelState[''])
$scope.registrationErrorDescription += data.ModelState['']; }).finally(function () {
});
} $scope.login = function () {
$scope.result = ''; var loginData = {
grant_type: 'password',
username: $scope.loginEmail,
password: $scope.loginPassword
}; $http({
method: 'POST',
url: tokenUrl,
data: $.param(loginData),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
}).then(function (result) {
console.log(result);
$location.path("/submitorder");
sessionStorage.setItem(tokenKey, result.data.access_token);
$scope.hasLoginError = false;
$scope.isAuthenticated = true;
}, function (data, status, headers, config) {
$scope.hasLoginError = true;
$scope.loginErrorDescription = data.data.error_description;
}); } });

有关登录页:

<div ng-controller="accountController">
<form role="form">
<input name="email" type="email" ng-model="loginEmail" autofocus="">
<input name="password" type="password" ng-model="loginPassword" value=""> <div ng-show="hasLoginError">
<a href="#" ng-bind="loginErrorDescription"></a>
</div> <a href="" ng-click="login()">Login</a>
<a href="" ng-click="createAccount()">Create account</a>
</form>
</div>

有关注册页:

<div ng-controller="accountController">
<form role="form">
<input name="email" type="email" ng-model="registerEmail" autofocus="">
<input name="password" type="password" ng-model="registerPassword" value="">
<input name="confirmPassword" type="password" ng-model="registerPassword2" value=""> <div ng-show="hasRegistrationError">
<a href="#" ng-bind="registrationErrorDescription"></a>
</div>
<a href="" ng-click="register()">Create account</a
</form>
</div>

在购物车摘要区域添加一个登出按钮。

<a href="" ng-show="isUserAuthenticated()" ng-click="logout()">Logout</a>

最后可以把账户相关封装在一个服务中。

angular.module("gadgetsStore")
.service('accountService', function ($http, registerUrl, tokenUrl, tokenKey) { this.register = function (data) {
var request = $http.post(registerUrl, data); return request;
} this.generateAccessToken = function (loginData) {
var requestToken = $http({
method: 'POST',
url: tokenUrl,
data: $.param(loginData),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
}); return requestToken;
} this.isUserAuthenticated = function () {
var token = sessionStorage.getItem(tokenKey); if (token)
return true;
else
return false;
} this.logout = function () {
sessionStorage.removeItem(tokenKey);
} });

把有关订单相关,封装在storeService.js中:

angular.module("gadgetsStore")
.service('storeService', function ($http, gadgetsUrl, categoriesUrl, tempOrdersUrl, ordersUrl, tokenKey) { this.getGadgets = function () {
var request = $http.get(gadgetsUrl); return request;
} this.getCategories = function () {
var request = $http.get(categoriesUrl); return request;
} this.submitOrder = function (order) {
var token = sessionStorage.getItem(tokenKey);
console.log(token); var headers = {};
if (token) {
headers.Authorization = 'Bearer ' + token;
} var request = $http.post(ordersUrl, order, { headers: { 'Authorization': 'Bearer ' + token } }); return request;
} this.saveTempOrder = function (currentProducts) {
var request = $http.post(tempOrdersUrl, currentProducts); return request;
} this.loadTempOrder = function () {
var request = $http.get(tempOrdersUrl); return request;
} });

本系列结束☺