近日,把之前使用.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.SignatureHeader
在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的合法性,进行下一步操作。
这里就涉及到了签名和验签两部分。先是签名:
在自己的项目中,找一个适合的地方,添加以下几个类:
- RSAKeyHelper.cs
- TokenAuthOption.cs
- 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]。
以上。
本文作者:老徐
本文链接:https://bigger.ee/archives/122.html
转载时须注明出处及本声明