.Net Core 2.0的一些不大一样的地方(一)——基础身份认证

@老徐  September 23, 2017

近日,把之前使用.net core 1.0写的网站,使用.net core 2.0改写了一下,发现一些不大一样的地方,如果不注意的话,会出现些问题。
一、先说下关于使用Cookie来验证用户登录的地方:
在.net core 1.x时代,具体作法如我前面的文章《.Net Core系列教程(四)—— 基础身份认证》所说,这里我就不重新写了
而在.net core 2.0中,需要做以下调整:
1)在Startup.cs文件中,ConfigureServices方法下添加:

            services.AddAuthentication(options=> {
                options.DefaultChallengeScheme = "Cookie";
                options.DefaultSignInScheme = "Cookie";
                options.DefaultAuthenticateScheme = "Cookie";
            })
            .AddCookie("Cookie", m =>
            {
                m.LoginPath = new PathString("/Manage/CPanel/Login");
                m.AccessDeniedPath = new PathString("/Manage/CPanel/Forbidden");
                m.LogoutPath = new PathString("/Manage/CPanel/Logout");
                m.Cookie.Path = "/";
            });

完整的代码如下:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplicationInsightsTelemetry(Configuration);

            services.AddAuthentication(options=> {
                options.DefaultChallengeScheme = "Cookie";
                options.DefaultSignInScheme = "Cookie";
                options.DefaultAuthenticateScheme = "Cookie";
            })
            .AddCookie("Cookie", m =>
            {
                m.LoginPath = new PathString("/Manage/CPanel/Login");
                m.AccessDeniedPath = new PathString("/Manage/CPanel/Forbidden");
                m.LogoutPath = new PathString("/Manage/CPanel/Logout");
                m.Cookie.Path = "/";
            });

            services.AddOptions();
            services.Configure<Models.ConnectionStrings>(Configuration.GetSection("ConnectionStrings"));
            services.AddAuthorization();    //Form基础验证
            services.AddMvc();
        }

之后在Configure方法下添加:

app.UseAuthentication();

完整代码:

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();


            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

            app.UseAuthentication();


            //app.UseApplicationInsightsRequestTelemetry();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            //app.UseApplicationInsightsExceptionTelemetry();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "area",
                    template: "{area:exists}/{controller=CPanel}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

之后在控制器中使用:
登录时:

            result = _manage.Login(login);
            if (result.status)
            {//登录成功
                string token = result.data.ToString();  //登录成功后生成的token,用于验证登录有效性
                var claims = new List<Claim>()
                {
                    new Claim(ClaimTypes.Name,login.username) 
                };
                var userPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, token));
                await HttpContext.SignInAsync("Cookie", userPrincipal,
                  new Microsoft.AspNetCore.Authentication.AuthenticationProperties
                  {
                      ExpiresUtc = DateTime.UtcNow.AddHours(12),
                      IsPersistent = true,
                      AllowRefresh = false
                  });
            }

验证有效性:

            var auth = await HttpContext.AuthenticateAsync("Cookie");
            if(auth.Succeeded)
            {    //验证有效
                string username = auth.Principal.Identity.Name;    //用户名
                //通过验证后的其他处理代码
            }

在其他需要验证是否登录状态,可以在对应的Action或Controller上增加特性:[Authorize],就可以了。

-==以下部分为2018年5月13日新增==-
二、使用Jwt方式
Jwt是Json Web Token的缩写,下面是关于Jwt介绍的一些搬运:

JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。

JWT包含了使用.分隔的三部分: Header 头部 Payload 负载 Signature 签名
其结构看起来是这样的Header.Payload.Signature

Header
在header中通常包含了两部分:token类型和采用的加密算法。{ "alg": "HS256", "typ": "JWT"} 接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。

Payload
Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim:reserved, public 和 private.Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有 iss(签发者),exp(过期时间戳), sub(面向的用户), aud(接收方), iat(签发时间)。 Public claims:根据需要定义自己的字段,注意应该避免冲突 Private claims:这些是自定义的字段,可以用来在双方之间交换信息 负载使用的例子:{ "sub": "1234567890", "name": "John Doe", "admin": true} 上述的负载需要经过Base64Url编码后作为JWT结构的第二部分。

Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 签名用于验证消息的发送者以及消息是没有经过篡改的。 完整的JWT 完整的JWT格式的输出是以. 分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。 下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名。

