使用Grpc做服务间通信,使用JWT,JWT可以使用在前端,后端,微服务等。
服务端:
首先需要安装nuget包 Microsoft.AspNetCore.Authentication.JwtBearer
首先创建JWTHelp.cs
using DataService01.protos;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace DataService01.Models
{
public class JWTHelper
{
/// <summary>
/// 创建token
/// </summary>
/// <param name="_users"></param>
/// <param name="jwtDTO"></param>
/// <returns></returns>
public async Task<TokenModel> IssueJwt(users _users, JWTDTO jwtDTO)
{
var exp = $"{new DateTimeOffset(DateTime.Now.AddMinutes(jwtDTO.ExpireMinutes)).ToUnixTimeMilliseconds()}";
List<Claim> claims = new List<Claim>() {
new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeMilliseconds()}"),
new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeMilliseconds()}"),
new Claim(JwtRegisteredClaimNames.Exp,exp),
new Claim(JwtRegisteredClaimNames.Iss,jwtDTO.Issuer),
new Claim(JwtRegisteredClaimNames.Aud,jwtDTO.Audience),
new Claim(ClaimTypes.Name,_users.Name),
new Claim(ClaimTypes.Role,_users.Roleid.ToString()),
new Claim("loginname",_users.LoginName),
new Claim("isman",_users.IsMan.ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtDTO.SecurityKey));
var cerds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: jwtDTO.Issuer,audience:jwtDTO.Audience, claims: claims,expires:DateTime.Now.AddMinutes(jwtDTO.ExpireMinutes), signingCredentials: cerds);
string jwt_token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
// jwtSecurityToken.ValidTo;//过期时间
return new TokenModel {
Token = jwt_token,
ExpireTime = jwtSecurityToken.ValidTo,
Success = "ok"
};
}
/// <summary>
/// 解析jwt中的明文内容,不建议放关键信息
/// </summary>
/// <param name="jwtstr"></param>
/// <returns></returns>
public users SerializeJwt(string jwtstr)
{
var jwt_hender = new JwtSecurityTokenHandler();
JwtSecurityToken securityToken = jwt_hender.ReadJwtToken(jwtstr);
users _users = new users()
{
//ID = Convert.ToInt32(securityToken.Id),
Name = securityToken.Payload[ClaimTypes.Name].ToString(),
LoginName = securityToken.Payload["loginname"].ToString(),
Roleid = Convert.ToInt32(securityToken.Payload[ClaimTypes.Role] ?? 0),
IsMan = Convert.ToBoolean(securityToken.Payload["isman"])
};
return _users;
}
}
public class TokenModel
{
public string Token { get; set; }
public DateTime ExpireTime { get; set; }
public string Success { get; set; }
}
}
JwtHelp.cs
在服务端的Startup.cs中添加身份认证和授权
首先在 ConfigureServices 方法中增加
services.AddAuthorization(option => option.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("sub");
}));
//services.AddAuthorization();
services.AddAuthentication().AddJwtBearer(options=> {
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "http://localhost:5001",
ValidAudience = "http://localhost:5000",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetSection("JWTDTO").GetSection("SecurityKey").Value))// "9e79234cd150108e5048d0e0cb4ca5e4"
};
});
View Code
然后在Configure方法中增加
app.UseAuthentication();
app.UseAuthorization();
在需要使用认证的服务中添加
[Authorize(AuthenticationSchemes =JwtBearerDefaults.AuthenticationScheme)]
JWT所使用的{发行方、使用方、key、超时时间等}放在了appsetting.json里
Startup.cs代码参考
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DataService01.Models;
using DataService01.protos;
using DataService01.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace DataService01
{
public class Startup
{
private readonly IConfiguration configuration;
public Startup(IConfiguration configuration)
{
this.configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
services.AddAuthorization(option => option.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("sub");
}));
//services.AddAuthorization();
services.AddAuthentication().AddJwtBearer(options=> {
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "http://localhost:5001",
ValidAudience = "http://localhost:5000",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetSection("JWTDTO").GetSection("SecurityKey").Value))// "9e79234cd150108e5048d0e0cb4ca5e4"
};
});
services.Configure<JWTDTO>(configuration.GetSection("JWTDTO"));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
//endpoints.MapGet("/", async context =>
//{
// await context.Response.WriteAsync("Hello World!");
//});
endpoints.MapGrpcService<ds01>();
// endpoints.MapGrpcService<ds02>();
});
}
}
}
Startup.cs
为了方便使用jwt的配置信息,创建一个JWTDTO类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DataService01.Models
{
public class JWTDTO
{
/// <summary>
/// 颁发者
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// 使用者
/// </summary>
public string Audience { get; set; }
/// <summary>
/// 密钥key
/// </summary>
public string SecurityKey { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public int ExpireMinutes { get; set; }
}
}
JWTDTO
在ConfigureServices 注册,在ds01.cs(服务实现类)的构造函数中注入
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Grpc.AspNetCore.Server;
using Grpc.AspNetCore;
using DataService01.protos;
using Grpc.Core;
using System.IO;
using Microsoft.AspNetCore.Authorization;
using System.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using DataService01.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace DataService01.Services
{
[Authorize(AuthenticationSchemes =JwtBearerDefaults.AuthenticationScheme)]
public class ds01 : userservice.userserviceBase
{
private readonly ILogger<ds01> logger;
private readonly IConfiguration configuration;
private readonly IOptions<JWTDTO> jwt_Options;
public ds01(ILogger<ds01> logger,IConfiguration configuration,IOptions<JWTDTO> Jwt_options)
{
this.logger = logger;
this.configuration = configuration;
jwt_Options = Jwt_options;
}
[AllowAnonymous]
public override async Task<return_token> gettoken(get_token request, ServerCallContext context)
{
users _user = new users();
_user.LoginName = request.LoginName;
if (request.LoginName.Equals("admin") && request.Password.Equals("123456"))
{
var jwttoken=await new JWTHelper().IssueJwt(_user, jwt_Options.Value);
return await Task.FromResult(new return_token() { Token = jwttoken.Token, ExpireTime = new DateTimeOffset(jwttoken.ExpireTime).ToUnixTimeSeconds().ToString() });
}
return await Task.FromResult(new return_token() { Token = "", ExpireTime = "" });
}
public override Task<getusersresponse> Getuser(getusers request, ServerCallContext context)
{
var matedata_md=context.RequestHeaders;
foreach (var pire in matedata_md)
{
logger.LogInformation($"{pire.Key}:{pire.Value}");
logger.LogInformation(pire.Key+":"+pire.Value);
}
users item = userdatas.userslist.SingleOrDefault(n => n.ID == request.ID);
if (item != null)
{
return Task.FromResult(new getusersresponse() { Code = 0, Msg = "成功", Usermodel = item });
}
else
{
return Task.FromResult(new getusersresponse() { Code = -1, Msg = "失败" });
}
}
public override async Task getall(getusers request, IServerStreamWriter<getusersresponse> responseStream, ServerCallContext context)
{
foreach (var item in userdatas.userslist)
{
//逐步返回数据
await responseStream.WriteAsync(new getusersresponse()
{
Usermodel = item
}
) ;
}
}
public override async Task<getusers> Add(IAsyncStreamReader<addphoto> requestStream, ServerCallContext context)
{
List<byte> bt = new List<byte>();
while (await requestStream.MoveNext())//有数据进入
{
bt.AddRange(requestStream.Current.Data);
}
//while 执行完之后表示没有数据再进来
FileStream file = new FileStream(AppDomain.CurrentDomain.BaseDirectory+"01.png",FileMode.OpenOrCreate) ;
file.Write(bt.ToArray(), 0, bt.Count);
file.Flush();
file.Close();
return new getusers() { Name = "成功",ID = 0 };
}
public override async Task saveall(IAsyncStreamReader<addphoto> requestStream, IServerStreamWriter<getusersresponse> responseStream, ServerCallContext context)
{
List<byte> bt = new List<byte>();
while (await requestStream.MoveNext())//有数据进入
{
bt.AddRange(requestStream.Current.Data);
}
//while 执行完之后表示没有数据再进来
FileStream file = new FileStream("/01.png", FileMode.OpenOrCreate);
file.Write(bt.ToArray(), 0, bt.Count);
file.Flush();
file.Close();
//返回数据
foreach (var item in userdatas.userslist)
{
await responseStream.WriteAsync(new getusersresponse()
{
Msg = "成功",
Code = 0,
Usermodel = item
});
}
}
}
public class userdatas
{
public static IList<users> userslist = new List<users>() {
new users(){ID=1,Name="11",LoginName="111",Roleid=1,IsMan=true},
new users(){ID=2,Name="22",LoginName="222",Roleid=2,IsMan=false},
new users(){ID=3,Name="33",LoginName="333",Roleid=3,IsMan=true}
};
}
}
ds01
在ds01 中增加一个获取jwttoken的方法,设置不鉴权
[AllowAnonymous]
public override async Task<return_token> gettoken(get_token request, ServerCallContext context)
{
users _user = new users();
_user.LoginName = request.LoginName;
if (request.LoginName.Equals("admin") && request.Password.Equals("123456"))
{
var jwttoken=await new JWTHelper().IssueJwt(_user, jwt_Options.Value);
return await Task.FromResult(new return_token() { Token = jwttoken.Token, ExpireTime = new DateTimeOffset(jwttoken.ExpireTime).ToUnixTimeSeconds().ToString() });
}
return await Task.FromResult(new return_token() { Token = "", ExpireTime = "" });
}
#region 以下一段是jwt刷新的内容,我没有在代码中实现,仅参考
由于jwt是无状态的,jwt超时后需要更新,不然刚刚还在操作的用户就被提示 认证失败了,所以需要在token过期前更新token。
网上看到一些方案 设置两个时间,一个是token有效时间(20分钟),另一个是token存活的时间(1小时,将token存放在redis中,1小时是redis的有效时间),如果超过了token时间,就到redis中取,我个人认为不太好。
看了下另外一个项目案例的方案觉得可以:token时间为20分钟,当请求时如果超过了时间的2/3,则返回202状态吗;客户端接收到202状态码后,客户端重新获取token,获取后再次请求业务逻辑;
案例是vue写的,以下为服务端与客户端代码:
function post(url, params, showLoading,config) {
_showLoading = showLoading;
axios.defaults.headers[_Authorization] = getToken();
return new Promise((resolve, reject) => {
// axios.post(url, qs.stringify(params)) //
axios.post(url, params,config)
.then(response => {
if (response.status == 202) {
getNewToken(() => { post(url, params, _showLoading); });
return;
}
resolve(response.data);
}, err => {
if (err.status == 202) {
getNewToken(() => { post(url, params, _showLoading); });
return;
}
reject(err.data && err.data.message ? err.data.message : '网络好像出了点问题~~');
})
.catch((error) => {
reject(error)
})
})
}
//当前token快要过期时,用现有的token换成一个新的token
function getNewToken(callBack) {
ajax({
url: "/api/User/replaceToken",
param: {},
json: true,
success: function (x) {
if (x.status) {
let userInfo = $httpVue.$store.getters.getUserInfo();
userInfo.token = x.data;
currentToken = x.data;
$httpVue.$store.commit('setUserInfo', userInfo);
callBack();
} else {
console.log(x.message);
toLogin();
}
},
errror: function (ex) {
console.log(ex);
toLogin();
},
type: "post",
async: false
});
}
vue客户端
客户端判断状态码为202时,先调用gettoken()gettoken的callback 是递归调用原方法。
DateTime expDate = context.HttpContext.User.Claims.Where(x => x.Type == JwtRegisteredClaimNames.Exp)
.Select(x => x.Value).FirstOrDefault().GetTimeSpmpToDate();
//如果过期时间小于设置定分钟数的1/3时,返回状态需要刷新token
if (expDate < DateTime.Now || (expDate - DateTime.Now).TotalMinutes < AppSetting.ExpMinutes / 3)
{
context.FilterResult(HttpStatusCode.Accepted, "Token即将过期,请更换token");//202
return;
}
服务端验证过期时间
服务端为Webapi 在Startup.cs的 mvc增加两个过滤器,其中第一个是 用来检验jwt的。服务端返回202状态码的这段,写在过滤器里就可以。
#endregion
至此服务端代码结束。
客户端
using GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001");
var service01 = new userservice.userserviceClient(channel);
return_token rt_token= service01.gettoken(new get_token() { LoginName = "admin", Password = "123456" });//获取JWTtoken
var md_add_token = new Metadata() ;
if (!string.IsNullOrEmpty(rt_token.Token))
{
md_add_token.Add("Authorization", $"Bearer {rt_token.Token}");//将Token 添加到 Hearers里,key和Value 是固定写法,value中 Bearer 与token中间 要加一个空格
}
getusersresponse us = await service01.GetuserAsync(new getusers() { ID = 2 },headers:md_add_token);//一元请求+传送元数据// header是Jwttoken
_logger.LogInformation(us.Msg);