.net core搭建认证服务器

124 阅读5分钟

搭建认证服务器

搭建步骤

1、新建一个minimapAPI项目

2、添加JWTTokenOptions实体类

public class JWTTokenOptions
    {
        public string? Audience { get; set; }

        public string? SecurityKey  {  get; set; }

        public string? Issuer  {  get; set;  }
    }

3、在配置文件中写入元信息

"JWTTokenOptions": {
  "Audience": "http://localhost:5200",
  "Issuer": "http://localhost:5200",
  "SecurityKey": "我是一个秘钥,秘钥长度尽量保证在16个字符以上"
}

4、在Program.cs中注册

//代表获取配置文件的JWTTokenOptions节点,映射到JWTTokenOptions对象中去
builder.Services.Configure<JWTTokenOptions>(builder.Configuration.GetSection("JWTTokenOptions"));

5、创建生成Token的抽象及实现类

抽象

public interface ICustomJWTService
{
    string GetToken(CurrentUser user);
}

对称可逆加密

通过构造函数注入的方式,读取到配置文件的信息

对称可逆加密的Key可以是任意的

public class CustomHSJWTService : ICustomJWTService
    {

        #region Option注入
        private readonly JWTTokenOptions _JWTTokenOptions;
        public CustomHSJWTService(IOptionsMonitor<JWTTokenOptions> jwtTokenOptions)
        {
            _JWTTokenOptions = jwtTokenOptions.CurrentValue;
        }
        #endregion

        /// <summary>
        /// 获取Token
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public string GetToken(CurrentUser user)
        {
            //1、准备有效载荷
            Claim[] claims = new[]
             {
               new Claim(ClaimTypes.Name, user.Name),
               new Claim("NickName",user.NikeName),
               new Claim(ClaimTypes.Role,user.RoleList),//传递其他信息   
               new Claim("Description",user.Description),
               new Claim("Age",user.Age.ToString()),
            };

            //2、准备加密key
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_JWTTokenOptions.SecurityKey));

            //Sha256 加密方式
            SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);


            JwtSecurityToken token = new JwtSecurityToken(
                  issuer: _JWTTokenOptions.Issuer,
                  audience: _JWTTokenOptions.Audience,
                  claims: claims,
                  expires: DateTime.Now.AddMinutes(5),//5分钟有效期
                  signingCredentials: creds);

            string returnToken = new JwtSecurityTokenHandler().WriteToken(token);

            return returnToken;

        }
    }

非对称可逆加密的Key需要帮助类库来生成

添加帮助类库

TryGetKeyParameters:判断路径中是否存在key,包含则读取,不包含则返回false

public class RSAHelper
    {
        /// <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)
        {
            //存放的文件名:私钥key.json 公钥key.public.json
            string filename = withPrivate ? "key.json" : "key.public.json";
            string fileTotalPath = Path.Combine(filePath, filename);
            keyParameters = default;
            if (!File.Exists(fileTotalPath))
            {
                return false;
            }
            else
            {
                keyParameters = JsonConvert.DeserializeObject<RSAParameters>(File.ReadAllText(fileTotalPath));
                return true;
            }
        }


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

            File.WriteAllText(Path.Combine(filePath, "key.public.json"), JsonConvert.SerializeObject(publicKeys));
            return withPrivate ? privateKeys : publicKeys;
        }
    }

非对称可逆加密

注意,想要在token中写入角色信息,不能自己写一个字符串"Role"作为键,而要用官方的ClaimTypes.Role(本质是一个长字符串)

//错误写法
new Claim("Role",user.RoleList!);
//正确写法
new Claim(ClaimTypes.Role,user.RoleList!);
public class CustomRSSJWTervice:ICustomJWTService
    {
        #region Option注入
        private readonly JWTTokenOptions _JWTTokenOptions;
        public CustomRSSJWTervice(IOptionsMonitor<JWTTokenOptions> jwtTokenOptions)
        {
            _JWTTokenOptions = jwtTokenOptions.CurrentValue;
        }

        /// <summary>
        /// 返回token
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public string GetToken(CurrentUser user)
        {
            #region 使用加密解密Key  非对称 
        	//指定当前的执行目录
            string keyDir = Directory.GetCurrentDirectory();
            if (RSAHelper.TryGetKeyParameters(keyDir, true, out RSAParameters keyParams) == false)
            {
                keyParams = RSAHelper.GenerateAndSaveKey(keyDir);
            }
            #endregion

            //1、准备有效载荷
            Claim[] claims = new[]
             {
               new Claim(ClaimTypes.Name, user.Name!),
               new Claim("NickName",user.NikeName!),
               new Claim(ClaimTypes.Role,user.RoleList!),//传递其他信息    "Role"
                new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role","teacher"),//传递其他信息    "Role"
                 new Claim(ClaimTypes.Role,"Student"),
                     new Claim(ClaimTypes.Role,"User"),
               new Claim("Description",user.Description!),
               new Claim("Age",user.Age.ToString()),
            };

            //2、准备加密key
            RsaSecurityKey key = new RsaSecurityKey(keyParams);

            //Sha256 加密方式
            SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature);


            JwtSecurityToken token = new JwtSecurityToken(
                  issuer: _JWTTokenOptions.Issuer,
                  audience: _JWTTokenOptions.Audience,
                  claims: claims,
                  expires: DateTime.Now.AddMinutes(5),//5分钟有效期
                  signingCredentials: creds);

            string returnToken = new JwtSecurityTokenHandler().WriteToken(token); 
            return returnToken;
        }
        #endregion
    }