关于概念性的介绍就写到这里,下面是使用方法。
先说流程:
客户端提交用户名和密码,发起登录请求;服务器接收到请求后,验证用户名和密码的合法性,验证通过,给生成token返回给客户端;
客户端得到token之后自行保存;
客户端再次发起其他需要用户登录身份的请求时,在HTTP头中带上前面申请到的token;服务器接受到请求后,验证token的合法性,进行下一步操作。

这里就涉及到了签名和验签两部分。先是签名:
在自己的项目中,找一个适合的地方,添加以下几个类:

  1. RSAKeyHelper.cs

  2. TokenAuthOption.cs

  3. RsaParameterStorage.cs

RSAKeyHelper.cs类内容:

    public class RSAKeyHelper
    {
        public static RSAParameters GenerateKey()
        {
            string keyDir = $"{PlatformServices.Default.Application.ApplicationBasePath}key";
            if (TryGetKeyParameters(keyDir, true, out RSAParameters keyParams) == false)
            {
                keyParams = GenerateAndSaveKey(keyDir);
            }
            return keyParams;
        }

        /// <summary>
        /// 从本地文件中读取用来签发 Token 的 RSA Key
        /// </summary>
        /// <param name="filePath">存放密钥的文件夹路径</param>
        /// <param name="withPrivate"></param>
        /// <param name="keyParameters"></param>
        /// <returns></returns>
        public static bool TryGetKeyParameters(string filePath, bool withPrivate, out RSAParameters keyParameters)
        {
            string filename = withPrivate ? "key.json" : "key.public.json";
            keyParameters = default(RSAParameters);
            string path = Path.Combine($"{filePath}/", filename);
            //Console.WriteLine($"filePath:{path}");
            if (File.Exists(path) == false) return false;
            string json = File.ReadAllText(path);
            var parameterStorage = JsonConvert.DeserializeObject<RsaParameterStorage>(json);
            keyParameters.D = parameterStorage.D;
            keyParameters.DP = parameterStorage.DP;
            keyParameters.DQ = parameterStorage.DQ;
            keyParameters.Exponent = parameterStorage.Exponent;
            keyParameters.InverseQ = parameterStorage.InverseQ;
            keyParameters.Modulus = parameterStorage.Modulus;
            keyParameters.P = parameterStorage.P;
            keyParameters.Q = parameterStorage.Q;            
            return true;
        }

        /// <summary>
        /// 生成并保存 RSA 公钥与私钥
        /// </summary>
        /// <param name="filePath">存放密钥的文件夹路径</param>
        /// <returns></returns>
        public static RSAParameters GenerateAndSaveKey(string filePath)
        {
            RSAParameters publicKeys, privateKeys;
            using (var rsa = new RSACryptoServiceProvider(2048))
            {
                try
                {
                    privateKeys = rsa.ExportParameters(true);
                    publicKeys = rsa.ExportParameters(false);
                }
                finally
                {
                    rsa.PersistKeyInCsp = false;
                }
            }
            if (Directory.Exists(filePath) == false) Directory.CreateDirectory(filePath);
            File.WriteAllText(Path.Combine(filePath, "key.json"), ToJsonString(privateKeys));
            File.WriteAllText(Path.Combine(filePath, "key.public.json"), ToJsonString(publicKeys));
            return privateKeys;
        }

        // 转换成 json 字符串
        static string ToJsonString(RSAParameters parameters)
        {
            var parameterStorage = new RsaParameterStorage
            {
                D = parameters.D,
                DP = parameters.DP,
                P = parameters.P,
                DQ = parameters.DQ,
                Q = parameters.Q,
                Exponent = parameters.Exponent,
                InverseQ = parameters.InverseQ,
                Modulus = parameters.Modulus
            };
            return JsonConvert.SerializeObject(parameterStorage);
        }
    }

这个类主要干了这些事:
1.生成公钥私钥对并转成JSON保存到本地;
2.读取保存在本地的公钥私钥对并生成Key

TokenAuthOption.cs类内容:

    public class TokenAuthOption
    {
        /// <summary>
        /// 受众,代表将要使用这些token的实体
        /// </summary>
        public static string Audience { get; };

        /// <summary>
        /// 发行者,表示生成token的实体
        /// </summary>
        public static string Issuer { get; };

        public static RsaSecurityKey Key { get; } = new RsaSecurityKey(RSAKeyHelper.GenerateKey());
        
        /// <summary>
        /// 安全密钥和创建签名算法
        /// </summary>
        public static SigningCredentials SigningCredentials { get; } = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256Signature);

        /// <summary>
        /// 有效期
        /// </summary>
        public static TimeSpan ExpiresSpan { get; } = TimeSpan.FromDays(30);
    }

