当前位置: 首页 > news >正文

5_springboot_shiro_jwt_多端认证鉴权_禁用Cookie

1. Cookie是什么

​ Cookie是一种在客户端(通常是用户的Web浏览器)和服务器之间进行状态管理的技术。当用户访问Web服务器时,服务器可以向用户的浏览器发送一个名为Cookie的小数据块。浏览器会将这个Cookie存储在客户端,为这个Cookie设置了时间后,这个Cookie就会存储到客户端端的磁盘上。如果没有设置时间,它就会存放在浏览器所开启的进程内存中。在随后对该服务器的请求中,每次都会自动附带上这个Cookie。

​ Cookie通常包含一些基本信息,如唯一标识符、用户偏好设置、会话状态等。服务器通过解析这些Cookie来识别用户的身份、维持会话状态、个性化用户体验,甚至追踪用户行为。

每个Cookie都有特定的属性,如名称(name)、值(value)、过期时间(expiration date)、域(domain)、路径(path)等。服务器可以根据这些属性来确定何时发送或接收Cookie,以及如何处理它们。

​ 简而言之,Cookie是Web应用中实现用户状态保持和个性化服务的重要手段,但它同时也涉及到用户隐私问题,因为它们可以被用于追踪用户在互联网上的活动。出于隐私保护原因,现代浏览器都提供了对Cookie的管理和控制功能,允许用户禁用、删除或限制特定网站的Cookie使用。

​ 上一章讨论了Shiro如何保存会话,每个会话都会有一个SessionID。当一个会话被创建后,默认情况下这个SessionID作为Key被保存起来,上章保存到了Redis中。同时会被放入到Cookie中响应给浏览器,这样浏览器以后每次访问服务端,都会携带这个SessionID,服务端收到这个SessionID后,到Redis中获取Session数据,这样就能够识别到这个用户了。这就是保持会话的原理

2. 禁用Cookie

浏览器是可以禁用Cookie的,一旦浏览器禁用了Cookie后,传递SessionID没法传递到服务端,那就没法实现会话保持了。有一种办法是在所有请求的URL上添加一个请求参数如:JSESSIONID=2e8e4189-9254-4651-a77b-151f504efc3d, 这个请求参数就是SessionID,服务端读取这个参数来获取SessionID从而实现保持会话,这种办法被称为 URL重写(URL rewrite)。但是这种方法相对比较麻烦,业内采用这种方式的不多。

2.1 为什么要禁用Cookie

有一些场景比如 服务端要为小程序提供API服务,需要与小程序应用之间保持会话,还有原生的手机应用客户端程序,这些都没有使用浏览器,没法使用Cookie。

而我们的服务端需要做到为多端提供服务,不同渠道的客户端与服务端之间保持会话如何进行统一?我们的思路是禁用掉Cookie,禁用后,服务端就不会再向客户端响应Cookie数据了,取而代之的是将SessionID 作为响应数据返回给客户端,客户端收到响应后,将这个SessionID保存起来,后面每次发出请求的时候,取出这个SessionID,放入到请求头中,服务端收到请求之后,从请求头中取出SessionID,从而实现会话跟踪。

其实就是把浏览器自动发送Cookie变成了客户端程序发送SessionID,只不过方式是放在请求头中的。但是Shiro默认是开启Cookie的,即使我们禁用了Cookie,Shiro也不会到请求头中去取,这些都需要我们自己写代码去进行改造。

2.2 服务端禁用Cookie

在前一章节中,自己配置了SessionManager, 配置的是DefaultWebSessionManager 我们可以直接在服务端配置,让服务端不产生Cookie。为了方便比较,这里把禁用前和禁用后的相应信息截图出来。