6、调用生成token的方法

builder.Services.AddTransient<ICustomJWTService, CustomHSJWTService>();//对称可逆加密
builder.Services.AddTransient<ICustomJWTService, CustomRSSJWTervice>();//非对称可逆加密

app.MapPost("Login", (string name, string password, ICustomJWTService _iJWTService) =>
{ 
    //在这里需要去数据库中做数据验证
    if ("Richard".Equals(name) && "123456".Equals(password)) 
    {
        //从数据库中查询出来的
        var user = new CurrentUser()
        {
            Id = 123,
            Name = "Richard",
            Age = 36,
            NikeName = "金牌讲师Richard老师",
            Description = ".NET架构师",
            RoleList = "admin"
        }; 
        //就应该生成Token 
        string token = _iJWTService.GetToken(user);
        return JsonConvert.SerializeObject(new
        {
            result = true,
            token
        }); 
    }
    else
    {
        return JsonConvert.SerializeObject(new
        {
            result = false,
            token = ""
        });
    }
});

授权

鉴权 --- 读取请求方带过来的各种渠道的用户信息,解析写入到HttpContext.user中去

授权 ---根据用户的信息,去检查当前用户是否可以去请求资源

如何对控制器类、方法授权访问

ASP.NET Core会按照HTTP协议的规范,从Authorization取出来令牌,并且进行校验、解析,然后把解析结果填充到User用户属性中,这一切都是ASP.NET Core完成的,不需要开发人员自己编写代码

  • 控制器类上标注[Authorize],则所有操作方法都会被进行身份验证和授权验证
  • 对于标注了[Authorize]的控制器中,如果其中某个操作方法不想被验证,可以在操作方法上添加[AllowAnonymous]
  • 如果没有在控制器类上标注[Authorize],那么这个控制器中的所有操作方法都允许被自由地访问
  • 对于没有标注[Authorize]的控制器中,如果其中某个操作方法需要被验证,可以在操作方法上添加[Authorize]

一旦出现401,没有详细的报错信息,很难排查,这是初学者遇到的难题

使用步骤

1、新建JWTTokenOptions类。在配置文件中写入配置信息

public class JWTTokenOptions
    {
        public string? Audience { get; set; }

        public string? SecurityKey { get; set; }

        public string? Issuer { get; set; }
    }
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "JWTTokenOptions": {
    "Audience": "http://localhost:5200",
    "Issuer": "http://localhost:5200",
    "SecurityKey": "我是一个秘钥,秘钥长度尽量保证在16个字符以上"
  }
}

2、在Program.cs中,启用鉴权、授权

对称可逆加密的鉴权、授权

JWTTokenOptions tokenOptions = new JWTTokenOptions();
builder.Configuration.Bind("JWTTokenOptions", tokenOptions);
builder.Services
    .AddAuthorization() //启用授权
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)//鉴权,参数是鉴权的渠道jwt
     .AddJwtBearer(options =>  //这里是配置的具体鉴权的逻辑
     {
        //授权时,需要先检查的参数
         options.TokenValidationParameters = new TokenValidationParameters
         {
             //JWT有一些默认的属性,就是给鉴权时就可以筛选了
             ValidateIssuer = true,//是否验证Issuer
             ValidateAudience = true,//是否验证Audience
             ValidateLifetime = true,//是否验证失效时间
             ValidateIssuerSigningKey = true,//是否验证SecurityKey
             ValidAudience = tokenOptions.Audience,//
             ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
             IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenOptions.SecurityKey)),
             AudienceValidator = (m, n, z) =>
             {
                 //这里可以写自己定义的验证逻辑
                 //return m != null && m.FirstOrDefault().Equals(builder.Configuration["audience"]);  
                 return true;
             },
             LifetimeValidator = (notBefore, expires, securityToken, validationParameters) =>
             {
                 //return notBefore <= DateTime.Now
                 //&& expires >= DateTime.Now;
                 ////&& validationParameters

                 return true;

             }//自定义校验规则
         };
     });
}

