购物车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(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;
        }

    });

 

本系列结束

 

posted @ 2015-11-12 17:37  Darren Ji  阅读(2022)  评论(1编辑  收藏  举报

我的公众号:新语新世界,欢迎关注。