不使用cookie和session如何进行认证?
  楠木大叔   10/15/22 8:52:18 AM
登录认证的方式很多,不同的场景有不同的方式,没有哪个最通用的方案。 JWT一种相对比较流行且轻量的实现方案,目前在前后端分离项目中使用比较多。

导航

  • 常见的登录实现方式
  • 什么是JWT
  • JWT的组成
    • Header
    • Payload
    • Signature
  • 认证流程
  • .NET Core 实现jwt登录认证
    • 集成步骤
    • 测试
  • 前端如何调用带jwt的web api
    • 前端调jwt的web api
    • 解决跨域访问问题
  • 结语
  • 参考

常见的登录实现方式

  • cookie

1)用户登录认证之后,将用户信息保存在本地浏览器中cookie(还可以设置过期时间),

2)后面每次发起http请求,都自动携带上该信息,就能达到认证用户,保持用户在线。

  • cookie+session

1)用户登录成功之后,在服务端就会生成一个键值对。key叫做sessionid,value就保存ssession(用户信息),客户端那边就需要把sessionid存储到cookie。

2)后续的http请求会携带上sessionid,服务器就根据sessionid来查找对应的信息。

  • token

1)用户登录成功之后,token 由服务器本身根据算法生成后下发给客户端,服务器端无需额外存储。

2)客户端请求服务器时,在请求头中追加携带该token

3)服务器端对token进行验签,从而决定本次访问是拒绝还是放行。

以上是我们常见的登录认证方式,但是在前后端分离的项目中,如今比较流行的还是JWT。

什么是JWT

JWT(json web token)基于开放标准(RFC 7519),是一种无状态的分布式的身份验证方式,主要用于在网络应用环境间安全地传递声明。它是基于JSON的,所以它也像json一样可以在.Net、JAVA、JavaScript,、PHP等多种语言使用。

为什么要使用JWT?

传统的Web应用一般采用Cookies+Session来进行认证。但对于目前越来越多的App、小程序等应用来说,它们对应的服务端一般都是RestFul 类型的无状态的API,再采用这样的的认证方式就不是很方便了。而JWT这种无状态的分布式的身份验证方式恰好符合这样的需求。

JWT的组成:

它是由三段“乱码”字符串通过两个“.”连接在一起组成。官网https://jwt.io/提供了它的验证方式

它的三个字符串分别对应了Header、Payload和Signature三部分

Header
{
"alg""HS256"
"typ""JWT"
}

标识加密方式为HS256,Token类型为JWT, 这段JSON通过Base64Url编码形成上例的第一个字符串

Payload

Payload是JWT用于信息存储部分,其中包含了许多种的声明(claims)。
可以自定义多个声明添加到Payload中,系统也提供了一些默认的类型

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

这部分通过Base64Url编码生成第二个字符串。

Signature

Signature是用于Token的验证。它的值类似这样的表达式:Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret),也就是说,它是通过将前两个字符串加密后生成的一个新字符串。

所以只有拥有同样加密密钥的人,才能通过前两个字符串获得同样的字符串,通过这种方式保证了Token的真实性。

认证流程


图片来源于网络(图片来源于网络)">

  • 认证服务器:用于用户的登录验证和Token的发放。
    -应用服务器:业务数据接口。被保护的API。
    -客户端:一般为APP、小程序等。

.NET Core 实现jwt登录认证

  1. 安装Microsoft.AspNetCore.Authentication.JwtBearer包


  1. 增加用于验证的实体
public class TokenManagement
    {
        [JsonProperty("secret")]
        public string Secret { getset; }
        [JsonProperty("issuer")]
        public string Issuer { getset; }
        [JsonProperty("audience")]
        public string Audience { getset; }
        [JsonProperty("accessExpiration")]
        public int AccessExpiration { getset; }
        [JsonProperty("refreshExpiration")]
        public int RefreshExpiration { getset; }
    }

  1. appsettings.json文件中增加jwt配置
"tokenManagement": {
    "secret""*#@^%$!(zhike-secret-key)^~",
    "issuer""zhike.business.Api",
    "audience""zhike.business.Api",
    "accessExpiration"30,
    "refreshExpiration"60
  }
  
  1. StartUp类中注册Authentication
services.Configure<TokenManagement>(Configuration.GetSection("tokenManagement"));
var token = Configuration.GetSection("tokenManagement").Get<TokenManagement>();

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.ASCII.GetBytes(token.Secret)),
        ValidIssuer = token.Issuer,
        ValidAudience = token.Audience,
        ValidateIssuer = false,
        ValidateAudience = false
    };
});

  1. 注册中间件
app.UseAuthentication();

  1. 新建BookController