app.UseAuthentication(); //鉴权 
app.UseAuthorization(); //授权 

非对称可逆加密的鉴权、授权

需要将公钥解密的key交给鉴权授权的项目

//先读取到公钥key
string path = Path.Combine(Directory.GetCurrentDirectory(), "key.public.json");
string key = File.ReadAllText(path);//this.Configuration["SecurityKey"];
Console.WriteLine($"KeyPath:{path}");
//反序列化为对象
RSAParameters keyParams = JsonConvert.DeserializeObject<RSAParameters>(key);
//读取配置文件的信息
JWTTokenOptions tokenOptions = new JWTTokenOptions();
builder.Configuration.Bind("JWTTokenOptions", tokenOptions);

builder.Services
    .AddAuthorization(Options =>
    {
        //策略授权
        Options.AddPolicy("PammionPolicy", builder =>
        {
    		//在这里就是判断逻辑
            builder.RequireRole("admin");
            builder.RequireUserName("Richard");
            builder.RequireAssertion(context =>
            {
                HttpContext httpcontext = context.Resource as HttpContext;
                //if (context.User.FindFirst(c => c.Type == "Role") == null)
                //{
                //    return false;
                //}
                //else
                //{
                //    return true;
                //} 
                //context.User.Claims.Count(c=>c.Type== ClaimTypes.Role)>=3
                return true;
            });
        	//扩展策略授权,PermissionRequirement是自定义的
            builder.Requirements.Add(new PermissionRequirement());
        });
    }) //启用授权
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,//是否验证Issuer
            ValidateAudience = true,//是否验证Audience
            ValidateLifetime = true,//是否验证失效时间
            ValidateIssuerSigningKey = true,//是否验证SecurityKey
            ValidAudience = tokenOptions.Audience,//Audience
            ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
            IssuerSigningKey = new RsaSecurityKey(keyParams),
            IssuerSigningKeyValidator = (m, n, z) =>
            {
                Console.WriteLine("This is IssuerValidator");
                return true;
            },
            IssuerValidator = (m, n, z) =>
            {
                Console.WriteLine("This is IssuerValidator");
                return "http://localhost:5726";
            },
            AudienceValidator = (m, n, z) =>
            {
                Console.WriteLine("This is AudienceValidator");
                return true;
        //return m != null && m.FirstOrDefault().Equals(this.Configuration["Audience"]);
            },//自定义校验规则,可以新登录后将之前的无效
        };
    });

3、方法标注[Authorize]特性

app.MapGet("getStringPara", [Authorize] (string str) => str)

Swagger配置支持Token传参数

1、添加安全定义

2、添加安全要求

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new() { Title = "朝夕教育 MinimalApi", Version = "v1" });
    options.SwaggerDoc("v2", new() { Title = "朝夕教育 MinimalApi", Version = "v2" });
    options.OperationFilter<SwaggerFileUploadFilter>();

    #region Swagger配置支持Token参数传递 
    //添加安全定义
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",
        Name = "Authorization",
        In = ParameterLocation.Header,//从头信息传进来
        Type = SecuritySchemeType.ApiKey,
        BearerFormat = "JWT",
        Scheme = "Bearer"  //JwtBearerDefaults.AuthenticationScheme
    });

    //添加安全要求
    options.AddSecurityRequirement(new OpenApiSecurityRequirement {
    {
        new OpenApiSecurityScheme
        {
            Reference =new OpenApiReference()
            {
                Type = ReferenceType.SecurityScheme,
                Id ="Bearer"
            }
        },
        new string[]{ }
    }
});

    #endregion
});

角色授权

先鉴权---再授权

在鉴权以后,已经正常解析了用户的信息,开始授权,就是判定用户信息中是否包含某一个角色;

//如果需要满足多个角色,多个角色之间并且关系---可以标记多个特性,分别指定角色
app.MapGet("getStringPara",
    [Authorize(Roles = "admin")][Authorize(Roles = "student")]
(string str) => str);
//如果是或者关系,标记一次特性,角色以逗号分割
app.MapGet("getStringPara",
    [Authorize(Roles = "admin,Student,Richard")]
(string str) => str);

策略授权

定义策略

在API标记特性的时候,指定策略,那么访问当前API,就需要按照定义的策略来授权验证。

    app.MapGet("getStringPara",
        [Authorize(policy: "PammionPolicy")]
    (string str) => str);

扩展策略授权

1、添加PermissionRequirement

public class PermissionRequirement: IAuthorizationRequirement
    {

    }