RsaParameterStorage.cs类,是用于保存和读取生成的公私钥对的转换,因为ASP.NET Core 2.0之后,私密信息在转成JSON时不允许保存了,必须要转一下,为了省事,这里就用了个最笨的办法:

    public class RsaParameterStorage
    {
        public byte[] D { get; set; }
        public byte[] DP { get; set; }
        public byte[] DQ { get; set; }
        public byte[] Exponent { get; set; }
        public byte[] InverseQ { get; set; }
        public byte[] Modulus { get; set; }
        public byte[] P { get; set; }
        public byte[] Q { get; set; }
    }

上面这些都是准备工作,下面开始配置:
打开Startup.cs文件,在ConfigureServices方法中添加:

           //Jwt验证
            //http://www.cnblogs.com/rocketRobin/p/8058760.html
            services.AddAuthentication(options => {
                options.DefaultAuthenticateScheme = "JwtBearer";
                options.DefaultChallengeScheme = "JwtBearer";                
            })            
            .AddJwtBearer("JwtBearer", jwtBearerOptions =>
            {
                jwtBearerOptions.RequireHttpsMetadata = false;
                
                jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
                {                    
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = TokenAuthOption.Key,
                    ValidateIssuer = true,
                    ValidIssuer = TokenAuthOption.Issuer,//The name of the issuer,
                    ValidateAudience = true,
                    ValidAudience = TokenAuthOption.Audience,//The name of the audience,
                    ValidateLifetime = true, //validate the expiration and not before values in the token
                    ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
                };
                jwtBearerOptions.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();
                        c.Response.StatusCode = 200;
                        c.Response.ContentType = "application/json";
                        return c.Response.WriteAsync(
                            JsonConvert.SerializeObject(
                                new Biz126.Models.ReturnModel<object>()
                                {
                                    Status = false,
                                    Message = "登录信息已失效",
                                    Code = 403,
                                    ResultData = 
                                    new
                                    {
                                        tokenExpired = true
                                    }
                                }
                            )
                        );
                    }
                };
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();  

这里需要注意:如果有开启CORS跨域的话,一定要放在CORS的配置之后。
还是在Startup.cs文件中,在Configure方法内,如果有CORS配置的话,也是在CORS配置之后,添加:

app.UseAuthentication();

我的完整的Startup.cs文件如下,包含了Redis、数据库连接、CORS跨域、log4net和Jwt验证:

    public class Startup
    {
        public static ILoggerRepository repository { get; set; }
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();

            if (env.IsDevelopment())
            {
                // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
                builder.AddApplicationInsightsSettings(developerMode: true);
            }
            Configuration = builder.Build();
            RedisHelper.InitializeConfiguration(Configuration);          //Redis  

            repository = LogManager.CreateRepository("NETCoreRepository");
            XmlConfigurator.Configure(repository, new FileInfo("log4net.config"));
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions();
            services.Configure<Dictionary<string, string>>(Configuration.GetSection("ConnectionStrings"));   //数据库连接

            //CORS跨域
            services.AddCors(options =>
            {
                options.AddPolicy("AnyOrigin", builder =>
                {
                    builder
                        .WithOrigins("http://localhost:65176")
                        .AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .AllowCredentials();
                });
            });

            services.Configure<MvcOptions>(options =>
            {
                options.Filters.Add(new CorsAuthorizationFilterFactory("AnyOrigin"));
            });

            //Jwt验证
            //http://www.cnblogs.com/rocketRobin/p/8058760.html
            services.AddAuthentication(options => {
                options.DefaultAuthenticateScheme = "JwtBearer";
                options.DefaultChallengeScheme = "JwtBearer";                
            })            
            .AddJwtBearer("JwtBearer", jwtBearerOptions =>
            {
                jwtBearerOptions.RequireHttpsMetadata = false;
                
                jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
                {                    
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = TokenAuthOption.Key,
                    ValidateIssuer = true,
                    ValidIssuer = TokenAuthOption.Issuer,//The name of the issuer,
                    ValidateAudience = true,
                    ValidAudience = TokenAuthOption.Audience,//The name of the audience,
                    ValidateLifetime = true, //validate the expiration and not before values in the token
                    ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
                };
                jwtBearerOptions.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();
                        c.Response.StatusCode = 200;
                        c.Response.ContentType = "application/json";
                        return c.Response.WriteAsync(
                            JsonConvert.SerializeObject(
                                new Biz126.Models.ReturnModel<object>()
                                {
                                    Status = false,
                                    Message = "登录信息已失效",
                                    Code = 403,
                                    ResultData = 
                                    new
                                    {
                                        tokenExpired = true
                                    }
                                }
                            )
                        );
                    }
                };
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();         
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            loggerFactory.AddLog4Net();

            //CORS跨域
            app.UseCors("AnyOrigin");
            app.UseAuthentication();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "area",
                    template: "{area:exists}/{controller=Product}/{action=List}/{id?}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

    }

