前言
相信用做过登录功能的小伙伴都知道,用户登录成功后的有用信息,如:姓名、用户ID等等,无非这几种做法来保存这些信息,Session、Cookie、QueryString等等。但如今跨平台,百花齐放的时代,小程序啊、APP端啊、多端时代,最常见的就是单点登录,这明显传统上的传参方式就无法满足我们现有的需求。那么就会引用一个新的传参方式:JWT,根据维基百科的定义,JSON WEB Token(JWT),是一种基于JSON的、用于在网络上声明某种主张的令牌(token),是目前最流行的接口认证方案。对于它的介绍,我就不多说了,有兴趣的小伙伴可以可以参考我之前的文章《.net core3.1项目引用JWT保护API接口》
功能简介
大致流程图:
用户输入账号密码=》用户信息存入HttpContext,并得到一个Token数据=》用Token进行JWT身份认=》 利用IHttpContextAccessor来获取HttpContext的User属性
JWT
1、这里封装一个jwt认证的帮助类,一共有两块功能,颁发注册JWT字符串(用于登录成功返回一个)和解析JWT字符串
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 CuoDing.Core.Common
{
/// <summary>
/// jwt封装类
/// </summary>
public class JwtHelper
{
/// <summary>
/// 颁发JWT字符串
/// </summary>
/// <param name="tokenModel"></param>
/// <returns></returns>
public static string IssueJwt(TokenModelJwt tokenModel)
{
string iss = Appsettings.app(new string[] { "Jwt", "Issuer" });
string aud = Appsettings.app(new string[] { "Jwt", "Audience" });
string secret = Appsettings.app(new string[] { "Jwt", "SecurityKey" });
int expires = Appsettings.app(new string[] { "Jwt", "Expires" }).ObjToInt();
int refreshExpires = Appsettings.app(new string[] { "Jwt", "RefreshExpires" }).ObjToInt();
var timestamp = DateTime.Now.AddMinutes(expires + refreshExpires).ObjToTimestamp().ToString();
var claims = new List<Claim>
{
/*
* 特别重要:
1、这里将用户的部分信息,比如 uid 存到了Claim 中,如果你想知道如何在其他地方将这个 uid从 Token 中取出来,请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了!
2、你也可以研究下 HttpContext.User.Claims
*/
new Claim(ClaimAttributes.UserId, tokenModel.UserId),
new Claim(ClaimAttributes.CreateDept, tokenModel.CreateDept),
new Claim(ClaimAttributes.RefreshExpires, timestamp)
};
//秘钥 (SymmetricSecurityKey 对安全性的要求,密钥的长度太短会报出异常)
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken(
issuer: iss,
audience: aud,
claims: claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddMinutes(expires),
signingCredentials: creds);
var jwtHandler = new JwtSecurityTokenHandler();
var encodedJwt = jwtHandler.WriteToken(jwt);
return encodedJwt;
}
/// <summary>
/// 解析
/// </summary>
/// <param name="jwtStr"></param>
/// <returns></returns>
public static TokenModelJwt SerializeJwt(string jwtStr)
{
var jwtHandler = new JwtSecurityTokenHandler();
TokenModelJwt tokenModelJwt = new TokenModelJwt();
// token校验
if (jwtStr.IsNotEmptyOrNull() && jwtHandler.CanReadToken(jwtStr))
{
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
Claim[] claimArr = jwtToken?.Claims?.ToArray();
if (claimArr != null && claimArr.Length > 0)
{
tokenModelJwt.UserId = claimArr.FirstOrDefault(a => a.Type == ClaimAttributes.UserId)?.Value;
tokenModelJwt.RefreshExpires = claimArr.FirstOrDefault(a => a.Type == ClaimAttributes.RefreshExpires)?.Value;
tokenModelJwt.CreateDept = claimArr.FirstOrDefault(a => a.Type == ClaimAttributes.CreateDept)?.Value;
}
}
return tokenModelJwt;
}
}
/// <summary>
/// 令牌
/// </summary>
public class TokenModelJwt
{
/// <summary>
/// 用户ID
/// </summary>
public string UserId { get; set; }
/// <summary>
/// 所属部门
/// </summary>
public string CreateDept { get; set; }
/// <summary>
/// 刷新有效时间
/// </summary>
public string RefreshExpires { get; set; }
}
}
2、JWT授权认证生效,需要在我们在Startup的ConfigureServices方法中将JwtBearer的注册信息注册进来
记得添加依赖包Microsoft.AspNetCore.Authentication.JwtBearer;
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
#region 身份认证授权
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true,//保存token,后台验证token是否生效(重要)
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
ValidAudience = Appsettings.app(new string[] { "Jwt", "Audience" }).ToString(),//订阅者
ValidIssuer = Appsettings.app(new string[] { "Jwt", "Issuer" }).ToString(),//Issuer,这两项和前面签发jwt的设置一致
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Appsettings.app(new string[] { "Jwt", "SecurityKey" }).ToString()))//密钥
};
options.Events = new JwtBearerEvents()
{
OnChallenge = context =>
{
context.HandleResponse();
context.Response.Clear();
context.Response.ContentType = "application/json";
context.Response.StatusCode = 401;
//context.Response.WriteAsync(new { message = "授权未通过", status = false, code = 401 }.ToJson());
context.Response.WriteAsync(new DXResult { code = DXCode.Unauthorized, msg = "授权未通过" }.ToJson());
return Task.CompletedTask;
}
};
});
#endregion
3、注册完以上信息,还需在我们的请求管道方法Configure把授权认证配置起来,注意先后顺序。
- 中间件的注册顺序严格按照官方配置推荐依次顺序,更多请看https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0
- ExceptionHandler=>HSTS=>HttpsRedirection=>StaticFiles=>CORS=>Authentication=>Authorization=>自定义中间组件=》Endpoint
app.UseAuthentication();
app.UseAuthorization();
IHttpContextAccessor
1、首先得把IHttpContextAccessor注册进来
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
2、然后我们封装一个方法从IHttpContextAccessor 的HttpContext中获取对应的ClaimsPrincipal,如下(认证通过后,User是具有当前用户的身份标志的ClaimsPrincipal),在这里我们定义一个User类负责从HttpContext?.User把用户的身份标志信息提取出来,比如用户的Id,部门等业务数据,这些是需要在获取Token时系统所提供过的信息。
ClaimAttributes.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CuoDing.Core.Common
{
/// <summary>
/// Claim属性
/// </summary>
public static class ClaimAttributes
{
/// <summary>
/// 用户Id
/// </summary>
public const string UserId = "id";
/// <summary>
/// 认证授权用户Id
/// </summary>
public const string IdentityServerUserId = "sub";
/// <summary>
/// 用户名
/// </summary>
public const string UserName = "na";
/// <summary>
/// 姓名
/// </summary>
public const string UserNickName = "nn";
/// <summary>
/// 刷新有效期
/// </summary>
public const string RefreshExpires = "re";
/// <summary>
/// 创建部门
/// </summary>
public const string CreateDept = "dept";
}
}
User.cs
using Microsoft.AspNetCore.Http;
namespace CuoDing.Core.Common
{
/// <summary>
/// 用户信息
/// </summary>
public class User : IUser
{
private readonly IHttpContextAccessor _accessor;
public User(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
/// <summary>
/// 用户Id
/// </summary>
public virtual string Id
{
get
{
var id = _accessor?.HttpContext?.User?.FindFirst(ClaimAttributes.UserId);
if (id != null && id.Value.IsNotEmptyOrNull())
{
return id.Value;
}
return "";
}
}
/// <summary>
/// 用户名
/// </summary>
public string Name
{
get
{
var name = _accessor?.HttpContext?.User?.FindFirst(ClaimAttributes.UserName);
if (name != null && name.Value.IsNotEmptyOrNull())
{
return name.Value;
}
return "";
}
}
/// <summary>
/// 创建部门
/// </summary>
public string CreateDept
{
get
{
var name = _accessor?.HttpContext?.User?.FindFirst(ClaimAttributes.CreateDept);
if (name != null && name.Value.IsNotEmptyOrNull())
{
return name.Value;
}
return "";
}
}
}
}
IUser.cs
namespace CuoDing.Core.Common
{
/// <summary>
/// 用户信息接口
/// </summary>
public interface IUser
{
/// <summary>
/// 用户Id
/// </summary>
string Id { get; }
/// <summary>
/// 用户名
/// </summary>
string Name { get; }
/// <summary>
/// 创建部门
/// </summary>
string CreateDept { get; }
}
}
3、一样的也要把这个类注册进来
services.AddSingleton<IUser, User>();
简单的登录接口
简单写了一个关于登录的控制器,包含登录获取token、token以旧换新、获取封装httpcontent的Claim属性的用户数据
LoginController.cs
using CuoDing.Core.BLL;
using CuoDing.Core.Common;
using CuoDing.Core.Model.Enum;
using CuoDing.Core.Model.Model;
using CuoDing.Core.Model.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DX.StdTrainning.WebApi.Controllers
{
/// <summary>
/// 登录管理【无权限】
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
[AllowAnonymous]
public class LoginController : ControllerBase
{
private readonly IUser _user;
private readonly ISystemUserBLL _systemUserBLL;
public LoginController(IUser user, ISystemUserBLL systemUserBLL)
{
_user = user;
_systemUserBLL = systemUserBLL;
}
/// <summary>
/// 登录获取token
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
[HttpPost]
public async Task<dynamic> GetToken(SystemUser param)
{
string jwtStr = string.Empty;
var obj = await _systemUserBLL.LoginToken(param);
if (obj != null&& obj.code==DXCode.Success)
{
var user = obj.data as SystemUser;
TokenModelJwt tokenModel = new TokenModelJwt { UserId = user.Id.ToString(), CreateDept ="测试部门" };
jwtStr = JwtHelper.IssueJwt(tokenModel);
obj.data = jwtStr;
}
return obj;
}
/// <summary>
/// 刷新Token(以旧换新)
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet]
public async Task<dynamic> RefreshToken(string token)
{
DXResult dXResult = new DXResult { code = DXCode.Success ,msg="刷新成功"};
string jwtStr = string.Empty;
var userInfo = JwtHelper.SerializeJwt(token);
if (userInfo == null)
{
return new DXResult { code = DXCode.Failure, msg = "无效token" };
}
var refreshExpires = userInfo?.RefreshExpires;
if (!refreshExpires.IsNotEmptyOrNull())
{
return new DXResult { code = DXCode.Failure, msg = "无效刷新时间" };
}
if (refreshExpires.ObjToLong() <= DateTime.Now.ObjToTimestamp())
{
return new DXResult { code = DXCode.Failure, msg = "登录信息已过期" };
}
var userId = userInfo?.UserId;
if (!userId.IsNotEmptyOrNull())
{
return new DXResult { code = DXCode.Failure, msg = "用户信息为空" };
}
var dXResultUser = await _systemUserBLL.TokenGetById(userId.ObjToLong());
if (dXResultUser.code != DXCode.Success)
{
return new DXResult { code = DXCode.Failure, msg = "获取用户信息失败" };
}
var userObj = dXResultUser.data as SystemUser;
TokenModelJwt tokenModel = new TokenModelJwt { UserId = userObj.Id.ToString(), CreateDept = "测试刷新部门" };
jwtStr = JwtHelper.IssueJwt(tokenModel);
dXResult.data = jwtStr;
return dXResult;
}
/// <summary>
/// 测试获取登录用户信息
/// </summary>
/// <returns></returns>
[HttpGet]
[Authorize]
public dynamic GetUserTest()
{
DXResult dXResult = new DXResult { code = DXCode.Success };
var userObj = new
{
id=_user.Id,
dept=_user.CreateDept
};
dXResult.data = userObj;
return dXResult;
}
}
}
这里需要注意一点,关于是否开启权限认证,需要用 [AllowAnonymous]跟[Authorize]来区别
测试
1、运行项目,调用api/Login/GetToken
2、先去www.jwt.io验证下我们这个token数据是否正常
3、调用api/Login/GetUserTest,获取封装httpcontent的Claim属性的用户数据
成功