WebApi+Grpc+Jwt身份认证

使用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);

感谢B站Up主“软件工艺师”。
https://gitee.com/zeran/core01.git

仅供参考,内容中会引用部分博友的文章。(侵删)
原文地址:https://www.cnblogs.com/zeran/p/14481591.html