Implement a login: Mac+. NET 5+Identity+JWT+VS Code

Implement a login: Mac+. NET 5+Identity+JWT+VS Code

Share a small login case you learned before

最后更新 10/18/2021 4:51 PM
Jarry
预计阅读 10 分钟
分类
ASP.NET Core
标签
.NET C# ASP.NET Core Web API authentication

Share a small login case you learned before. Please correct if there are any shortcomings in the code!!!

** Tool: Developed using VS Code and its plug-ins, it is lightweight and reduces command-line typing. There is no conflict when using VS **

1. Create a WebApi project through plug-ins

原文是个动图,可点击原文查看

2. Use the plug-in to download the NuGet packages required by the project

3. Code writing

① Create a new User entity

/// <summary>
/// 登录用户实体类  继承Identiy框架提供的 IdentityUser类
/// </summary>
public class AppUser:IdentityUser
{
    // 自己再扩充三个字段
    public DateTime DateCreated { get; set; }
    public DateTime DateModified { get; set; }

    public string FullName { get; set; }
}

② Create a new context class

public class AppDBContext : IdentityDbContext<AppUser, IdentityRole, string>
{
    public AppDBContext(DbContextOptions options) : base(options)
    {
    }
}

③ Dependency injection context class in Startup

services.AddDbContext<AppDBContext>(options =>
{
    options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), MySqlServerVersion.LatestSupportedServerVersion);
});
// AddEntityFrameworkStores 用来创建 用户和密码之间的服务
services.AddIdentity<AppUser, IdentityRole>(opt => { }).AddEntityFrameworkStores<AppDBContext>();

④ Generate a data table at terminal codefirst

dotnet ef migrations add init
dotnet ef  database update

④ Configure JWT

Configure services in the ConfigureServices method

services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddJwtBearer(options =>
    {
        // jwt的 key 需要设置复杂点
        var key = Encoding.ASCII.GetBytes(Configuration["JWTConfig:Key"]);
        var issure = Configuration["JWTConfig:Issuer"];   // 发行人
        var audience = Configuration["JWTConfig:Audience"];  // 受众
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = true, // 设置为True时 ValidIssure 属性设置下 不然jwt验证不会通过
            ValidateAudience = true, // 同上 ValidAudience 属性设置下
            RequireExpirationTime = true,
            ValidateLifetime=true,   //  token失效缓冲时间 默认是五分钟 失效时间需要加上这五分钟缓冲
            //  如果 上面 ValidateIssuer  配置为false 则不需要下面两个属性
            ValidIssuer = issure,
            ValidAudience = audience,

        };
    });
// 多角色时 可以这样配置  [Authorize(Policy ="PolicyGroup")] 动作方法上可以简写
services.AddAuthorization(options =>
{
    options.AddPolicy("PolicyGroup", policy => policy.RequireRole("Admin", "User"));
});

Use services in the Configure method

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerDemo v1"));
    }

    app.UseHttpsRedirection();
    app.UseCors("any");
    app.UseRouting();
    // 注意顺序
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

④ swagger configuration

Configure services in the ConfigureServices method

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "SwaggerDemo", Version = "v1", Description = "Demo API for showing Swagger" });

    // 下面两步配置 实现 swagger 上面 “锁”
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,  // 位于Header
        Description = "请于此处直接填写token 无需 Bearer然后再加空格的形式",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement{
        {
            new OpenApiSecurityScheme{
                Reference=new OpenApiReference{
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },Array.Empty<string>()
        }
    });

    // swagger接口注释显示
    // 注意 vscode 用户需要在项目的csproj文件里面手动配置生成注释文档的属性
    // 具体参见项目文件里的PropertyGroup
    var fileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var filePath = Path.Combine(AppContext.BaseDirectory, fileName);
    c.IncludeXmlComments(filePath);
});

Use services in the Configure method

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        // swagger 中间件使用
        app.UseSwagger();
        // 此处的 v1 必须与上面c.SwaggerDoc("v1") 里的一致
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerDemo v1"));
    }

    app.UseHttpsRedirection();
    app.UseCors("any");
    app.UseRouting();
    // 注意顺序
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