现在不禁用Cookie, 用 Api fox 工具发起一次正确的登录,看看请求和响应报文分别是什么:( Api fox 工具的实际请求=>请求代码=>HTTP 可以看到请求报文)

  • 请求报文

    POST /login HTTP/1.1
    Host: 127.0.0.1:8080
    User-Agent: Apifox/1.0.0 (https://apifox.com)
    Accept: */*
    Host: 127.0.0.1:8080
    Connection: keep-alive
    Content-Type: application/x-www-form-urlencodedusername=administrator&password=admin
    
  • 响应报文
    请添加图片描述
    可以看到,服务器端产生了Cookie,并返回给了客户端。

现在在服务端禁用Cookie,代码如下(第13,15行):

package com.qinyeit.shirojwt.demos.configuration;
@Configuration
@Slf4j
public class ShiroConfiguration {// sessionManager配置@Beanpublic SessionManager sessionManager(SessionFactory sessionFactory,SessionDAO sessionDAO) {DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();// 禁用CookiewebSessionManager.setSessionIdCookieEnabled(false);// 禁用URL重写webSessionManager.setSessionIdUrlRewritingEnabled(false);// 既然cookie都禁用了,就没有必要设置它了// webSessionManager.setSessionIdCookie(cookieTemplate);// 自动配置中已经配置了sessionFactory 直接注入进来webSessionManager.setSessionFactory(sessionFactory);// 使用自定义的ShiroRedisSessionDAOwebSessionManager.setSessionDAO(sessionDAO);// 清理无效的sessionwebSessionManager.setDeleteInvalidSessions(true);// 开启session定时检查webSessionManager.setSessionValidationSchedulerEnabled(true);webSessionManager.setSessionValidationScheduler(new ExecutorServiceSessionValidationScheduler());return webSessionManager;}...
}

修改完毕,重启服务后,将redis中保存的会话数据全部清除掉,然后再执行登录:

  • 请求报文:

    POST /login HTTP/1.1
    Host: 127.0.0.1:8080
    User-Agent: Apifox/1.0.0 (https://apifox.com)
    Accept: */*
    Host: 127.0.0.1:8080
    Connection: keep-alive
    Content-Type: application/x-www-form-urlencodedusername=administrator&password=admin
    
  • 响应报文:
    请添加图片描述

可以看到,服务端不会再将SessionID响应回给客户端了。(不必关注那个 Cookie 1,那是工具里遗留的上一次的数据,没有清理掉)

现在在请求Home,返回的就是:

{"code": 401,"msg": "未登录或登录已过期"
}

因为保持会话的 SessionID丢了。也就是说虽然登录成功了,但是后面再请求服务器的时候,服务器依然不认识这个请求。

3. 保持会话的办法

引用Cookie后,无法保持会话。我们可以将会话ID作为数据响应给客户端程序,客户端程序将这个会话ID临时存储起来,下次发起请求的时候,将它放入到请求头中。(JavaScript 中使用 axios 库,在拦截器中很方便将数据放入到请求头中)。

这里做一个约定: SessionID在服务端依然叫 SessionID, 返回到客户端后,换一个叫法叫做 Access-Token , 请求头的名字也叫 Access-Token ,其实它就是SessionID

如果请求头中加入了自定义的头,这里会引发跨域问题。根据CORS(Cross-Origin Resource Sharing,跨源资源共享)规范,当发起跨域请求时,如果请求包含了自定义请求头(即非简单请求头),浏览器会先发送一个预检(OPTIONS)请求到服务器,询问服务器是否允许实际的请求发生。

这个预检请求会携带Access-Control-Request-Headers头部,列出实际请求打算发送的自定义请求头。服务器需要在响应中通过Access-Control-Allow-Headers头部告知浏览器哪些自定义请求头是可以接受的。只有当服务器确认允许这些自定义请求头之后,浏览器才会发送真实的POST、PUT、DELETE等请求。

简单请求指的是那些方法为GET、HEAD、POST,且满足以下条件之一的请求:

  • Content-Type 是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain。
  • 请求头仅包含Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(满足上述简单请求条件),以及其他若干标准请求头。

只要超出简单请求的范畴,浏览器都会执行预检请求以确保跨域请求的安全性。

不过不用担心,如果我们能确保请求是在同源策略(协议、域名、端口号均相同)下进行的。如果是同源请求,则无论是否有自定义请求头,浏览器都不会发送OPTIONS预检请求。

4.改造DefaultWebSessionManager

DefaultWebSessionManager 默认从Cookie中获取。我们从SessionManager接口开始跟踪,看看它到底是如何获取sessionID的。

先看看类继承图:
在这里插入图片描述
脉络很清晰: DefaultWebSessionManager->DefaultSessionManager->AbstractValidatingSessionManager->AbstractNativeSessionManager->AbstractSessionManager->SessionManger

4.1 找改造点

而SessionManger中哪个方法是获取SessionID的呢?

public interface SessionManager {Session start(SessionContext context);Session getSession(SessionKey key) throws SessionException;
}

从第一个实现类开始,一次向下查找源代码:

  • AbstractSessionManager : 抽象类,并没有实现SessionManager接口。这个类中定义了默认失效时间为 30分钟,也可以在外部调用 setGlobalSessionTimeout(long globalSessionTimeout) 来设置失效时间

  • AbstractNativeSessionManager 抽象类,它实现了 getSession方法,在这个方法中又调用了 本类中的 lookupSession方法, lookupSession方法调用了本类中的抽象方法 doGetsession。 因为是抽象方法,所以子类中一定会实现这个doGetSession方法。

    这个类中可以set 一个 SessionListener 集合进来,这样就可以监听Session的 onStart, onStop,onExpiration

  • AbstractValidatingSessionManager 抽象类,它实现了 doGetSession方法,在doGetSession方法中,调用了 retrieveSession方法, 而retrieveSession方法又是本类中的一个抽象方法, 继续在子类中找retrieveSession 抽象方法的实现

    这个类中可以设置是否开启 用于定期验证会话的调度,和 调度器对象(SessionValidationScheduler)

  • DefaultSessionManager 类,它实现了 retrieveSession 方法,代码片段如下:

    在 retrieveSession 主要调用了本类中的getSessionId 和 retrieveSessionFromDataSource 两个方法。 getSessionId 方法被子类DefaultWebSessionManager 重写了。

    非Web应用的SessionManager 使用的就是这个类。

    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {// 可以看到 从SessionKey 中获取sessionID是有可能为null的// getSessionId 被 子类 DefaultWebSessionManager重写了Serializable sessionId = getSessionId(sessionKey);if (sessionId == null) {LOGGER.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a "+ "session could not be found.", sessionKey);return null;}Session s = retrieveSessionFromDataSource(sessionId);if (s == null) {//session ID was provided, meaning one is expected to be found, but we couldn't find one:String msg = "Could not find session with ID [" + sessionId + "]";throw new UnknownSessionException(msg);}return s;
    }
    // 从sessionKey中获取sessionID, 这个方法是 protected的,子类可以重写
    protected Serializable getSessionId(SessionKey sessionKey) {return sessionKey.getSessionId();
    }
    // 从dao中获取sessionID关联的session对象
    protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {return sessionDAO.readSession(sessionId);
    }
    

    这个类中,可以set进来 sessionDAO, cacheManager和 sessionFactory

  • DefaultWebSessionManager 类,看名字就知道它与Web会话管理有关系,有与cookie,url重写相关的属性。来看看它重写的 getSessionId方法:

    // 重写父类的方法
    @Override
    public Serializable getSessionId(SessionKey key) {Serializable id = super.getSessionId(key);// 父类方法中没有获取到SessionID,而且是 一个 web key (WebSessionKey类)if (id == null && WebUtils.isWeb(key)) {ServletRequest request = WebUtils.getRequest(key);ServletResponse response = WebUtils.getResponse(key);// 调用了下面的方法,最终调用 getReferencedSessionId 方法从cookie中获取sessionIDid = getSessionId(request, response);}return id;
    }protected Serializable getSessionId(ServletRequest request, ServletResponse response) {return getReferencedSessionId(request, response);
    }
    // 省略的代码就是在从Cookie中获取 SessionID
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {// 省略代码....return id;
    }
    

    代码跟踪到这里,就知道了我们该如何做了。

    1. 继承DefaultWebSessionManager

    2. 重写 protected Serializable getSessionId(ServletRequest request, ServletResponse response) 这个方法,在这个方法中从请求头中获取sessionID

    3. 在DefaultWebSessionManager 中,看到了一个方法 private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) 它是私有方法,被 protected void onStart(Session session, SessionContext context) 调用了,这个私有方法主要是创建cookie,并将cookie写回到浏览器。

      所以可以根据自己的需要,如果需要将SessionID响应到客户端,比如写入到响应头上,就可以重写onStart方法,如果没有这个需求则不用管这个方法

