Java Web之Servlet及Cookie/Session的原理
Servlet参考文献:
1、http://www.cnblogs.com/luoxn28/p/5460073.html
2、http://www.cnblogs.com/xdp-gacl/p/3760336.html
一、Servlet简介
Servlet是sun公司提供的一门用于开发动态web资源的技术。
Sun公司在其API中提供了一个servlet接口,用户若想用发一个动态web资源(即开发一个Java程序向浏览器输出数据),需要完成以下2个步骤:
1、编写一个Java类,实现servlet接口。
2、把开发好的Java类部署到web服务器中。
按照一种约定俗成的称呼习惯,通常我们也把实现了servlet接口的java程序,称之为Servlet。
二、Servlet实现
Servlet接口SUN公司定义了两个默认实现类,分别为:GenericServlet、HttpServlet。
HttpServlet指能够处理HTTP请求的servlet,它在原有Servlet接口上添加了一些与HTTP协议处理方法,它比Servlet接口的功能更为强大。因此开发人员在编写Servlet时,通常应继承这个类,而避免直接去实现Servlet接口。
HttpServlet在实现Servlet接口时,覆写了service方法,该方法体内的代码会自动判断用户的请求方式,如为GET请求,则调用HttpServlet的doGet方法,如为Post请求,则调用doPost方法。因此,开发人员在编写Servlet时,通常只需要覆写doGet或doPost方法,而不要去覆写service方法。
通常是通过继承HttpServlet来实现自己的Servlet。要访问所实现的Servlet,需要在web.xml里进行配置,典型配置如下:
<servlet> <description>test of servlet life cycle</description> <display-name>ServletLifeCycle</display-name> <servlet-name>ServletLifeCycle</servlet-name> <servlet-class>com.zsm.servlet.ServletLifeCycle</servlet-class> <!--使用<init-param>元素配置Servlet辅助信息,当Tomcat初始化一个Servlet时,会将该配置信息封装成一个ServletConfig对象通过init(ServletConfig config)传递到Servlet,在其中可以访问到。可以不配做--> <init-param> <param-name>myServletInfo</param-name> <param-value>“hello there”</param-value> </init-param> <!-- 1)load-on-startup元素标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法)。 2)它的值必须是一个整数,表示servlet应该被载入的顺序 2)当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet; 3)当值小于0或者没有指定时,则表示容器在该servlet被选择时才会去加载。 4)正数的值越小,该servlet的优先级越高,应用启动时就越先加载。 5)当值相同时,容器就会自己选择顺序来加载。--> <load-on-startup>-10</load-on-startup> </servlet> <!--一个servlet-name可以有多个url-pattern;在Servlet映射到的URL中也可以使用*通配符,但是只能有两种固定的格式:一种格式是"*.扩展名",另一种格式是以正斜杠(/)开头并以"/*"结尾。如:*.jsp、/*、/abc、/abc/*等--> <servlet-mapping> <servlet-name>ServletLifeCycle</servlet-name> <url-pattern>/ServletLifeCycle</url-pattern> </servlet-mapping>
三、Servlet的运行过程
Servlet程序是由WEB服务器调用,web服务器收到客户端的Servlet访问请求后:
①Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
②装载并创建该Servlet的一个实例对象。
③调用Servlet实例对象的init()方法。
④创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
⑤WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。
四、Servlet调用图
五、关于HttpServletResponse
参考文献:http://www.cnblogs.com/xdp-gacl/p/3789624.html
- getOutputStream和getWriter方法分别用于得到输出二进制数据、输出文本数据的ServletOuputStream、Printwriter对象。
- getOutputStream和getWriter这两个方法互相排斥,调用了其中的任何一个方法后,就不能再调用另一方法。
- Servlet程序向ServletOutputStream或PrintWriter对象中写入的数据将被Servlet引擎从response里面获取,Servlet引擎将这些数据当作响应消息的正文,然后再与响应状态行和各响应头组合后输出到客户端。
- Serlvet的service方法结束后,Servlet引擎将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,Servlet引擎将调用close方法关闭该输出流对象。
六、关于HttpServletRequest
参考文献:Request http://www.cnblogs.com/xdp-gacl/p/3798347.html
使用Request对象实现请求转发:
请求转发:指一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理。
请求重定向和请求转发的区别:
- 一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理,称之为请求转发/307。请求转发只能转发到同一web应用内的其他url去。
- 一个web资源收到客户端请求后,通知浏览器去访问另外一个web资源进行处理,称之为请求重定向/302。请求重定向可以转发到任何url去,不限制在同一web应用内。
请求转发的应用场景:MVC设计模式
在Servlet中实现请求转发的两种方式:
- 通过ServletContext的getRequestDispatcher(String path)方法,该方法返回一个RequestDispatcher对象,调用这个对象的forward方法可以实现请求转发。 例如:将请求转发到test.jsp页面
RequestDispatcher reqDispatcher =this.getServletContext().getRequestDispatcher("/test.jsp"); reqDispatcher.forward(request, response);
- 通过request对象提供的getRequestDispatche(String path)方法,该方法返回一个RequestDispatcher对象,调用这个对象的forward方法可以实现请求转发。 例如:将请求转发的test.jsp页面
request.getRequestDispatcher("/test.jsp").forward(request, response);
七、会话管理
这里的会话管理指的是HTTP会话管理,下面的Cookie、Session也是指的HTTP Cookie、HTTP Session。
1、会话的概念
会话可简单理解为:用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话。
有状态会话:一个同学来过教室,下次再来教室,我们会知道这个同学曾经来过,这称之为有状态会话。
2、会话过程中要解决的问题
每个用户在使用浏览器与服务器进行会话的过程中,不可避免各自会产生一些数据,程序要想办法为每个用户保存这些数据。
3、保存会话数据的方案
3.1、Cookie
Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。这样,web资源处理的就是用户各自的数据了。
3.2、Session
Session是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其独享的session对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,服务器可以把与该用户相关的数据关联到其对应的session中,当用户再去访问服务器中的其它web资源时,其它web资源再从用户各自的session中取出数据为用户服务。
Session 的实现方式有多种,使用场景不尽相同,其中借助Cookie的实现方案最常见。
3.3 JWT
见 https://www.cnblogs.com/z-sm/p/9125995.html
此方案本质跟Cookie方案有点像,服务端都不保存数据而是客户端保存、客户端访问服务端时带上数据。
八、使用Cookie进行会话管理
参考资料:http://www.cnblogs.com/xdp-gacl/p/3803033.html
1、Cookie的类型
有 session cookies 和 persistent cookies 两种类型。前者存在内存、后者存在磁盘,未给Cookie指定过期时间则该Cookie为Session Cookie,Session Cookie随着Session的过期而过期。
There are two different types of cookies - session cookies and persistent cookies. If a cookie does not contain an expiration date, it is considered a session cookie. Session cookies are stored in memory and never written to disk. When the browser closes, the cookie is permanently lost from this point on. If the cookie contains an expiration date, it is considered a persistent cookie. On the date specified in the expiration, the cookie will be removed from the disk.
2、Cookie的设置
//用户访问过之后重新设置用户的访问时间,存储到cookie中,然后发送到客户端浏览器 Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");//创建一个cookie,cookie的名字是lastAccessTime //将cookie对象添加到response对象中,这样服务器在输出response对象中的内容时就会把cookie也输出到客户端浏览器 response.addCookie(cookie);
cookie的属性:
private String comment; // ;Comment=VALUE ... describes cookie's use // ;Discard ... implied by maxAge < 0 private String domain; // ;Domain=VALUE ... domain that sees cookie。注意值不包括端口 private int maxAge = -1; // ;Max-Age=VALUE ... cookies auto-expire。负值表示会话级有效(存内存)、0表示立即失效(删除)、正值表示有效期(存硬盘) private String path; // ;Path=VALUE ... URLs that see the cookie private boolean secure; // ;Secure ... e.g. use SSL private int version = 0; // ;Version=1 ... means RFC 2109++ style private boolean isHttpOnly = false;
通过maxAge可以设置Cookie的有效期,如:
cookie.setMaxAge(0);//不记录cookie,以前有的话立即删除。 cookie.setMaxAge(-1);//会话级cookie,存在内存,关闭浏览器失效。 cookie.setMaxAge(60*60);//过期时间为1小时,会持久化到硬盘。
3、Cookie的删除
//创建一个名字为lastAccessTime的cookie Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+""); //将cookie的有效期设置为0,命令浏览器删除该cookie cookie.setMaxAge(0); response.addCookie(cookie);
4、Cookie中存取中文
Cookie cookie = new Cookie("user", URLEncoder.encode("小张"));// 包含中文时必须编码否则会出错 response.addCookie(cookie);
一些细节:
- 一个Cookie只能标识一种信息,它至少含有一个标识该信息的名称(NAME)和设置值(VALUE)。
- 一个WEB站点可以给一个WEB浏览器发送多个Cookie,一个WEB浏览器也可以存储多个WEB站点提供的Cookie。
- 浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。
- 如果创建了一个cookie,并将他发送到浏览器,默认情况下(未指定过期时间)它是一个会话级别的cookie(即存储在浏览器的内存中),用户退出浏览器之后即被删除。若希望浏览器将该cookie存储在磁盘上,则需要使用maxAge,并给出一个以秒为单位的时间。将最大时效设为0则是命令浏览器删除该cookie。
一个坑:
Cookie有有版本0和版本1,前者早出现,几乎被所有浏览器支持,但不能含空格,方括号,圆括号,等于号(=),逗号,双引号,斜杠,问号,@符号,冒号,分号等特殊字符,否则浏览器收到时会将值加上双引号,导致服务端收到的与写入的不一致;后支持了特殊字符,但浏览器兼容性比前者差。实践中就踩了这个坑,jwt token在cookie中传回server时被加了双引号。
为了兼容性,jdk里支持的是版本0,因此要解决双引号问题:写时url encode读时decode。可参阅:https://maoxian.de/2018/02/1480.html
java.servlet.http.Cookie 3.1.0已经0和1版本都支持
九、使用Session进行会话管理
参考资料:http://www.cnblogs.com/xdp-gacl/p/3855702.html
1、Session简单介绍
在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。
2、Session和Cookie的主要区别
- Cookie是把用户的数据写给用户的浏览器。
- Session技术把用户的数据写到用户独占的session中。
- Session对象由服务器创建,开发人员可以调用request对象的getSession方法得到session对象。
3、Session的设置
通过maxInactiveInterval可以设置session有效期。
4、Session实现原理
(Session技术由Web容器如Tomcat实现)
服务器创建session对象后,在服务器内存存储该对象,并把session的id号以cookie的形式回写给客户机(键为JSESSIONID,值为该session的id)。这样,只要客户机的浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户机浏览器带session id过来了,即可在内存中找到对应的session。
即Session会话(如何识别同一浏览器的多次请求属于同一个会话)是通过Cookie实现的。因此如果浏览器禁用Cookie会使Session没法work。
- 服务端如何保存不同会话的Session信息: protected Map<String, Session> sessions = new ConcurrentHashMap<>(); ,key为session的id、值为session对象
- 服务端如何保存与某个Session绑定的信息: protected Map<String, Object> attributes = new ConcurrentHashMap<String, Object>(); ,key为各属性名、值为属性值,保存的内容如session有效期及用户存入的其他attribute等
Session的具体实现原理可参阅:https://www.cnblogs.com/chenpi/p/5434537.html
透过现象看本质,可以发现Session技术根本上是依赖于浏览器会在每次请求时自动把某些数据放请求头上传给服务端(即Cookie技术)这一基础的。
Session技术的缺点:
- 内存占用:服务端在内存维护Session数据及关联的Attributes等信息,用户多时占用内存大
- 多节点共享:服务端一个应用常会部署多个实例,此场景下Session无法共享,导致在A实例上登录后在B实例不认为已登录这种情况
- 安全问题:在浏览器上登录某站点后得到的sessionID复制到B浏览器该站点对应的Cookie下,则B会被服务端认为是A,这就是Session劫持问题
5、Session对象的创建和销毁时机
-
创建:第一次调用request.getSession()方法时就会创建一个新的Session,可以用isNew()方法来判断Session是不是新创建的
- 销毁:session对象默认30分钟没有使用(指的是没有去读或写session,在业务上体现为有没有该session对应的request请求)则服务器会自动销毁session,在web.xml文件中可以手工配置session的失效时间(如下);当需要在程序中手动设置Session失效时,可以手工调用session.invalidate方法,摧毁session。
<session-config> <session-timeout>15</session-timeout> </session-config>
注意,session.setMaxInactiveInterval(int interval) 是用来设置最大不操作时间的,超过该时间不读写session则会被服务端删除该session,默认值是30分钟。
6、浏览器禁用Cookie时如何还能让Servlet共享Session中的数据
浏览器禁用Cookie后,服务端创建的Session的id就无法保存到客户端也就无法识别那个用户是哪个了。
解决方法:对URL进行重写,当禁用时会自动Encodes the specified URL by including the session ID in it。仅适用于URL由服务端返回的场景。
response.encodeRedirectURL(java.lang.String url) 用于对sendRedirect方法后的url地址进行重写。
response.encodeURL(java.lang.String url)用于对表单action和超链接的url地址进行重写
Java.servlet.http.HttpSession says: The server can maintain a session in many ways such as using cookies or rewriting URLs.
示例:
out.println("<hr>"); request.getSession();// 获取或创建Session,必须要有此句后面的encodeURL才会在必要时把sessionid编入 String pageContent = "<form action='" + response.encodeURL("VerificationCode") + "' method='post'> " + "验证码:<input type='text' name='validateCode'> " + "<img alt='验证码看不清,换一张' src='" + response.encodeURL("VerificationCode?createTypeFlag=nl") + "' > <br> " + "<input type='submit' value='提交'></form>"; out.println(pageContent);
缺点:rewriting URL can be used in case a link is output to the user and you need to maintain the session state if cookies is turned off. 即:此方法仅用于维护由server返回的url间的session state,对于非由server返回的url,由于没有把相关信息编码到url故无法将这些url识别为同一个session中的。因此,此法没有基于cookie实现的session方法强大。
实际上,session技术的实现方式有多种,如:
COOKIE:Send a cookie in response to the client's first request.
URL Rewrite:Rewrite the URL to append a session ID.
SSL:Use SSL build-in mechanism to track the session.
6、防止表单重复提交
6.1、重复提交的场景
- 用户点提交后在处理完之前又点提交
- 点提交后刷新页面导致重复提交
- 提交后点后退导致重复提交
6.2、解决方法
- 对于场景1,可以在客户端用js防止重复提交。或者在提交响应结束前禁用提交按钮。
var isCommitted = false;//表单是否已经提交标识,默认为false function dosubmit(){ if(isCommitted==false){ isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true return true;//返回true让表单正常提交 }else{ return false;//返回false那么表单将不提交 } }
- 对于场景2或3,单靠客户端无法解决,对于单节点服务器可以借助Session来解决:生成唯一的标识(Token)放在表单的隐藏域,同时把标识存入当前session域;收到请求时判断表单隐藏域Token是否与当前session中的一致,是则处理,并删掉session域中的Token。
具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
在下列情况下,服务器程序将拒绝处理用户提交的表单请求:- 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
- 当前用户的Session中不存在Token(令牌)。
- 用户提交的表单数据中没有Token(令牌)。
7、分布式session
基于Redis实现分布式Session:
import java.io.IOException; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.UUID; import javax.servlet.ServletContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionContext; import org.springframework.http.HttpStatus; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * servlet容器中的{@link javax.servlet.http.HttpSession } * 只支持单实例,在多实例部署的情况下无法保证session的一致性,如:client(如浏览器)第一次访问了实例A,A为之建立了个session会话并将session * id通过cookie写回client;之后client访问了实例B,此时B并不会认为与刚A接收到的是同一个请求者。<br> * <br> * 此工具要解决的就是上述问题。此外,这里只解决通过cookie实现session的场景下的上述问题;对于通过rewriting * url、通过ssl等实现session的场景,仍会有上述问题,我们暂不考虑这些场景下的该问题。 <br> * <br> * 说明:<br> * 1、有前端请求时才能创建本类的实例,否则会报错。<br> * <br> * 2、cookie有两种类型:session cookie、expire * cookie,cookie不设有效期则默认是前者。按标准,session给前端设置cookie时用的是session * cookie,故只要不设置有效期即可添加第一种类型的cookie。<br> * <br> * 3、此DistributedHttpSession与传统的HTTPSession的区别:后者若是同一个会话则得到的Session对象完全是“同一个”,即不仅内容而且地址都相同;前者则只是内容相同。实际上,地址不同也不影响功能,只不过相同则可使得不用每次新new对象从而减少内存占用。<br> * <br> * 4、当前进程内获得会话对象后,在使用该会话对象的过程中其他进程可能对同一会话的数据做了修改(如修改有效期、置为无效等)。因此,为了一致性,不在本类内缓存session * data,而是每次都去redis取<br> * <br> * 5、此DistributedHttpSession过期删除借由redis key expire实现(maxInactiveInterval)<br> * <br> * 6、为免数据无限增加,内部设置了默认的session maxInactiveInterval 值。若使用者未设置有效期,则默认为此值。见 * {@link #SESSION_DEFAULT_TTL }</b> <br> * <br> */ @Slf4j public class DistributedHttpSession implements HttpSession { private static final ObjectMapper jsonUtil = new ObjectMapper(); /** session id数据在cookie中的key。标准HttpSession的为"JSESSIONID" */ private static final String SESSIONID_KEY_IN_COOKIE = "ss_JSESSIONID"; /** 存到redis的session数据的redis key */ private static final String SESSION_KEY_PREFIX = "ss_distributed_session:"; /** 为免数据无限增加,设置了默认的session ttl。若使用者未设置,则默认为此值。单位为秒。 */ private static final int SESSION_DEFAULT_TTL = 1 * 24 * 60 * 60; private boolean isNewCreatedSession; private String sessionId; // 创建时机为前端发来请求时 public DistributedHttpSession(HttpServletRequest request, HttpServletResponse response) { // 确保在创建此类实例时有调用者发起请求,从而确保lastAccessTime在由前端调用时才更新 CustomException.assertTrue(null != request, ManagementErrorTypeEnum.NOT_FOUND_PARAM, "HttpServletRequest param not found", null); CustomException.assertTrue(null != response, ManagementErrorTypeEnum.NOT_FOUND_PARAM, "HttpServletResponse param not found", null); String reqSessionId = getSessionIdFromRequestCookie(request); SessionData sessionData = null; // 判断是否新建会话数据,优先尝试根据sessionId获取会话数据 if (null != reqSessionId) { SessionData savedSessionData = getSessionDataFromRedis(getRedisKeyOfSession(reqSessionId)); if (null != savedSessionData) { sessionData = savedSessionData; isNewCreatedSession = false; } else { isNewCreatedSession = true; } } else { isNewCreatedSession = true; } // 新建会话数据:创建会话数据并保存到redis;将session id通过cookie发给浏览器 long curTimeMs = System.currentTimeMillis(); if (isNewCreatedSession) { sessionData = new SessionData(generateNewSessionId(), curTimeMs, curTimeMs, SESSION_DEFAULT_TTL, new HashMap<>()); } // 更新会话数据:最后访问时间 else { sessionData.setLastAccessTime_MS(curTimeMs); } sessionId = sessionData.getSessionId(); String redisKey = getRedisKeyOfSession(sessionId); // 保存会话数据 setSessionDataToRedis(redisKey, sessionData); // 有人访问则延长有效期。不管新建还是更新,都需要如此。这里通过redis expire实现 Integer sessionTTL = sessionData.getMaxInactiveInterval_S(); if (null != sessionTTL && sessionTTL > 0) { JedisPoolClientUtil.expire(redisKey, sessionTTL); } // 回写cookie Cookie cookie = new Cookie(SESSIONID_KEY_IN_COOKIE, sessionData.getSessionId()); cookie.setPath("/"); // cookie.setMaxAge(expireSeconds);//按标准,session用的是第一种类型的cookie,故不用设置有效期。 response.addCookie(cookie); // log.info("create session instance with session id {}" + sessionId); } // 以下为自定义无状态的方法 // ====== stateless methods start ====== private String getSessionIdFromRequestCookie(HttpServletRequest request) { String sessionId = null; // 尝试从cookie取 Cookie[] cookies = request.getCookies(); if (null != cookies) { for (Cookie cookie : cookies) { if (cookie.getName().equalsIgnoreCase(SESSIONID_KEY_IN_COOKIE)) { sessionId = cookie.getValue(); break; } } } return sessionId; } private String generateNewSessionId() { return (UUID.randomUUID().toString()).replaceAll("-", ""); } private String getRedisKeyOfSession(String sessionId) { return SESSION_KEY_PREFIX + sessionId; } private SessionData getSessionDataFromRedis(String redisKey) { String redisData = JedisPoolClientUtil.get(redisKey); try { return null == redisData ? null : jsonUtil.readValue(redisData.getBytes(), SessionData.class); } catch (IOException e) { log.error(e.getMessage(), e); return null; } } public void setSessionDataToRedis(String redisKey, SessionData sessionData) { try { JedisPoolClientUtil.set(redisKey, jsonUtil.writeValueAsString(sessionData)); } catch (JsonProcessingException e) { log.error(e.getMessage(), e); } } // ====== stateless methods end ====== // 以下为父接口的方法 @Override public long getCreationTime() { SessionData sessionData = getSessionDataFromRedis(getRedisKeyOfSession(sessionId)); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); return sessionData.getCreateTime_Ms(); } @Override public String getId() { SessionData sessionData = getSessionDataFromRedis(getRedisKeyOfSession(sessionId)); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); return sessionData.getSessionId(); } @Override public long getLastAccessedTime() { SessionData sessionData = getSessionDataFromRedis(getRedisKeyOfSession(sessionId)); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); return sessionData.getLastAccessTime_MS(); } @Override public void setMaxInactiveInterval(int interval) { String redisKey = getRedisKeyOfSession(sessionId); SessionData sessionData = getSessionDataFromRedis(redisKey); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); sessionData.setMaxInactiveInterval_S(interval); setSessionDataToRedis(redisKey, sessionData); // ttl if (interval > 0) { JedisPoolClientUtil.expire(redisKey, interval); } } @Override public int getMaxInactiveInterval() { SessionData sessionData = getSessionDataFromRedis(getRedisKeyOfSession(sessionId)); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); return sessionData.getMaxInactiveInterval_S(); } @Override public Object getAttribute(String name) { SessionData sessionData = getSessionDataFromRedis(getRedisKeyOfSession(sessionId)); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); return sessionData.getSessionAttributes().get(name); } @Override public Enumeration<String> getAttributeNames() { SessionData sessionData = getSessionDataFromRedis(getRedisKeyOfSession(sessionId)); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); return Collections.enumeration(sessionData.getSessionAttributes().keySet()); } @Override public void setAttribute(String name, Object value) { String redisKey = getRedisKeyOfSession(sessionId); SessionData sessionData = getSessionDataFromRedis(redisKey); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); sessionData.getSessionAttributes().put(name, value); setSessionDataToRedis(redisKey, sessionData); } @Override public void removeAttribute(String name) { String redisKey = getRedisKeyOfSession(sessionId); SessionData sessionData = getSessionDataFromRedis(redisKey); CustomException.assertTrue(null != sessionData, ManagementErrorTypeEnum.AUTH_UNAUTENTICATE, "session is invalid", null, HttpStatus.UNAUTHORIZED); sessionData.getSessionAttributes().remove(name); setSessionDataToRedis(redisKey, sessionData); } @Override public void invalidate() { JedisPoolClientUtil.del(getRedisKeyOfSession(sessionId)); } @Override public boolean isNew() { return isNewCreatedSession; } @Override public Object getValue(String name) { return getAttribute(name); } @Override public void removeValue(String name) { removeAttribute(name); } @Override public void putValue(String name, Object value) { setAttribute(name, value); } @Override public String[] getValueNames() { return getValueNames(); } @Override public HttpSessionContext getSessionContext() { throw new RuntimeException("method not support"); } @Override public ServletContext getServletContext() { throw new RuntimeException("method not support"); } @AllArgsConstructor @NoArgsConstructor @Data private static class SessionData { private String sessionId; private Long createTime_Ms; // 只有在前端发来request(即创建DistributedHttpSession对象)时才会更新此值,后端对同一个会话对象的多次访问不会更新此值。 private long lastAccessTime_MS; private Integer maxInactiveInterval_S; private Map<String, Object> sessionAttributes; } }
使用:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; /** 记住登录状态的工具,仅给本OAuth模块内部用(用于记录授权者登录状态)!! */ public class AuthLoginStateUtil {// TODO response作为方法参数 private static final String KEY_NAME_USERID = "ss_oauth_authorizerUserId"; private static final String KEY_NAME_USERNAME = "ss_oauth_authorizerUserName"; private static final int SESSION_TTL_S = 10 * 60; // 内部工具方法 private static boolean useDistributedSession = true; private static HttpSession getHttpSession(HttpServletRequest request, HttpServletResponse response) { return useDistributedSession ? new DistributedHttpSession(request, response) : request.getSession(); } // 对外方法 public static void setAsAlreadyLogin(String userId, String userName, HttpServletRequest request) { HttpSession session = getHttpSession(request, ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse()); session.setAttribute(KEY_NAME_USERID, userId); session.setAttribute(KEY_NAME_USERNAME, userName); session.setMaxInactiveInterval(SESSION_TTL_S); } public static void setAsNotLogin(HttpServletRequest request) { HttpSession session = getHttpSession(request, ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse()); session.invalidate(); } public static boolean isAlreadyLogIn(HttpServletRequest request) { return null != getUserId(request); } public static String getUserId(HttpServletRequest request) { Object val = getHttpSession(request, ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse()) .getAttribute(KEY_NAME_USERID); return (val instanceof String) ? (String) val : null; } public static String getUserName(HttpServletRequest request) { Object val = getHttpSession(request, ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse()) .getAttribute(KEY_NAME_USERNAME); return (val instanceof String) ? (String) val : null; } }
十、其他
1、Servlet与普通Java类的区别
Servlet是一个供其他Java程序(Servlet引擎)调用的Java类,它不能独立运行,它的运行完全由Servlet引擎来控制和调度。
针对客户端的多次Servlet请求,通常情况下,服务器只会创建一个Servlet实例对象,也就是说Servlet实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至web容器退出,servlet实例对象才会销毁。
在Servlet的整个生命周期内,Servlet的init方法只被调用一次。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest请求对象和一个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调用的Servlet的service()方法,service方法再根据请求方式分别调用doXXX方法。
如果在<servlet>元素中配置了一个<load-on-startup>元素,那么WEB应用程序在启动时,就会装载并创建Servlet的实例对象、以及调用Servlet实例对象的init()方法。用途:为web应用写一个InitServlet,这个servlet配置为启动时装载,为整个web应用创建必要的数据库表和数据。
2、缺省Servlet
如果某个Servlet的映射路径仅仅为一个正斜杠(/),那么这个Servlet就成为当前Web应用程序的缺省Servlet。
凡是在web.xml文件中找不到匹配的<servlet-mapping>元素的URL,它们的访问请求都将交给缺省Servlet处理,也就是说,缺省Servlet用于处理所有其他Servlet都不处理的访问请求。
在<tomcat的安装目录>\conf\web.xml文件中,注册了一个名称为org.apache.catalina.servlets.DefaultServlet的Servlet,并将这个Servlet设置为了缺省Servlet。如下:
<servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>listings</param-name> <param-value>false</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- The mapping for the default servlet --> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
3、线程安全问题
当多个客户端并发访问同一个Servlet时,web服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用Servlet的service方法,service方法会调用doGet或doPost等方法。因此在后者内如果访问了同一个文件资源或全局变量等就有可能引发线程安全问题,访问方法内定义的局部变量则不会有线程安全问题。因此在写代码时要注意线程安全问题。