第二节:基于Token和Session的混合鉴权方案

一.  复习

1 Cookie、Session、Token的区别?

登录中,Token和Session作用是什么?

 Token:主要用来鉴权的,里面可以存放一些非敏感信息,比如用户昵称、账号等

 Session:存放敏感信息,比如权限,或者一些实时性要求较高的数据。

 

Token是否需要存放到Redis中呢?

 像一些简单的身份认证,服务端通过代码直接验证token的签名和有效期,这也就是常说的token是无状态的,这种情况下是不需要进行存储的。

 需要存放在Redis的场景:

  A 主动使Token生效,比如注销登录、修改密码。

  B Token续签:token快过期的时候,服务端端会生成一个新的token返回给客户端,同时更新redis的存储

 

全部使用Token存放信息是否可以?

 在一些极简场景是可以的,比如:非敏感且不频繁变更的信息(用户基础信息:用户名、用户账号等等)

 很多场景是不可替代的:

   A 存在安全性问题:jwt的payLoad只是base64编码,非加密,敏感信息不能放到里面(财务信息)

   B 无法及时更新:比如权限变更,jwt只能等着过期后才失效

   C 负载过大:大量业务数据放到payLoad中,导致jwt体积膨胀,增加网络传输成本。

所以:通常是混合使用,建议JWT(身份认证)+Sesson(权限和状态存储) 共同使用。

 

Session ID如何生成,SessionID存放在哪里?

SessionID 就是  Guid.NewGuid().ToString() 即可。

可以通过服务代码直接写到浏览器Cookie 或者 返回客户端让其自己存放

 

能否使用token作为Session ID ?

不建议这么使用,因为token存在有效期问题,token还可能存在刷新机制

 

Token和Session的混合使用方案

如何实现不相关的网站A登录后,B也能自动登录?

【详见77】

用户登录信息保存在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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2025-04-29 07:37  Yaopengfei  阅读(85)  评论(1)    收藏  举报