[Route("api/[controller]")]
    [ApiController]
    public class BookController : Controller
    {
        // GET: api/<controller>
        [HttpGet]
        [AllowAnonymous]
        public IEnumerable<stringGet()
        {
            return new string[] { "ASP""C#" };
        }

        // POST api/<controller>
        [HttpPost]
        [Authorize]
        public JsonResult Post()
        {
            return new JsonResult("Create Book ...");
        }
    }

  1. 增加接口
 public interface IAuthenticateService:IService
    {
        bool IsAuthenticated(LoginRequestDTO request, out string token);
    }

  1. 实现接口
public class AuthenticateService : IAuthenticateService
    {
        private readonly TokenManagement _tokenManagement;

        public AuthenticateService(IOptions<TokenManagement> tokenManagement)
        {
            this._tokenManagement = tokenManagement.Value;
        }
        public bool IsAuthenticated(LoginRequestDTO request, out string token)
        {
            //TODO:验证账户密码逻辑 自行补全
            token = string.Empty;
            var claims = new[]
            {
            new Claim(ClaimTypes.Name,request.UserName),
            new Claim(ClaimTypes.Name,request.Password)
        };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenManagement.Secret));
            SigningCredentials credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwtToken = new JwtSecurityToken(_tokenManagement.Issuer, _tokenManagement.Audience, claims, expires: DateTime.Now.AddMinutes(_tokenManagement.AccessExpiration), signingCredentials: credentials);
            token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
            return true;
        }
    }

9.在Controller中增加Login

[Produces("application/json")]
    [Route("v1/account")]
    public class AccountController : BaseApiController
    {
        private readonly IAuthenticateService _authService;

        public AccountController(
            IAuthenticateService authService
)

        {
            _authService = authService;
        }

        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        [HttpPost("login")]
        [ProducesResponseType(typeof(ResponseInfo<LoginVM>), 200)]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody]LoginRequestDTO req)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest("Invalid Request");
            }
            string sToken;
            if (_authService.IsAuthenticated(req, out sToken))
            {
                return Ok(sToken);
            }
            return BadRequest("Invalid Request");

        }
    }

测试

通过swaggerpostman可以模拟登录

启动jwt.demo.passport站点,在浏览器输入:http://localhost:5000/swagger/index.html



入参:


{
  "userName""jeffreyHu",
  "password""123456"
}

返回结果

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjpbImplZmZyZXlIdSIsIjEyMzQ1NiJdLCJleHAiOjE1OTE1MjU0MTYsImlzcyI6IkhTVHJhZGUuQXBwbGV0TWFuYWdlLkFwaSIsImF1ZCI6IkhTVHJhZGUuQXBwbGV0TWFuYWdlLkFwaSJ9.6r0zaoWCpLlwOU-zgXQJZ2fuFj9dqzi9wSZsAo986e0"

此时,我们可以拷贝该字符串到jwt官网验证是否解码



以上基本实现 .NET CORE登录生成jwt-token功能。

前端如何调用带jwt的web api

前后端分离模式,很早之前笔者有过实践(.NET3.0时代 )。最近几年炒的很火,jwt的应用使得前后端分离更加便捷。



前端调jwt的web api

基本思路是调用登录接口,获取token,使用token请求其他JWT接口:

这里以angular为例,如下

getHomeDetails(): Observable<HomeDetails> {
    let headers = new Headers();
    headers.append('Content-Type''application/json');
    let authToken = localStorage.getItem('auth_token');
    headers.append('Authorization'`Bearer ${authToken}`);
  
    return this.http.get(this.baseUrl + "/dashboard/home",{headers})
      .map(response => response.json())
      .catch(this.handleError);
}

解决跨域访问问题

  1. nuget安装 Microsoft.AspNetCore.Cors 中间件。
  2. 在Startup类里先定义一个全局变量。
private readonly string AllowSpecificOrigin = "AllowSpecificOrigin";

3.在Startup的ConfigureServices中添加以下代码来配置跨域处理。

#region 跨域
services.AddCors(options =>
{
    options.AddPolicy(AllowSpecificOrigin,
        builder =>
        {
            builder.AllowAnyMethod()
                .AllowAnyOrigin()
                .AllowAnyHeader();
        });
});
#endregion

4.在Startup的Configure中添加以下代码来配置跨域处理。

app.UseRouting();
//CORS 中间件必须配置为在对 UseRouting UseEndpoints的调用之间执行。 配置不正确将导致中间件停止正常运行。
app.UseCors(AllowSpecificOrigin);
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

结语

登录认证的方式很多,不同的场景有不同的方式,没有哪个最通用的方案。
JWT一种相对比较流行且轻量的实现方案,目前在前后端分离项目中使用比较多。

参考

版权声明: 本文为智客工坊「楠木大叔」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。