③ Create a UserController and inject the login service through the constructor

private readonly UserManager<AppUser> _userManger;  // 用户服务
private readonly SignInManager<AppUser> _signInManger;  // 登录服务

private readonly RoleManager<IdentityRole> _roleManger; // 角色服务
private readonly JWTConfig _jwtConfig;  // 配置框架将配置文件注入实体类
public UserController(ILogger<UserController> logger, UserManager<AppUser> userManager,
        SignInManager<AppUser> signInManager, IOptions<JWTConfig> jwtConfig, RoleManager<IdentityRole> roleManger)
{
    this._logger = logger;
    this._userManger = userManager;
    this._signInManger = signInManager;
    this._jwtConfig = jwtConfig.Value;
    this._roleManger = roleManger;
}

Registered user

/// <summary>
/// 用户注册
/// AddAndUpdateUserrRegisterModel 是一个Dto 接受对象
/// AllowAnonymous 不需要权限验证
/// 作者 xxxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("RegisterUser")]
public async Task<Object> RegisterUser(AddAndUpdateUserrRegisterModel model)
{
    try
    {
        // check  注册的时候是否包含角色
        if (model.Roles is null || model.Roles.Count <= 0)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "角色不能为空"));
        }
        // 循环判断用户所注册的角色时候存在 创建角色的方法  AddRole()
        foreach (var item in model.Roles)
        {
            if (!await _roleManger.RoleExistsAsync(item))
            {
                return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "不存在创建的角色"));
            }
        }
        // 生成一个用户类
        var user = new AppUser()
        {
            UserName = model.Email,
            FullName = model.FullName,
            Email = model.Email,
            DateCreated = DateTime.Now,
            DateModified = DateTime.UtcNow
        };
        // 注册用户
        var result = await _userManger.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            // 注册成功后 获取临时刚刚创建的用户
            var tempUser = await _userManger.FindByEmailAsync(model.Email);
            // 循环给创建的用户添加角色
            foreach (var role in model.Roles)
            {
                await _userManger.AddToRoleAsync(tempUser, role); // 添加角色
            }
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "用户被成功注册!", null));
        }
        // 创建用户失败返回
        return await Task.FromResult(string.Join(",", result.Errors.Select(x => x.Description).ToArray()));
    }
    catch (System.Exception ex)
    {
        // 异常返回
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, ex.Message, null));
    }
}

Login

/// <summary>
/// 生成Token
/// 作者 xxxx
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
private string GenarateToken(AppUser user, List<string> roles)
{
    var jwtTokenHandle = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_jwtConfig.Key);

    // 配置 Subject
    var claims = new List<Claim>()
    {
        new Claim(JwtRegisteredClaimNames.NameId,user.Id),
        new Claim(JwtRegisteredClaimNames.Email,user.Email),
        new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
    };
    foreach (var role in roles)
    {
        claims.Add(new Claim(ClaimTypes.Role,role));
    }
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        // 多重角色
        Subject=new ClaimsIdentity(claims),

        // 单一角色
        // Subject = new ClaimsIdentity(new[]
        // {
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.NameId,user.Id),
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.Email,user.Email),
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
        //     // new System.Security.Claims.Claim(ClaimTypes.Role,"role")
        // }),

        // 过期时间 12小时
        Expires = DateTime.UtcNow.AddSeconds(6),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
        Audience = _jwtConfig.Audience,  // 这里不配置 也会返回UnAuthorized
        Issuer = _jwtConfig.Issuer // 同上
    };
    // 创建token
    var token = jwtTokenHandle.CreateToken(tokenDescriptor);
    return jwtTokenHandle.WriteToken(token);
}
/// <summary>
/// 用户登录
/// LoginModel 登录Dto接收
/// 作者 xxxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
public async Task<object> Login(LoginModel model)
{
    try
    {
        if (!ModelState.IsValid)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "参数不合法!", null));
        }
        var result = await _signInManger.PasswordSignInAsync(model.UserName, model.Password, false, false);
        if (result.Succeeded)
        {
            // 成功的话获取用户
            var appUser = await _userManger.FindByNameAsync(model.UserName);
            var roles = (await _userManger.GetRolesAsync(appUser)).ToList();
            // await _userManger.GetRolesAsync(appUser);
            var user = new UserDto(appUser.FullName, appUser.Email, appUser.UserName, appUser.DateCreated, roles)
            {
                // 生成Token
                Token = GenarateToken(appUser,roles)
            };
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "登录成功", user));
        }
        else
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "登录失败", null));
        }

    }
    catch (System.Exception ex)
    {
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, ex.Message, null));
    }
}

