从Angular JS使用Web API令牌身份验证时出现错误请求(400)

问题描述:

我想以Angular JS作为客户端建立Web API令牌认证.我对Web API中的令牌身份验证这一概念非常陌生.

I want to establish Web API Token Authentication with Angular JS as client. I am very new to this concept of Token Authentication inside Web API.

我不想使用ASP.NET Identity默认表来添加或验证用户.我有自己的数据库和一个名为"EmployeeAccess"的表,其中包含EmployeeNumber作为用户ID和密码.我想针对此表中的值对用户进行身份验证,然后想要授予令牌,以便他们被授权进行后续调用.我已经使用了所有必需的OWIN和ASP.NET参考来实现结果.这是我的不同组件的代码:-

I do not want to use ASP.NET Identity default tables to add or authenticate user. I have my own database and a table called "EmployeeAccess" table which contains EmployeeNumber as User Id and Password. I want to authenticate the users against the values in this table and then want to grant token so that they gets authorized for subsequent call. I have used all required OWIN and ASP.NET References to achieve the result. Here is my code of different components:-

Global.asax

public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
           // AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);

        }

        protected void Application_BeginRequest()
        {
            if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
            {
                // Cache the options request.
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", HttpContext.Current.Request.Headers["Origin"]);
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, POST, OPTIONS");
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
                HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000");
                HttpContext.Current.Response.End();
            }
        }
    }

WebApiConfig.cs

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {            
            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
            config.Formatters.Remove(config.Formatters.XmlFormatter);

            var cors = new EnableCorsAttribute("*", "*", "*");
            config.EnableCors(cors);
        }
    }

Startup.cs

[assembly: OwinStartup(typeof(Application.WebAPI.Startup))]

namespace Application.WebAPI
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
            app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
            var myProvider = new AuthorizationServerProvider();
            OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/Token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                Provider = myProvider
            };
            app.UseOAuthAuthorizationServer(options);
        }
    }
}

AuthorizationServerProvider.cs

 public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            context.Validated(); // 
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            //context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
            string userId = context.UserName;
            string password = context.Password;

            EmployeeAccessBLL chkEmpAccessBLL = new EmployeeAccessBLL();
            EmployeeAccessViewModel vmEmployeeAccess = chkEmpAccessBLL.CheckEmployeeAccess(Convert.ToInt32(userId), password);

            if(vmEmployeeAccess != null)
            {
                var identity = new ClaimsIdentity(context.Options.AuthenticationType);
                identity.AddClaim(new Claim("username", vmEmployeeAccess.EmpName));
                context.Validated(identity);
            }
            else
            {
                context.SetError("invalid_grant", "Provided username and password is incorrect");
                return;
            }
        }               
    } 

Login.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js"></script>
    <script src="../Scripts/AngularControllers/LoginController.js"></script>
    <script src="../Scripts/AngularServices/ApiCallService.js"></script>
</head>
<body ng-app="appHome">
    <div ng-controller="ctrlLogin">
        <label>Employee Number</label>
        <input type="text" id="txtEmpNumber" ng-model="md_empnumber" />
        <br/>
        <br/>
        <label>Password</label>
        <input type="text" id="txtEmpNumber" ng-model="md_password"  />

        <button id="btnAdd" type="submit" ng-click="Login()">Login</button>
    </div>
</body>
</html>

LoginController.js

var myApp = angular.module('appHome', []);
myApp.controller("ctrlLogin", ['$scope', 'MetadataOrgFactory', '$location', function ($scope, MetadataOrgFactory, $location) {
    $scope.Login = function () {
        var objLogin = {
            'username' : $scope.md_empnumber,
            'password' : $scope.md_password,
            'grant_type' : 'password'
        };

        MetadataOrgFactory.postLoginCall('Token', objLogin, function (dataSuccess) {
            alert("Welcome " + dataSuccess);           
        }, function (dataError) {
        });
    }
}]);

ApiCallService.js

var appService = angular.module('appHome');
appService.factory('MetadataOrgFactory', ['$http', function ($http) {

    var url = 'http://localhost:60544';    
    var dataFactory = {};    
    dataFactory.postLoginCall = function (controllerName, objData, callbackSuccess, callbackError) {

        $http.post(url + '/' + controllerName, objData,{headers:{ 'Content-Type': 'application/x-www-form-urlencoded' }}).then
            (function success(response) {
                alert("Success");
                callbackSuccess(response.data);
            }, function error(response) {
                callbackError(response.status);
            });
    };
    return dataFactory;
}])

当我单击登录"按钮时,我收到以下错误消息:-

When I click on Login button then I am getting below error message:-

POST http://localhost:60544/Token 400(错误请求)

POST http://localhost:60544/Token 400 (Bad Request)

当我调试WebAPI代码时,我发现"AuthorizationServerProvider.cs"中的方法"GrantResourceOwnerCredentials()"从不执行.错误消息在此之前出现.仅执行方法"ValidateClientAuthentication"和"MatchEndpoint".