4.2 扩展DefaultWebSessionManager

下面我们写一个 AccessTokenWebSessionManager类,继承 DefaultWebSessionManager,并重写getSessionId 方法,从请求头上获取SessionID

package com.qinyeit.shirojwt.demos.shiro.session;
...
@Slf4j
public class AccessTokenWebSessionManager extends DefaultWebSessionManager {public AccessTokenWebSessionManager() {// 禁用Cookiesuper.setSessionIdCookieEnabled(false);// 禁用URL重写super.setSessionIdUrlRewritingEnabled(false);// 因为已经禁用了cookie,所以没有必要有这个配置了.// super.setSessionIdCookie(cookieTemplate);// 清理无效的sessionsuper.setDeleteInvalidSessions(true);// 开启session定时检查super.setSessionValidationSchedulerEnabled(true);super.setSessionValidationScheduler(new ExecutorServiceSessionValidationScheduler());}//从请求头 X-Access-Token 获取SessionIDprotected Serializable getSessionId(ServletRequest request, ServletResponse response) {String sessionId = WebUtils.toHttp(request).getHeader("X-Access-Token");if (sessionId != null) {return sessionId;}return super.getSessionId(request, response);}
}

4.3 改写Controller登录方法

原来登录成功后,是将sessionID,放入了cookie中响应给浏览器,浏览器下次请求的时候,只要cookie没有过期就会自动发送包含了sessionID的cookie。

现在cookie被禁用了,我们需要在登录成功后,将sessionID作为数据返回给客户端,客户端收到后存储起来,下次发送请求的时候,将它放入到请求头X-Access-Token上 .

package com.qinyeit.shirojwt.demos.controller;
@RestController
@Slf4j
public class AuthenticateController {...@PostMapping("/login")public Map<String, String> login(HttpServletRequest req) {Subject             subject = SecurityUtils.getSubject();Map<String, String> map     = new HashMap<>();if (subject.isAuthenticated()) {// 主体的标识,可以有多个,但是需要具备唯一性。比如:用户名,手机号,邮箱等。PrincipalCollection principalCollection = subject.getPrincipals();log.info("是否认证:{},当前登录用户主体信息:{}", subject.isAuthenticated(), principalCollection.getPrimaryPrincipal());map.put("name", principalCollection.getPrimaryPrincipal().toString());// 将sessionID作为数据返回给客户端。map.put("accessToken", subject.getSession().getId().toString());map.put("message", "登录成功");} else {...}return map;}...
}

4.3 配置SessionManager

自定义的AccessTokenWebSessionManager 需要配置成SpringBean:

@Configuration
@Slf4j
public class ShiroConfiguration {...// sessionManager配置 AccessTokenWebSessionManager@Beanpublic SessionManager sessionManager(SessionFactory sessionFactory,SessionDAO sessionDAO) {AccessTokenWebSessionManager webSessionManager = new AccessTokenWebSessionManager();// 自动配置中已经配置了sessionFactory 直接注入进来webSessionManager.setSessionFactory(sessionFactory);// 使用自定义的ShiroRedisSessionDAOwebSessionManager.setSessionDAO(sessionDAO);return webSessionManager;}...
}

5. 测试

程序启动后,先登录:

请求报文:

POST /login HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Apifox/1.0.0 (https://apifox.com)
Accept: */*
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Type: application/x-www-form-urlencodedusername=administrator&password=admin

响应结果:

{"name": "SystemAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt=55ae2b2c63ddd6d4763e0c57bda9078e)","accessToken": "eb6490d0-7562-457c-b98a-69af27b8d6bc","message": "登录成功"
}

可以看到,sessionID作为数据响应回来了。这里没有客户端程序(JavaScript) ,就手动在Api fox 中添加请求头 X-Access-Token, 将 accessToken设置到工具中,然后发送请求到 home
在这里插入图片描述
请求报文:

GET / HTTP/1.1
Host: 127.0.0.1:8080
X-Access-Token: 312cc7ce-38e5-4f89-914d-4d452bb130e5
User-Agent: Apifox/1.0.0 (https://apifox.com)
Accept: */*
Host: 127.0.0.1:8080
Connection: keep-alive

响应结果:

{"sessionKeys": "[org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY, org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY]","name": "SystemAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt=55ae2b2c63ddd6d4763e0c57bda9078e)"
}

6. 总结

  1. 可以在服务端应用中禁用Cookie,配置SessionManager中的 sessionIdCookieEnabled和sessionIdUrlRewritingEnabled
  2. Cookie一旦被禁用,SessionID无法传递,无法保持会话。我们可以在每个请求发出前,在请求报文中加入自定义请求头如: X-Access-Token ,将SessionID放入到这个头上
  3. 自定义一个SessionManager,继承DefaultWebSessionManager,并重写getSessionId 方法从请求头中获取SessionID

代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 5_springboot_shiro_jwt_多端认证鉴权_禁用Cookie 分支上.

相关文章:

  • C语言经典面试题目(十九)
  • JSONP漏洞详解
  • 算法——贪心算法
  • Qt文件以及文件夹相关类(QDir、QFile、QFileInfo)的使用
  • 第七节:Vben Admin权限-后端获取路由和菜单
  • 使用Docker在windows上安装IBM MQ
  • Android 辅助功能 -抢红包
  • VUE3生命周期钩子
  • HCIA_IP路由基础问题?
  • SOPHON算能服务器SDK环境配置和相关库安装
  • 【代码】YOLOv8标注信息验证
  • Element UI +Vue页面生成二维码的方法
  • C++_day6:2024/3/18
  • AWS监控,AWS 性能监控工具
  • Apache Doris 2.1 核心特性 Variant 数据类型技术深度解析
  • 【347天】每日项目总结系列085(2018.01.18)
  • 2017 年终总结 —— 在路上
  • CentOS6 编译安装 redis-3.2.3
  • go append函数以及写入
  • Javascript设计模式学习之Observer(观察者)模式
  • Java新版本的开发已正式进入轨道,版本号18.3
  • PaddlePaddle-GitHub的正确打开姿势
  • Spring Cloud Feign的两种使用姿势
  • vue从入门到进阶:计算属性computed与侦听器watch(三)
  • 关于extract.autodesk.io的一些说明
  • 前言-如何学习区块链
  • 山寨一个 Promise
  • 我建了一个叫Hello World的项目
  • ionic入门之数据绑定显示-1
  • Unity3D - 异步加载游戏场景与异步加载游戏资源进度条 ...
  • 新海诚画集[秒速5センチメートル:樱花抄·春]
  • !!java web学习笔记(一到五)
  • #{}和${}的区别是什么 -- java面试
  • #调用传感器数据_Flink使用函数之监控传感器温度上升提醒
  • (10)STL算法之搜索(二) 二分查找
  • (C++)八皇后问题
  • (Java数据结构)ArrayList
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • (附源码)spring boot校园拼车微信小程序 毕业设计 091617
  • (六)Hibernate的二级缓存
  • (七)Knockout 创建自定义绑定
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (转)关于pipe()的详细解析
  • (轉貼) VS2005 快捷键 (初級) (.NET) (Visual Studio)
  • *Django中的Ajax 纯js的书写样式1
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .NET 5种线程安全集合
  • .NET Core MongoDB数据仓储和工作单元模式封装
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .NET3.5下用Lambda简化跨线程访问窗体控件,避免繁复的delegate,Invoke(转)
  • .net实现头像缩放截取功能 -----转载自accp教程网
  • .NET委托:一个关于C#的睡前故事
  • @RequestMapping-占位符映射
  • [1127]图形打印 sdutOJ
  • [20171102]视图v$session中process字段含义