Add role

First set the token generated by the user login above into swagger, and then access the interface that only the Admin role can access

/// <summary>
/// 添加角色
/// [Authorize(Roles ="Admin")]  只有Admin角色的用户可以访问
/// 作者 xxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[Authorize(Roles ="Admin")]
[HttpPost("AddRole")]
public async Task<object> AddRole(AddRoleModel model)
{
    try
    {
        if (model is null || string.IsNullOrWhiteSpace(model.Role))
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "角色不能为空"));
        }
        // 判断【AspNetRoles】 表里  角色是否存在
        if (await _roleManger.RoleExistsAsync(model.Role))
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "角色存在了"));
        }
        var role = new IdentityRole()
        {
            Name = model.Role,
        };
        // 创建角色
        var result = await _roleManger.CreateAsync(role);
        if (result.Succeeded)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "角色创建成功!"));
        }
        else
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "角色创建失败!"));
        }

    }
    catch (System.Exception)
    {
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error));
    }

}

What should I do if our token permission fails during verification? Here, ASP.NET Core 5.0 has added an interface [IAuthorizationMiddlewareResultsHandler] that can handle authorization verification. See the code below!

/// <summary>
/// 这个是ASP.NET Core 5 新增的授权处理失败  可以直接暴露出请求上下文 省事很多啦!!!
/// 作者 xxx
/// </summary>
public class AuthorizationHandleMiddleWare : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler authorizationHandleMiddleWare =new();
    public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        // 当 token失效或者token不存在的时候 authorizeResult.Challenged 为True
        if(authorizeResult.Challenged)
        {
            // todo 拿到上下文user对象后 此处可以check token  区分token是否是过期了
            var a=context.Request.Headers["Authorization"];
            context.Response.StatusCode=(int)HttpStatusCode.OK;
            await context.Response.WriteAsJsonAsync(new ResponseModel(Enums.ResponseCode.UnAuthorized,"您未授权,请检查Token是否有效!"));
            return ;
        }
        // 此时token 校验通过  但是访问的资源的没权限的话 authorizeResult.Forbidden 为true
        if(authorizeResult.Forbidden)
        {
            context.Response.StatusCode=(int)HttpStatusCode.OK;
            await context.Response.WriteAsJsonAsync(new ResponseModel(Enums.ResponseCode.ForBidden,"您无此权限访问哦!"));
            return ;
        }
        await authorizationHandleMiddleWare.HandleAsync(next,context,policy,authorizeResult);
    }
}

In addition, you need to register the service in ConfigureService

// .net 5 新增的权限验证中间件  在此处依赖注入一下  详见 AuthorizationHandleMiddleWare.cs 文件
services.AddSingleton<IAuthorizationMiddlewareResultHandler,AuthorizationHandleMiddleWare>();

以上就是一个登录的简单 demo,详细代码请访问码云:https://gitee.com/holyace/together/tree/JarryGu_develop/framework/JwtLoginDemo

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 6/22/2022

Localization of ASP.NET Core WebAPI (single resource file)

Microsoft's default method is that one class corresponds to multiple resource files, which is quite troublesome to use. This article introduces the use of single resource files, that is, all classes of the entire project correspond to a set of multi-language resource files.

继续阅读