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的源码。现在代码简单了,只需要调用中间件就能解决大部分问题,但是我们也要根据源码研究它是如何调用如何处理的,这样才能了解"为什么",而不是只知道"怎么做"

    多思考,多总结~~

  • 相关阅读:
    【UOJ 121】Hzwer的陨石
    【UOJ 666】古老的显示屏
    【UOJ 222】正方形二分型
    【UOJ 654】虫洞问题
    【UOJ 226】最近公共祖先
    【UOJ 92】有向图的强连通分量
    poj2139 Floyd
    poj1631 dp,最长上升子序列
    poj1065&1548 dp,最长上升子序列,偏序与反偏序
    poj1458(裸LCS)
  • 原文地址:https://www.cnblogs.com/AceZhai/p/9087690.html
Copyright © 2011-2022 走看看