手把手教你 .NET Core 3.1 JWT身份认证

概述

什么是JWT?
JWT:Json Web Token,是基于Json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,他的两大使用场景是:认证和数据交换使用起来就是,由服务端根据规范生成一个令牌(token),并且发放给客户端。此时客户端请求服务端的时候就可以携带者令牌,以令牌来证明自己的身份信息。作用:类似session保持登录状态 的办法,通过token来代表用户身份。

先来看一下它的流程图:

流程描述:
1、用户使用账号、密码登录应用,登录的请求发送到Authentication Server。
2、Authentication Server进行用户验证,然后创建JWT字符串返回给客户端。
3、客户端请求接口时,在请求头带上JWT。
4、Application Server验证JWT合法性,如果合法则继续调用应用接口返回结果。

JWT 的数据结构

JWT主要由三部分组成,如下:HEADER.PAYLOAD.SIGNATURE

完整的Token,如下所示:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJleHAiOjE1Nzg2NDU1MzYsImlzcyI6IndlYmFwaS5jbiIsImF1ZCI6IldlYkFwaSJ9.2_akEH40LR2QWekgjm8Tt3lesSbKtDethmJMo_3jpF4

HEADER 头部

上面示例中 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. 即为 HEADER , 它包含token的元数据,主要是加密算法,和签名的类型,原始信息为 {"alg":"HS256","typ":"JWT"} ,通过 BASE64编码后成为 token 的第一部分。

Payload 消息体(载荷)

payload 信息存放的是Claims声明信息。载荷其实就是自定义的数据,一般存储用户Id,过期时间等信息。也就是JWT的核心所在,因为这些数据就是使后端知道此token是哪个用户已经登录的凭证。而且这些数据是存在token里面的,由前端携带,所以后端几乎不需要保存任何数据。

Signature 签名

JWT第三部分是签名。是这样生成的,首先需要指定一个secret,该secret仅仅保存在服务器中,保证不能让其他用户知道。然后使用Header指定的算法对Header和Payload进行计算,然后就得出一个签名哈希。也就是Signature。那么Application Server如何进行验证呢?可以利用JWT前两段,用同一套哈希算法和同一个secret计算一个签名值,然后把计算出来的签名值和收到的JWT第三段比较,如果相同则认证通过。

代码示例

新建WebAPI项目

这里新建的是.NET Core 3.1的 WebAPI项目,如下图所示,完成项目的创建后,通过Nuget添加 Microsoft.AspNetCore.Authentication.JwtBearer 的包,因为项目是 .NET Core 3.1的项目,这里的Nuget包选 3.1.24。

添加JWT配置

在appsetting.json 添加 JWT 的配置,如下图所示:

  "JWTConfig": {
    "Secret": "abcdefghijklmnop",
    "Issuer": "cnblogs.com",
    "Audience": "WebApi",
    "AccessExpiration": 30,
    "RefreshExpiration": 60
  }

这里需要注意的是 Secret 必须是16位以上的字符,否则在认证的时候会报错。

1、在项目下新建目录 Model ,然后创建类 JWTConfig ,代码如下:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace JwtTest.WebApi
{
    public class JWTConfig
    {
        /// <summary>
        /// 密钥 长度不少于16位
        /// </summary>
        [JsonProperty("Secret")]
        public string Secret { get; set; }

        /// <summary>
        /// 发行人
        /// </summary>
        [JsonProperty("Issuer")]
        public string Issuer { get; set; }

        /// <summary>
        /// 观众
        /// </summary>
        [JsonProperty("Audience")]
        public string Audience { get; set; }

        /// <summary>
        /// 访问过期
        /// </summary>
        [JsonProperty("AccessExpiration")]
        public int AccessExpiration { get; set; }

        /// <summary>
        /// 刷新过期
        /// </summary>
        [JsonProperty("RefreshExpiration")]
        public int RefreshExpiration { get; set; }
    }
}


2、在 Startup 的 ConfigureServices 方法添加 读取配置,以及认证配置,添加后,代码会提示引用其他的一些命名空间,这里都按提示的引用即可。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));

            var tokenConfigs = Configuration.GetSection("JWTConfig").Get<JWTConfig>();

            //Authentication
            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(x => {
                x.RequireHttpsMetadata = false;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenConfigs.Secret)),
                    ValidIssuer = tokenConfigs.Issuer,
                    ValidAudience = tokenConfigs.Audience,
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
            });
        }

