网页设计站点规划,软件app开发公司,做视频搬运哪个网站最赚钱,做亚马逊网站费用1. Cookie是什么
Cookie是一种在客户端#xff08;通常是用户的Web浏览器#xff09;和服务器之间进行状态管理的技术。当用户访问Web服务器时#xff0c;服务器可以向用户的浏览器发送一个名为Cookie的小数据块。浏览器会将这个Cookie存储在客户端#xff0c;为这个Co…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上添加一个请求参数如JSESSIONID2e8e4189-9254-4651-a77b-151f504efc3d, 这个请求参数就是SessionID服务端读取这个参数来获取SessionID从而实现保持会话这种办法被称为 URL重写(URL rewrite)。但是这种方法相对比较麻烦业内采用这种方式的不多。
2.1 为什么要禁用Cookie
有一些场景比如 服务端要为小程序提供API服务需要与小程序应用之间保持会话还有原生的手机应用客户端程序这些都没有使用浏览器没法使用Cookie。
而我们的服务端需要做到为多端提供服务不同渠道的客户端与服务端之间保持会话如何进行统一我们的思路是禁用掉Cookie禁用后服务端就不会再向客户端响应Cookie数据了取而代之的是将SessionID 作为响应数据返回给客户端,客户端收到响应后将这个SessionID保存起来后面每次发出请求的时候取出这个SessionID放入到请求头中服务端收到请求之后从请求头中取出SessionID从而实现会话跟踪。
其实就是把浏览器自动发送Cookie变成了客户端程序发送SessionID只不过方式是放在请求头中的。但是Shiro默认是开启Cookie的即使我们禁用了CookieShiro也不会到请求头中去取这些都需要我们自己写代码去进行改造。
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-urlencodedusernameadministratorpasswordadmin响应报文 可以看到服务器端产生了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-urlencodedusernameadministratorpasswordadmin响应报文
可以看到服务端不会再将SessionID响应回给客户端了。(不必关注那个 Cookie 1那是工具里遗留的上一次的数据没有清理掉)
现在在请求Home返回的就是
{code: 401,msg: 未登录或登录已过期
}因为保持会话的 SessionID丢了。也就是说虽然登录成功了但是后面再请求服务器的时候服务器依然不认识这个请求。
3. 保持会话的办法
引用Cookie后无法保持会话。我们可以将会话ID作为数据响应给客户端程序客户端程序将这个会话ID临时存储起来下次发起请求的时候将它放入到请求头中。JavaScript 中使用 axios 库在拦截器中很方便将数据放入到请求头中。
这里做一个约定 SessionID在服务端依然叫 SessionID, 返回到客户端后换一个叫法叫做 Access-Token , 请求头的名字也叫 Access-Token 其实它就是SessionID 如果请求头中加入了自定义的头这里会引发跨域问题。根据CORSCross-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 couldnt 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会话管理有关系有与cookieurl重写相关的属性。来看看它重写的 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;
}代码跟踪到这里就知道了我们该如何做了。 继承DefaultWebSessionManager 重写 protected Serializable getSessionId(ServletRequest request, ServletResponse response) 这个方法在这个方法中从请求头中获取sessionID 在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 MapString, String login(HttpServletRequest req) {Subject subject SecurityUtils.getSubject();MapString, 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配置 AccessTokenWebSessionManagerBeanpublic 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-urlencodedusernameadministratorpasswordadmin响应结果
{name: SystemAccount(accountadministrator, pwdEncrypt0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt55ae2b2c63ddd6d4763e0c57bda9078e),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(accountadministrator, pwdEncrypt0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt55ae2b2c63ddd6d4763e0c57bda9078e)
}6. 总结
可以在服务端应用中禁用Cookie配置SessionManager中的 sessionIdCookieEnabled和sessionIdUrlRewritingEnabledCookie一旦被禁用SessionID无法传递无法保持会话。我们可以在每个请求发出前在请求报文中加入自定义请求头如: X-Access-Token 将SessionID放入到这个头上自定义一个SessionManager继承DefaultWebSessionManager并重写getSessionId 方法从请求头中获取SessionID
代码仓库 https://github.com/kaiwill/shiro-jwt 本节代码在 5_springboot_shiro_jwt_多端认证鉴权_禁用Cookie 分支上.