zoukankan      html  css  js  c++  java
  • 基于aspnet core 2.0 为app开发接口 (一. Authorize篇 )

    最近要为app端开发接口,为了安全考虑要为部分接口添加Authorize验证,因此选用了JWT技术。具体做法是:用户登录,注册时发放token,由app端保存,在调用服务器端接口时,携带token,服务器端验证token是否合法,验证通过则正常响应,验证失败则返回401信息。其他问题如保证某个账号当前只能在一个手机登陆,自定义401信息方便app端处理,自定义404等友好信息。

    园子里有很多介绍Authorize的文章,我在开发过程中也参考了一些园友的文章,参考的作者和链接我会在文章中放出来,如有忘记提到的引用文章,还请回复,作者会及时修改。


    接下来我们逐步实现上面提到的功能,step by step~

    在api项目添加nuget引用

    Microsoft.AspNetCore.Authentication.JwtBearer
    Microsoft.IdentityModel.Tokens

    用户登录,注册时发放token

            [AllowAnonymous]
            [HttpPost]
            [Route("login")]
            public async Task<IActionResult> Login()
            {
                string userName = Request.Form["UserName"].ToString();
                string password = Request.Form["Password"].ToString();
                bool pass = await _userService.CheckPassword(userName, pasWord);
                if (pass)
                {
                    //发放token
                    return Ok(CreateToken(userName));
                }
                else
                {
                    return BadRequest("登录名或密码不正确");
                }
            }

    发放token的地方可以根据业务在token中添加一些信息

            private string CreateToken(string userName)
            {
                var claims = new[]
                    {
                       //可以添加一些需要的信息
                       new Claim(ClaimTypes.Name, userName),
                   };
                //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["SecurityKey"]));
                var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                /**
                    Claims 部分包含了一些跟这个 token 有关的重要信息。 JWT 标准规定了一些字段,下面节选一些字段:
    
                    iss: The issuer of the token,token 是给谁的
                    sub: The subject of the token,token 主题
                    exp: Expiration Time。 token 过期时间,Unix 时间戳格式
                    iat: Issued At。 token 创建时间, Unix 时间戳格式
                    jti: JWT ID。针对当前 token 的唯一标识
                    除了规定的字段外,可以包含其他任何 JSON 兼容的字段。
                 * */
                var token = new JwtSecurityToken(
                    issuer: "ace.com",
                    audience: "ace.com",
                    claims: claims,
                    expires: DateTime.Now.AddMinutes(30),
                    signingCredentials: creds);
                return new JwtSecurityTokenHandler().WriteToken(token);
            }    

    在Startup中配置服务端验证的代码

         public void ConfigureServices(IServiceCollection services)
            {
                //...
    
                services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,//是否验证Issuer
                        ValidateAudience = true,//是否验证Audience
                        ValidateLifetime = true,//是否验证失效时间
                        ValidateIssuerSigningKey = true,//是否验证SecurityKey
                        ValidAudience = "ace.com",//Audience
                        ValidIssuer = "ace.com",//Issuer,这两项和前面签发jwt的设置一致
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SecurityKey"]))//拿到SecurityKey
                    };
                });
            }
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                //...
                app.UseAuthentication();
            }

    现在已经配置好了token的发放和验证,只需要在需要验证的controller或action添加Authorize标记即可

            [Route("api/[controller]")]
            public class ValuesController : Controller
            {
                // GET api/values
                [HttpGet]
                [Authorize]
                public string Get()
                {
                    return "api project~~";
                }
            }

    现在直接访问这个接口会报401的错误

    在Headers中添加token后可正常调用接口


    接口提供给app等前端使用时,为了统一接口样式,方便前台操作处理,可能会自定义401信息,以达到下图的效果

    这样处理response的效果是:服务端只给app端返回200的状态码,根据业务需求在response的body中返回不同的code,app端只根据response的body中的code去处理。

    解决方思路是服务器端配置jwt验证时添加一个响应事件,拦截自身的响应处理。现在想的就是在哪里拦截,如何拦截的问题。

    组件Microsoft.AspNetCore.Authentication.JwtBearer验证处理的代码都在JwtBearerHandler的HandleAuthenticateAsync方法中,查看源码可发现JwtBearerEvents提供了几个event来供开发者自定义一些处理。

     JwtBearerEvents的源码如下所示:

        /// <summary>
        /// Specifies events which the <see cref="JwtBearerHandler"/> invokes to enable developer control over the authentication process.
        /// </summary>
        public class JwtBearerEvents
        {
            /// <summary>
            /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
            /// </summary>
            public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
    
            /// <summary>
            /// Invoked when a protocol message is first received.
            /// </summary>
            public Func<MessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.CompletedTask;
    
            /// <summary>
            /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
            /// </summary>
            public Func<TokenValidatedContext, Task> OnTokenValidated { get; set; } = context => Task.CompletedTask;
    
            /// <summary>
            /// Invoked before a challenge is sent back to the caller.
            /// </summary>
            public Func<JwtBearerChallengeContext, Task> OnChallenge { get; set; } = context => Task.CompletedTask;
    
            public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
    
            public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context);
    
            public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context);
    
            public virtual Task Challenge(JwtBearerChallengeContext context) => OnChallenge(context);
        }

    验证的逻辑在HandleAuthenticateAsync方法中,验证完成后的处理在HandleChallengeAsync方法中,若是验证通过不会调用这个方法。其部分源码如下所示:

         protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
            {
                var authResult = await HandleAuthenticateOnceSafeAsync();
                var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties)
                {
                    AuthenticateFailure = authResult?.Failure
                };
    
                // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token).
                if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null)
                {
                    eventContext.Error = "invalid_token";
                    eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure);
                }
    
                await Events.Challenge(eventContext);
                if (eventContext.Handled)
                {
                    return;
                }
    
                Response.StatusCode = 401;
                //.....
             }

    所以我们可以选择在Event.Challenge(eventContext)时拦截其处理,直接响应我们的自定义内容。我们可以在刚才配置JwtBearer的option中添加OnChallenge事件。代码如下所示

            public void ConfigureServices(IServiceCollection services)
            {
                //...其他设置
                services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.Events = new JwtBearerEvents
                        {
                            OnChallenge = context =>
                            {
                                //if (context.AuthenticateFailure != null)
                                //{
    
                                context.Response.StatusCode = 200;
                                byte[] body = Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(new ApiResponse
                                {
                                    code = 401,
                                    data = null,
                                    msg = "登录失效,请重新登陆"
                                }));
                                context.Response.ContentType = "application/json";
                                context.Response.Body.Write(body, 0, body.Length);
                                context.HandleResponse();
    
                                //}
                                return Task.CompletedTask;
                            }
                        };
                        //...其他设置
                    });
            }  

    在文章开始我们提到了保证app一个账号当前只在一个手机登陆的问题,这个问题可以有两种解决思路。

    第一种思路:用户每次登陆时发送机器唯一识别码,服务器端发现本次登陆机器和上次登陆机器不同时,给上个机器推送消息,让上个机器的app端下线,跳转到登录页面。

    第二种思路:用户每次登陆时发送机器唯一识别码,服务器端将该识别码放在token中,app每次请求服务端的数据都需要验证token,在验证token时验证是否是当前登陆的机器,若不是,返回401信息,使app端跳转至登录页面。

    我选择了第二种方式,因为第一种需要第三方的推送组件,过分的依赖别人是很危险的,^_^所以我们选择相信自己,相信服务器端。

    JwtBearerHandler的HandleAuthenticateAsync方法代码如下:

    /// <summary>
            /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
            /// </summary>
            /// <returns></returns>
            protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
            {
                string token = null;
                try
                {
                    // Give application opportunity to find from a different location, adjust, or reject token
                    var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
    
                    // event can set the token
                    await Events.MessageReceived(messageReceivedContext);
                    if (messageReceivedContext.Result != null)
                    {
                        return messageReceivedContext.Result;
                    }
                    //...
            }

    我们可以发现Events.MessageReceived事件在处理的最开始时调用,所以我们在此入手。先检查该机器是不是当前登陆的机器,若是当前登陆的机器,再走下面的验证流程。若不是当前登陆的机器,直接将token置空(或其他方法)。因为即便不是当前的机器,该token还是可以使用的(token只要不过期就可以使用),置空token能保证下面的验证逻辑将其拦截。

    操作方法仍然是配置JwtBearer的JwtBearerOptions,代码如下所示

            public void ConfigureServices(IServiceCollection services)
            {
                services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.Events = new JwtBearerEvents
                        {
                            OnMessageReceived = context =>
                            {
                                if (!StringValues.IsNullOrEmpty(context.Request.Headers["Authorization"]))
                                {
                                    try
                                    {
                                        //todo  验证多app登陆
                                        var startLength = "Bearer ".Length;
                                        var tokenStr = context.Request.Headers["Authorization"].ToString();
                                        var token = new JwtSecurityTokenHandler().ReadJwtToken(tokenStr.Substring(startLength, tokenStr.Length - startLength));
                                        string userName = token.Claims.ToList().First(o => o.Type == System.Security.Claims.ClaimTypes.Name).Value.ToString();
                                        string clientId = token.Claims.ToList().First(o => o.Type == "ClientId").Value.ToString();
                                        var clientService = container.GetRequiredService<IClientService>();
                                        if (!clientService.CheckLogin(userName, clientId))//验证逻辑根据业务实现
                                            context.Request.Headers["Authorization"] = string.Empty;
                                    }
                                    catch (Exception ex)
                                    {
                                        context.Request.Headers["Authorization"] = string.Empty;
                                    }
    
                                }
                                return Task.CompletedTask;
                            }
                        };
                    });
            }

    当然在生成token的地方也要在token的Claims中添加机器唯一识别码。

    到这里为止,我们已经基本解决了文章开头提到的问题。


    文中我们说到为了方便app处理响应,统一response的格式,状态码都为200。但是有些默认的如404等状态码还是会造成如下的效果

    我们可以修改其全全局设置,在Startup的Configure方法中添加代码

    app.UseStatusCodePagesWithRedirects("/error/{0}");

    我们可以看到这个方法的注释如下

            //
            // 摘要:
            //     Adds a StatusCodePages middleware to the pipeline. Specifies that responses should
            //     be handled by redirecting with the given location URL template. This may include
            //     a '{0}' placeholder for the status code. URLs starting with '~' will have PathBase
            //     prepended, where any other URL will be used as is.
            //
            // 参数:
            //   app:
            //
            //   locationFormat:

    我们可以添加一个controller,来处理不同的状态码,代码如下所示

        public class ErrorController : Controller
        {
            [Route("error/404")]
            public IActionResult Error404()
            {
                return Ok(new ApiResponse
                {
                    code = 404,
                    msg = "请求出错",
                    data = null
                });
            }
            [Route("error/{code:int}")]
            public IActionResult Error(int code)
            {
                return Ok(new ApiResponse
                {
                    code = code,
                    data = null,
                    msg = "请求出错"
                });
            }
        }

    这时再调用不存在的接口时,是如下效果

     现在就达到了我们想要的格式。

     过程中参考的博客如下:

    ASP.NET Core 认证与授权[1]:初识认证 [雨夜朦胧]

    JwtBearer 认证 [Leo_wlCnBlogs]

    ASP.NET Core 中的那些认证中间件及一些重要知识点 [Savorboard](推荐其CAP项目,好用且好玩~~)

    aspnet 认证相关的源码

    详解ASP.NET Core 处理 404 Not Found


    总结:

    先明确需求再解决问题

    看别人的文章要学习别人解决问题的思路

    知其然也知其所以然,要对照问题多看aspnet core的源码。现在代码简单了,只需要调用中间件就能解决大部分问题,但是我们也要根据源码研究它是如何调用如何处理的,这样才能了解"为什么",而不是只知道"怎么做"

    多思考,多总结~~

  • 相关阅读:
    龙井和碧螺春的功效与作用
    064 01 Android 零基础入门 01 Java基础语法 08 Java方法 02 无参带返回值方法
    063 01 Android 零基础入门 01 Java基础语法 08 Java方法 01 无参无返回值方法
    062 01 Android 零基础入门 01 Java基础语法 07 Java二维数组 01 二维数组应用
    061 01 Android 零基础入门 01 Java基础语法 06 Java一维数组 08 一维数组总结
    060 01 Android 零基础入门 01 Java基础语法 06 Java一维数组 07 冒泡排序
    059 01 Android 零基础入门 01 Java基础语法 06 Java一维数组 06 增强型for循环
    058 01 Android 零基础入门 01 Java基础语法 06 Java一维数组 05 案例:求数组元素的最大值
    057 01 Android 零基础入门 01 Java基础语法 06 Java一维数组 04 案例:求整型数组的数组元素的元素值累加和
    056 01 Android 零基础入门 01 Java基础语法 06 Java一维数组 03 一维数组的应用
  • 原文地址:https://www.cnblogs.com/AceZhai/p/9087690.html
Copyright © 2011-2022 走看看