When I was debugging WebAPI code then I found that method "GrantResourceOwnerCredentials()" inside "AuthorizationServerProvider.cs" never gets executed. The error message comes before this. Only the method "ValidateClientAuthentication" and "MatchEndpoint" gets executed.

请帮助我成功运行Web API令牌认证的场景.如果发现有多余的代码,请告诉我,以便将其删除.

Please help me to run the scenario of Web API Token Authentication successfully. If any code is found redundant then please let me know so that I can remove this.

好的,这将是一个很长的答案,但请坚持到最后:)

步骤1:删除Global.asax

在Owin管道上运行时,不需要Global.asax.我会说Startup.cs是Owins Global.asax. 它们基本上达到了相同的目的,因此继续进行删除.

The Global.asax is not needed when you run on the Owin pipeline. The Startup.cs is what I would say Owins Global.asax. They basically fills the same purpose so go ahead and remove it.

步骤2:在WebApiConfig.cs中删除Cors处理

不需要此代码,因为您已经在Startup.cs中声明了它.

This code is not needed as you already declare it in the Startup.cs.

app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

您的WebApiConfig.cs将如下所示

Your WebApiConfig.cs will then look like this

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {            
        // Web API routes
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
        config.Formatters.Remove(config.Formatters.XmlFormatter);
    }
}

第3步:在Startup.cs的Owin管道中添加Web Api和承载令牌自动验证

您无需将WebApiConfig绑定到Global.asax中,而是将其附加到管道中. 还将不记名令牌处理应用于管道.

Instead of binding the WebApiConfig in the Global.asax you attach it to the pipeline. Also apply the bearer token handling to the pipeline.

您的Startup.cs将如下所示

Your Startup.cs will then look like this

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
        OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/Token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new AuthorizationServerProvider()
        };
        app.UseOAuthAuthorizationServer(options);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        //Register the web api to the pipeline 
        HttpConfiguration config = new HttpConfiguration();
        WebApiConfig.Register(config);
        app.UseWebApi(config);
    }
}

步骤4:在AuthorizationServerProvider.cs中向请求添加标头

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated(); 
    }
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        SetContextHeaders(context);
        string userId = context.UserName;
        string password = context.Password;

        EmployeeAccessBLL chkEmpAccessBLL = new EmployeeAccessBLL();
        EmployeeAccessViewModel vmEmployeeAccess = chkEmpAccessBLL.CheckEmployeeAccess(Convert.ToInt32(userId), password);

        if(vmEmployeeAccess != null)
        {
            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            identity.AddClaim(new Claim("username", vmEmployeeAccess.EmpName));
            context.Validated(identity);
        }
        else
        {
            context.SetError("invalid_grant", "Provided username and password is incorrect");
            return;
        }
    }
    private void SetContextHeaders(IOwinContext context)
    {
        context.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
        context.Response.Headers.Add("Access-Control-Allow-Methods", new[] { "GET, PUT, DELETE, POST, OPTIONS" });
        context.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Content-Type, Accept, Authorization" });
        context.Response.Headers.Add("Access-Control-Max-Age", new[] { "1728000" });
    }
} 

步骤5:向Oauth服务器发出正确的请求

对oauth服务器的请求必须具有内容类型 x-www-form-urlencoded ,该内容类型基本上是字符串. 我还添加了promise而不是$ q所做的回调.海事组织,我认为诺言更容易阅读

The request to the oauth server need to be of content type x-www-form-urlencoded which basically is a string. I also added promise instead of callbacks which is what the $q does. IMO I think the promise is more clear to read

提示:不要以明文形式发送凭据. 您可以使用btoa(password)将它们解码为Base64字符串,然后在后端对其进行解码.

TIP: don't send the credentials in clear text. You could endecode them to Base64 string with btoa(password) and then decode it in your backend.

angular.module('appHome').factory('MetadataOrgFactory', ['$http', function ($http) {

    var url = 'http://localhost:60544';    
    var dataFactory = {};  
    dataFactory.login = function (userName, password) {
        var deferred = $q.defer();

        $http({
            method: 'POST',
            url: url + '/Token',
            processData: false,
            contentType: 'application/x-www-form-urlencoded',
            data: "grant_type=password&username=" + userName + "&password=" + password,
        }).
            success(function (data) {
                deferred.resolve(data);
            }).
            error(function (message, status) {              
                console.log(message);
                deferred.reject(message, status);
            });

        return deferred.promise;
    };  
    return dataFactory;
}]);

第6步:从您的控制器发出登录请求

angular.module('appHome', []);
angular.module('appHome').controller("ctrlLogin", ['$scope', 'MetadataOrgFactory', '$location', function ($scope, MetadataOrgFactory, $location) {
    $scope.Login = function () {

        MetadataOrgFactory.postLoginCall($scope.md_empnumber, $scope.md_password).then(
            function (result) {
                //success
            },
                function (error, statusCode) {
                    console.log(error);
                }
            );;
    }
}]);

就是这样.