接下来,还需要在 Configure 方法中,添加启用认证。如下图:


添加 Model 对象

在Model 目录添加的两个对象,主要是用于认证的信息对象(LoginModel)以及用户实体(UserModel)(例子为了演示便利,没有连接数据库),代码如下:

    /// <summary>
    /// 认证信息对象
    /// </summary>
    public class LoginModel
    {
        /// <summary>
        /// 用户名
        /// </summary>
        [Required]
        [JsonProperty("username")]
        public string UserName { get; set; }

        /// <summary>
        /// 密码
        /// </summary>
        [Required]
        [JsonProperty("password")]
        public string Password { get; set; }
    }


    /// <summary>
    /// 用户对象
    /// </summary>
    public class UserModel
    {
        /// <summary>
        /// 用户Id
        /// </summary>
        public string UserId { get; set; }

        /// <summary>
        /// 用户名
        /// </summary>
        public string UserName { get; set; }

        /// <summary>
        /// 密码
        /// </summary>
        public string Password { get; set; }

        /// <summary>
        /// 地址
        /// </summary>
        public string Address { get; set; }

        /// <summary>
        /// 手机号
        /// </summary>
        public string MobilePhone { get; set; }

        /// <summary>
        /// 邮箱
        /// </summary>
        public string Email { get; set; }

        /// <summary>
        /// 性别
        /// </summary>
        public string Gender { get; set; }

        /// <summary>
        /// QQ号码
        /// </summary>
        public string QQ { get; set; }
    }


添加服务类和接口

1、在目录下新建service目录,然后新建 接口 IJWTService 以及实现类 JWTService,用于JWT的认证处理,代码如下:

    /// <summary>
    /// 认证处理接口
    /// </summary>
    public interface IJWTService
    {
        bool IsAuthenticated(LoginModel model, out string token);
    }


    /// <summary>
    /// 认证处理服务
    /// </summary>
    public class JWTService : IJWTService
    {
        public bool IsAuthenticated(LoginModel model, out string token)
        {
            throw new NotImplementedException();
        }
    }

2、在目录下新建service目录,然后新建 接口 IUserService 以及实现类 UserService,用于用户数据的验证处理,代码如下:

    /// <summary>
    /// 用户数据服务接口
    /// </summary>
    public interface IUserService
    {
        /// <summary>
        /// 根据用户名,获取用户信息
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        UserModel GetUserByUserName(string username);
    }


    /// <summary>
    /// 用户数据处理服务
    /// </summary>
    public class UserService : IUserService
    {
        public UserModel GetUserByUserName(string username)
        {
            throw new NotImplementedException();
        }
    }


3、首先,我们先来实现用户数据处理的服务的方法,这里为了简化例子,并没有连接数据库,只是模拟了数据的查询。完整代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace JwtTest.WebApi
{
    public class UserService : IUserService
    {
        private readonly List<UserModel> _userList;

        public UserService()
        {
            _userList = new List<UserModel>
            {
                new UserModel { UserId = "123", UserName = "Jack", Address = "北京", Email = "jack@mail.com", Gender = "男", MobilePhone = "13800138000", Password = "123456", QQ = "83849484", },
                new UserModel { UserId = "456", UserName = "Lily", Address = "上海", Email = "lily@mail.com", Gender = "女", MobilePhone = "13800138001", Password = "654321", QQ = "23423456", },
                new UserModel { UserId = "123", UserName = "Tom", Address = "深圳", Email = "tom@mail.com", Gender = "男", MobilePhone = "13800138003", Password = "abcdef", QQ = "54387678", },
            };
        }

        /// <summary>
        /// 检查用户名和密码
        /// </summary>
        /// <param name="username"></param>
        /// <param name="pasword"></param>
        /// <returns></returns>
        public UserModel GetUserByUserName(string username)
        {
            var user = _userList.Where(o => o.UserName == username).FirstOrDefault();
            return user;
        }
    }
}



4、接下来便是实现JWT的认证处理的服务了,想要使用用户数据处理服务和JWT配置信息,我们就需要通过构造函数注入。然后才能在 IsAuthenticated 方法里面引用相应的服务或配置信息。完整代码如下:

using Microsoft.Extensions.Options;
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 JwtTest.WebApi
{
    /// <summary>
    /// 认证处理服务
    /// </summary>
    public class JWTService : IJWTService
    {
        private readonly IUserService _userService;
        private readonly JWTConfig _jwtConfig;

        public JWTService(IUserService userService, IOptions<JWTConfig> jwtConfig)
        {
            this._userService = userService;
            this._jwtConfig = jwtConfig.Value;
        }

        /// <summary>
        /// 认证用户,生成Token
        /// </summary>
        /// <param name="model"></param>
        /// <param name="token"></param>
        /// <returns></returns>
        public bool IsAuthenticated(LoginModel model, out string token)
        {
            token = string.Empty;

            var user = _userService.GetUserByUserName(model.UserName);
            //用户不存在
            if (user == null)
            {
                return false;
            }

            //密码不正确
            if (user.Password != model.Password)
            {
                return false;
            }

            //把有需要的信息写到Token
            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.UserId),
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(ClaimTypes.Email, user.Email),
                new Claim(ClaimTypes.Gender, user.Gender),
                new Claim(ClaimTypes.MobilePhone, user.MobilePhone),
                new Claim(ClaimTypes.StreetAddress, user.Address),
                new Claim("QQ", user.QQ),  //自定义
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Secret));
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwtToken = new JwtSecurityToken(_jwtConfig.Issuer,
                _jwtConfig.Audience,
                claims,
                expires: DateTime.Now.AddMinutes(_jwtConfig.AccessExpiration),
                signingCredentials: credentials);

            token = new JwtSecurityTokenHandler().WriteToken(jwtToken);

            return true;
        }
    }
}

完成了上面的服务类之后,我们还需要在Startup类的 ConfigureServices 方法中,完成注册到容器中。这样具体的构造函数才能完成注入。如下所示:

添加控制器,实现认证功能

BaseController
经过了上面的准备,我们可以着手实现认证的功能了,我们先添加 BaseController 控制器,并添加 [Authorize] 的特性,如下所示:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace JwtTest.WebApi.Controllers
{
    [Authorize]
    public class BaseController : Controller
    {
    }
}

使其作为基础控制器,其他的控制器如果希望通过认证才能使用,只需要继承这个控制器就可以了。

AuthenticationController
新增 AuthenticationController 控制器,使其继承 BaseController 控制器,添加构造函数,实现服务的注入,新增 Login 接口,使用 LoginModel 作为参数,在此接口中实现用户的认证,并返回 Token,完整代码如下:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace JwtTest.WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthenticationController : BaseController
    {
        private readonly IJWTService _authenticateService;
        public AuthenticationController(IJWTService authenticateService)
        {
            this._authenticateService = authenticateService;
        }


        /// <summary>
        /// 用户认证
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [AllowAnonymous]
        [HttpPost, Route("Login")]
        public ActionResult Login([FromBody] LoginModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest("非法请求。");
            }

            string token;
            if (_authenticateService.IsAuthenticated(model, out token))
            {
                return Ok(token);
            }

            return BadRequest("认证失败。");
        }
    }
}

WeatherForecastController
修改 WeatherForecastController 控制器,使其继承 BaseController , 这样便需要认证才能访问这个控制器下的接口了。这里,为了让大家进一步了解 JWT,我添加了个 UserInfo 接口,可以从这个接口,获取到用户的基本信息,完整代码如下所示:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace JwtTest.WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : BaseController
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }

        /// <summary>
        /// 获取用户名
        /// </summary>
        /// <returns></returns>
        [HttpGet, Route("UserInfo")]
        public IActionResult UserInfo()
        {
            //获取用户信息
            var claimsPrincipal = Response.HttpContext.User;
            var user = new UserModel
            {
                UserId = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.NameIdentifier).FirstOrDefault().Value,
                UserName = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.Name).FirstOrDefault().Value,
                Email = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.Email).FirstOrDefault().Value,
                Gender = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.Gender).FirstOrDefault().Value,
                MobilePhone = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.MobilePhone).FirstOrDefault().Value,
                Address = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.StreetAddress).FirstOrDefault().Value,
                QQ = claimsPrincipal.Claims.Where(o => o.Type == "QQ").FirstOrDefault().Value,
            };

            return Ok(user);
        }
    }
}

至此,代码部分已经完毕,大家可以通过 Postman 测试,先请求 /api/Authentication/login 接口,拿到 token 后,再附加到 Authorization 标签页面的 Token 输入框,记得 Type 需要选项 Bearer Token 。

整个示例项目的结构,如下图所示:


源代码可在 https://github.com/jlonghe/JwtWebAPI 获取。

posted @ 2022-04-27 20:26  鹅城小将  阅读(2180)  评论(2编辑  收藏  举报