第二节:基于Token和Session的混合鉴权方案
一. 复习
1 Cookie、Session、Token的区别?

2 登录中,Token和Session作用是什么?
Token:主要用来鉴权的,里面可以存放一些非敏感信息,比如用户昵称、账号等
Session:存放敏感信息,比如权限,或者一些实时性要求较高的数据。
3 Token是否需要存放到Redis中呢?
像一些简单的身份认证,服务端通过代码直接验证token的签名和有效期,这也就是常说的token是无状态的,这种情况下是不需要进行存储的。
需要存放在Redis的场景:
A 主动使Token生效,比如注销登录、修改密码。
B Token续签:token快过期的时候,服务端端会生成一个新的token返回给客户端,同时更新redis的存储
4 全部使用Token存放信息是否可以?
在一些极简场景是可以的,比如:非敏感且不频繁变更的信息(用户基础信息:用户名、用户账号等等)
很多场景是不可替代的:
A 存在安全性问题:jwt的payLoad只是base64编码,非加密,敏感信息不能放到里面(财务信息)
B 无法及时更新:比如权限变更,jwt只能等着过期后才失效
C 负载过大:大量业务数据放到payLoad中,导致jwt体积膨胀,增加网络传输成本。
所以:通常是混合使用,建议JWT(身份认证)+Sesson(权限和状态存储) 共同使用。
5 Session ID如何生成,SessionID存放在哪里?
SessionID 就是 Guid.NewGuid().ToString() 即可。
可以通过服务代码直接写到浏览器Cookie 或者 返回客户端让其自己存放。
6 能否使用token作为Session ID ?
不建议这么使用,因为token存在有效期问题,token还可能存在刷新机制
7 Token和Session的混合使用方案

8 如何实现不相关的网站A登录后,B也能自动登录?
【详见77】
9 用户登录信息保存在A服务器上,B服务器如何共享这个Session呢?
【详见59】
10 JWT的组成?

11 其他---临时存放
SessionId 可以直接通过Server接口写到浏览器Cookie中,也可以返回给客户端,让其自己存储。
Session中存放用户的各种权限,当权限修改的时候,可以直接在redis中进行修改,实时生效。后续在过滤器中直接去Redis中获取权限,校验即可,不在需要频繁的查询DB了。
Token 前端携带。 在网关拦截器中校验, 放在string中,key为token,value为userId。
二. 方案实操
1 目标
实现token和Session的混合鉴权
(1) token 用来快速鉴权(准确性、是否过期)
(2) session 用来存放用户权限,用来校验用户是否具备访问接口的权限
2 核心步骤
(1) 登录成功,生成token;然后生成sessionId,作为key,value为该用户的权限信息,作为value,存放到redis中。
/// <summary>
/// 校验登录
/// </summary>
/// <param name="userAccount">账号</param>
/// <param name="pwd">密码</param>
/// <returns></returns>
[HttpPost]
public IActionResult CheckLogin(string userAccount, string pwd)
{
try
{
//一.账号密码正确(模拟DB)
if (userAccount == "admin" && pwd == "123456")
{
//1. 查询一些信息(DB查询)
string userId = "10010";
List<string> permissonList = ["/system/user/search", "/system/user/add", "/system/user/del",]; //此处要结合已有的案例,处理一下权限的问题
//2. 生成Token
double exp = (DateTime.UtcNow.AddHours(12) - new DateTime(1970, 1, 1)).TotalSeconds; //12个小时过期
var payload = new Dictionary<string, object> { { "userId", userId }, { "userAccount", userAccount }, { "exp", exp } };
var token = JWTHelp.JWTJiaM(payload, _configuration["JWTSecret"]);
//3. 生成Session
var sessionId = "sessionId_" + Guid.NewGuid().ToString("N");
var sessionData = new SessionData()
{
perList = permissonList,
otherData = "我是敏感数据",
};
RedisHelper.Set(sessionId, sessionData);
return Json(new { status = "ok", msg = "登录成功", data = new { token, sessionId } });
}
//二. 密码错误后的业务
else
{
//比如可以执行多次输错后的锁定业务
return Json(new { status = "error", msg = $"账号或密码错误" });
}
}
catch (Exception)
{
return Json(new { status = "error", msg = $"登录失败" });
}
}
(2) 将token 和 sessionId 返回给客户端,客户端存放在localstorage中
$('#loginButton').click(function() {
$.ajax({
url: '/Demo3/CheckLogin', // 假设登录接口路径
type: 'POST',
data: { userAccount: "admin", pwd: "123456" },
success: function(response) {
const { status, msg, data } = response;
if (status == "ok") {
window.localStorage.setItem('token', data.token);
window.localStorage.setItem('sessionId', data.sessionId);
alert(msg)
} else {
alert(msg)
}
},
error: function(error) {
console.log(error)
if (error.status === 401) {
alert('未授权,请检查用户名和密码');
} else {
alert('登录失败,请稍后重试');
}
}
});
});
(3) 后续ajax请求需要在header中携带token 和 sessionId