这里配置结束,下面是开始使用了:
先增加一个生成token的方法,我这里是单独拿的一个类来做的:
Authorize.cs类:

    public static class Authorize
    {
        /// <summary>
        /// 生成Token
        /// </summary>
        /// <param name="userToken"></param>
        /// <returns></returns>
        public static string GenerateToken(Biz126.User.Models.UserToken userToken)
        {
            var handler = new JwtSecurityTokenHandler();
            string jti = TokenAuthOption.Audience + userToken.User_Name + DateTime.Now.Add(TokenAuthOption.ExpiresSpan).Millisecond;
            jti = WebTools.Tools.MD5Cryptog(jti); // Jwt 的一个参数,用来标识 Token
            ClaimsIdentity identity = new ClaimsIdentity(
                new GenericIdentity(userToken.User_Name, "TokenAuth"),
                new[] {
                    new Claim("user_id", userToken.User_Id.ToString()), //用户ID
                    new Claim("user_name",userToken.User_Name), //用户名
                    new Claim("user_type",userToken.User_Type.ToString()),   //身份
                    new Claim("jti",jti,ClaimValueTypes.String) // jti,用来标识 token
                }
            );
            var securityToken = handler.CreateToken(new SecurityTokenDescriptor
            {
                Issuer = TokenAuthOption.Issuer,
                Audience = TokenAuthOption.Audience,
                SigningCredentials = TokenAuthOption.SigningCredentials,
                Subject = identity,
                NotBefore = DateTime.Now,
                Expires = DateTime.Now.Add(TokenAuthOption.ExpiresSpan)
            });
            return handler.WriteToken(securityToken);
        }
    }

重写IHttpContextAccessorExtension.cs类,得到当前用户信息:

    public static class IHttpContextAccessorExtension
    {
        public static User.Models.UserToken CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {
            var claimsIdentity = httpContextAccessor?.HttpContext?.User?.Identity as ClaimsIdentity;
            //var stringUserId = claimsIdentity?.Claims?.FirstOrDefault(x => x.Type.Equals("user_id", StringComparison.CurrentCultureIgnoreCase))?.Value;
            //int.TryParse(stringUserId ?? "0", out int userId);

            var userToken = new User.Models.UserToken();
            var claims = claimsIdentity?.Claims?.ToList();
            var propertys = userToken.GetType().GetProperties();
            foreach (PropertyInfo property in propertys)
            {
                string name = property.Name.ToLower();
                var value = claims?.FirstOrDefault(x => x.Type.Equals(name, StringComparison.CurrentCultureIgnoreCase))?.Value;
                property.SetValue(userToken, string.IsNullOrEmpty(value) ? null : Convert.ChangeType(value, property.PropertyType), null);
            }
            return userToken;
        }
    }

用户登录时,验证账号密码通过,调用Authorize.GenerateToken(userinfo)方法生成token返回给客户端;
客户端在请求头中增加"Authorization",值为"Bearer"+空格+Token,如“Bearer Header.Payload.Signature”

服务器WebAPI接口在控制器的构造函数中,这样写:

        private Biz126.Models.UserToken current_userToken = new Biz126.Models.UserToken();   //当前用户基本信息

        public MemberController(IOptions<Dictionary<string, string>> settings, IHttpContextAccessor httpContextAccessor)
        {
            current_userToken = httpContextAccessor.CurrentUser();
        }

就可以拿到保存在token中的用户信息了。

当然,记得要在需要使用验证的控制器或者方法上,增加[Authorize]特性,如果不需要验证的话,要增加[AllowAnonymous]。

以上。


添加新评论