2、添加PermissionHandler,用于验证业务逻辑

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
    	//下面这个只是个注入示例
        private ICompanyService _ICompanyService;

        public PermissionHandler(ICompanyService companyService)
        { 
            this._ICompanyService= companyService;
        }

        /// <summary>
        /// 在这里就可以扩展逻辑
        /// </summary>
        /// <param name="context"></param>
        /// <param name="requirement"></param>
        /// <returns></returns>
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
        {
            HttpContext httpContext = context.Resource as HttpContext;
            bool? isAuthenticated = context?.User?.Identity?.IsAuthenticated; //是否解析到用户信息了  
             
            if (isAuthenticated != null && isAuthenticated.Value)
            {
                context?.Succeed(requirement); // 验证成功了刷
            }
            else
            {
                context?.Fail();
            } 
            return Task.CompletedTask;
        }
    }

3、注入自定义策略处理类型

builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

封装优化

为了解决什么问题?所有代码都写到了Program.cs中,导致代码太长,可以进行封装

示例:将鉴权授权的方法进行封装

1、新建静态类及封装的扩展方法

public static class AuthenticationExtension
    {
        public static void AuthenticationHsExt(this WebApplicationBuilder builder)
        {
            #region Jwt对称可逆加密方式-鉴权授权
            {
                JWTTokenOptions tokenOptions = new JWTTokenOptions();
                builder.Configuration.Bind("JWTTokenOptions", tokenOptions);
                builder.Services
                    .AddAuthorization() //启用授权
                    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                     .AddJwtBearer(options =>  //这里是配置的鉴权的逻辑
                     {
                         options.TokenValidationParameters = new TokenValidationParameters
                         {
                             //JWT有一些默认的属性,就是给鉴权时就可以筛选了
                             ValidateIssuer = true,//是否验证Issuer
                             ValidateAudience = true,//是否验证Audience
                             ValidateLifetime = true,//是否验证失效时间
                             ValidateIssuerSigningKey = true,//是否验证SecurityKey
                             ValidAudience = tokenOptions.Audience,//
                             ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
                             IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenOptions.SecurityKey)),
                             AudienceValidator = (m, n, z) =>
                             {
                                 //这里可以写自己定义的验证逻辑
                                 //return m != null && m.FirstOrDefault().Equals(builder.Configuration["audience"]);  
                                 return true;
                             },
                             LifetimeValidator = (notBefore, expires, securityToken, validationParameters) =>
                             {
                                 //return notBefore <= DateTime.Now
                                 //&& expires >= DateTime.Now;
                                 ////&& validationParameters

                                 return true;

                             }//自定义校验规则
                         };
                     });
            }
            #endregion
        }


        public static void AuthenticationRssExt(this WebApplicationBuilder builder)
        {
            string path = Path.Combine(Directory.GetCurrentDirectory(), "key.public.json");
            string key = File.ReadAllText(path);//this.Configuration["SecurityKey"];
            Console.WriteLine($"KeyPath:{path}");
            RSAParameters keyParams = JsonConvert.DeserializeObject<RSAParameters>(key);

            JWTTokenOptions tokenOptions = new JWTTokenOptions();
            builder.Configuration.Bind("JWTTokenOptions", tokenOptions);

            builder.Services
                .AddAuthorization(Options =>
                {
                    Options.AddPolicy("PammionPolicy", builder =>
                    {
                //在这里就是判断逻辑
                        builder.RequireRole("admin");
                        builder.RequireUserName("Richard");
                        builder.RequireAssertion(context =>
                        {
                            HttpContext httpcontext = context.Resource as HttpContext;
                    //if (context.User.FindFirst(c => c.Type == "Role") == null)
                    //{
                    //    return false;
                    //}
                    //else
                    //{
                    //    return true;
                    //} 
                    //context.User.Claims.Count(c=>c.Type== ClaimTypes.Role)>=3
                            return true;
                        });

                        builder.Requirements.Add(new PermissionRequirement());
                    });
                }) //启用授权
                .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,//是否验证Issuer
                        ValidateAudience = true,//是否验证Audience
                        ValidateLifetime = true,//是否验证失效时间
                        ValidateIssuerSigningKey = true,//是否验证SecurityKey
                        ValidAudience = tokenOptions.Audience,//Audience
                        ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
                        IssuerSigningKey = new RsaSecurityKey(keyParams),
                        IssuerSigningKeyValidator = (m, n, z) =>
                        {
                            Console.WriteLine("This is IssuerValidator");
                            return true;
                        },
                        IssuerValidator = (m, n, z) =>
                        {
                            Console.WriteLine("This is IssuerValidator");
                            return "http://localhost:5726";
                        },
                        AudienceValidator = (m, n, z) =>
                        {
                            Console.WriteLine("This is AudienceValidator");
                            return true;
                    //return m != null && m.FirstOrDefault().Equals(this.Configuration["Audience"]);
                        },//自定义校验规则,可以新登录后将之前的无效
                    };
                });

        }
    }

2、使用此扩展方法

builder.AuthenticationRssExt();