(4) 在过滤器中对token 和 session进行校验
A. token就是常规校验
B. 根据sessionId去redis中取出权限集合 perList
C. 判单该用户是否具有当前接口的权限
重点:当前接口的权限名称是什么? 在过滤器调用的时候以参数的形式传递进去!!!
[TypeFilter(typeof(CheckJwt_Per), Arguments = ["/system/user/search"])]
过滤器代码
查看代码
/// <summary>
/// 校验JWT 和 是否具有访问接口的权限
/// </summary>
public class CheckJwt_Per : ActionFilterAttribute
{
private readonly string nowPerStr;
private readonly IConfiguration configuration;
/// <summary>
/// 过滤器调用的时候,需要即传入参数,还需要保证注入的生效
/// </summary>
/// <param name="nowPerStr"></param>
/// <param name="configuration"></param>
public CheckJwt_Per(string nowPerStr, IConfiguration configuration)
{
this.nowPerStr = nowPerStr;
this.configuration = configuration;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
// 1 获取Token 和 SessionId
var token = context.HttpContext.Request.Headers["auth"].ToString();
var sessionId = context.HttpContext.Request.Headers["sessionId"].ToString();
//2 token校验
if (token == "null" || string.IsNullOrEmpty(token))
{
context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,token参数为空" };
return;
}
//校验token的正确性
var result = JWTHelp.JWTJieM(token, configuration["JWTSecret"]);
if (result == "expired")
{
context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,参数已经过期" };
return;
}
else if (result == "invalid")
{
context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,未通过校验1" };
return;
}
else if (result == "error")
{
context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,未通过校验2" };
return;
}
else
{
//表示校验通过,用于向控制器中传值
context.RouteData.Values.Add("auth", result);
}
//3 利用Session进行权限校验
if (sessionId == "null" || string.IsNullOrEmpty(sessionId))
{
context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,SessionId参数为空" };
return;
}
//进行权限校验
var dataStr = RedisHelper.Get(sessionId);
if (string.IsNullOrEmpty(dataStr))
{
context.Result = new ContentResult() { StatusCode = 401, Content = "没有权限1" };
return;
}
SessionData data = JsonSerializer.Deserialize<SessionData>(dataStr) ;
List<string> perList = data.perList;
if (!perList.Contains(nowPerStr))
{
context.Result = new ContentResult() { StatusCode = 401, Content = "没有权限2" };
return;
}
else
{
//表示校验通过,用于向控制器中传值
context.RouteData.Values.Add("perList", perList);
}
base.OnActionExecuting(context);
}
}
action代码
/// <summary>
/// 查询信息--测试权限
/// </summary>
/// <returns></returns>
[HttpPost]
[TypeFilter(typeof(CheckJwt_Per), Arguments = ["/system/user/search"])]
public IActionResult Search()
{
try
{
return Json(new { status = "ok", msg = "获取成功", data = new List<string> { "张三", "李四", "王五" } });
}
catch (Exception)
{
return Json(new { status = "error", msg = "获取失败" });
}
}
/// <summary>
/// 更新信息--测试权限
/// </summary>
/// <returns></returns>
[HttpPost]
[TypeFilter(typeof(CheckJwt_Per), Arguments = ["/system/user/update"])]
public IActionResult Update()
{
try
{
return Json(new { status = "ok", msg = "更新成功" });
}
catch (Exception)
{
return Json(new { status = "error", msg = "更新失败" });
}
}
3 测试
(1) 先调用登录接口
(2) 调用查询接口,正常访问
(3) 调用update接口,没有权限
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。

浙公网安备 33010602011771号