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(10), 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 = 6, 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; } });
本系列结束☺