我在一些网站上看到用户登录他们的帐户然后关闭浏览器。
关闭并重新打开浏览器后,他们的帐户仍然登录。
但是有些网站,不能那样做。
我很困惑它被认为是会话还是cookie?
如果我希望我的网站以这样的方式登录,是否必须设置session.setMaxInactiveInterval()
或cookie.setMaxAge()
?
*此答案存在严重缺陷,请参阅评论。*
您的问题是关于会话跟踪。
[第 1 部分]:会话对象
HTTP 请求是单独处理的,因此为了在每个请求之间保留信息(例如,有关用户的信息),必须在服务器端创建一个会话对象。
有些网站根本不需要会话。用户无法修改任何内容的网站不必管理会话(例如,在线简历)。在这样的网站上,您不需要任何 cookie 或会话。
创建一个会话:
在 servlet 中,使用request.getSession(true)
HttpServletRequest 对象中的方法创建一个新的 HttpSession 对象。请注意,如果您使用request.getSession(false)
,如果尚未创建会话,则将返回null 。查看此答案以获取更多详细信息。
设置/获取属性:
会话的目的是在每个请求之间保持服务器端的信息。例如,保留用户名:
session.setAttribute("name","MAGLEFF");
// Cast
String name = (String) session.getAttribute("name");
销毁会话:
如果长时间保持不活动状态,会话将自动销毁。查看此答案以获取更多详细信息。但是您可以手动强制销毁会话,例如在注销操作的情况下:
HttpSession session = request.getSession(true);
session.invalidate();
[第 2 部分]:所以...加入黑暗面,我们有 COOKIES 吗?
饼干来了。
JSESSIONID:
每次使用request.getSession()
. _ 为什么?因为在服务器端创建的每个会话都有一个 ID。除非您没有正确的 ID,否则您无法访问其他用户的会话。这个 ID 保存在 JSESSIONID cookie 中,并允许用户找到他的信息。查看此答案以获取更多详细信息!
JSESSIONID 什么时候被删除?
JSESSIONID 没有过期日期:它是一个会话 cookie。与所有会话 cookie 一样,它会在浏览器关闭时被删除。如果您使用基本的 JSESSIONID 机制,那么在您关闭并重新打开浏览器后,会话将变得无法访问,因为 JSESSIONID cookie 已被删除。
请注意,客户端无法访问会话,但仍在服务器端运行。设置MaxInactiveInterval允许服务器在会话不活动时间过长时自动使会话无效。
JSESSIONID的邪恶破坏
只是为了好玩,有一天我在一个项目中发现了这段代码。它用于通过使用 javascript 删除 JSESSIONID cookie 来使会话无效:
<SCRIPT language="JavaScript" type="text/javascript">
function delete_cookie( check_name ) {
// first we'll split this cookie up into name/value pairs
// note: document.cookie only returns name=value, not the other components
var a_all_cookies = document.cookie.split( ';' );
var a_temp_cookie = '';
var cookie_name = '';
var cookie_value = '';
var b_cookie_found = false; // set boolean t/f default f
// var check_name = 'JSESSIONID';
var path = null;
for ( i = 0; i < a_all_cookies.length; i++ )
{
// now we'll split apart each name=value pair
a_temp_cookie = a_all_cookies[i].split( '=' );
// and trim left/right whitespace while we're at it
cookie_name = a_temp_cookie[0].replace(/^\s+|\s+$/g, '');
// alert (cookie_name);
// if the extracted name matches passed check_name
if ( cookie_name.indexOf(check_name) > -1 )
{
b_cookie_found = true;
// we need to handle case where cookie has no value but exists (no = sign, that is):
if ( a_temp_cookie.length > 1 )
{
cookie_value = unescape( a_temp_cookie[1].replace(/^\s+|\s+$/g, '') );
document.cookie = cookie_name + "=" + cookie_value +
";path=/" +
";expires=Thu, 01-Jan-1970 00:00:01 GMT";
// alert("cookie deleted " + cookie_name);
}
}
a_temp_cookie = null;
cookie_name = '';
}
return true;
}
// DESTROY
delete_cookie("JSESSIONID");
</SCRIPT>
再看看这个答案。使用 JavaScript,可以读取、修改 JSESSIONID,使其会话丢失或被劫持。
[第 3 部分]:关闭浏览器后保持会话
关闭并重新打开浏览器后,他们的帐户仍然登录。但有些网站,不能这样做。我很困惑它被认为是会话还是cookie?
是饼干。
我们看到,当 Web 浏览器删除了 JSESSIONID 会话 cookie 时,服务器端的会话对象就丢失了。如果没有正确的 ID,就无法再次访问它。
如果我希望我的网站以这样的方式登录,是否必须设置 session.setMaxInactiveInterval() 或 cookie.setMaxAge()?
我们还看到这session.setMaxInactiveInterval()
是为了防止无限期地运行丢失的会话。JSESSIONID cookiecookie.setMaxAge()
也不会将我们带到任何地方。
使用带有会话 ID 的持久 cookie:
在阅读了以下主题后,我来到了这个解决方案:
主要思想是在一个 Map 中注册用户的会话,放入 servlet 上下文中。每次创建会话时,都会以 JSESSIONID 值为 key 将其添加到 Map 中;还会创建一个持久 cookie 来记住 JSESSIONID 值,以便在 JSESSIONID cookie 被销毁后找到会话。
当您关闭 Web 浏览器时,JSESSIONID 将被销毁。但是所有 HttpSession 对象地址都保存在服务器端的 Map 中,您可以使用保存在持久 cookie 中的值访问正确的会话。
首先,在您的 web.xml 部署描述符中添加两个侦听器。
<listener>
<listener-class>
fr.hbonjour.strutsapp.listeners.CustomServletContextListener
</listener-class>
</listener>
<listener>
<listener-class>
fr.hbonjour.strutsapp.listeners.CustomHttpSessionListener
</listener-class>
</listener>
CustomServletContextListener 在上下文初始化时创建一个映射。此地图将注册用户在此应用程序上创建的所有会话。
/**
* Instanciates a HashMap for holding references to session objects, and
* binds it to context scope.
* Also instanciates the mock database (UserDB) and binds it to
* context scope.
* @author Ben Souther; ben@souther.us
* @since Sun May 8 18:57:10 EDT 2005
*/
public class CustomServletContextListener implements ServletContextListener{
public void contextInitialized(ServletContextEvent event){
ServletContext context = event.getServletContext();
//
// instanciate a map to store references to all the active
// sessions and bind it to context scope.
//
HashMap activeUsers = new HashMap();
context.setAttribute("activeUsers", activeUsers);
}
/**
* Needed for the ServletContextListener interface.
*/
public void contextDestroyed(ServletContextEvent event){
// To overcome the problem with losing the session references
// during server restarts, put code here to serialize the
// activeUsers HashMap. Then put code in the contextInitialized
// method that reads and reloads it if it exists...
}
}
CustomHttpSessionListener 在创建时会将会话放入 activeUsers 映射中。
/**
* Listens for session events and adds or removes references to
* to the context scoped HashMap accordingly.
* @author Ben Souther; ben@souther.us
* @since Sun May 8 18:57:10 EDT 2005
*/
public class CustomHttpSessionListener implements HttpSessionListener{
public void init(ServletConfig config){
}
/**
* Adds sessions to the context scoped HashMap when they begin.
*/
public void sessionCreated(HttpSessionEvent event){
HttpSession session = event.getSession();
ServletContext context = session.getServletContext();
HashMap<String, HttpSession> activeUsers = (HashMap<String, HttpSession>) context.getAttribute("activeUsers");
activeUsers.put(session.getId(), session);
context.setAttribute("activeUsers", activeUsers);
}
/**
* Removes sessions from the context scoped HashMap when they expire
* or are invalidated.
*/
public void sessionDestroyed(HttpSessionEvent event){
HttpSession session = event.getSession();
ServletContext context = session.getServletContext();
HashMap<String, HttpSession> activeUsers = (HashMap<String, HttpSession>)context.getAttribute("activeUsers");
activeUsers.remove(session.getId());
}
}
使用基本表单按名称/密码测试用户身份验证。此 login.jsp 表单仅用于测试。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title><bean:message key="formulaire1Title" /></title>
</head>
<body>
<form action="login.go" method="get">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="submit" />
</form>
</body>
</html>
我们去吧。当用户不在会话中时,此 java servlet 将转发到登录页面,并在他处于会话时转发到另一个页面。它仅用于测试持久会话!
public class Servlet2 extends AbstractServlet {
@Override
protected void doGet(HttpServletRequest pRequest,
HttpServletResponse pResponse) throws IOException, ServletException {
String username = (String) pRequest.getParameter("username");
String password = (String) pRequest.getParameter("password");
// Session Object
HttpSession l_session = null;
String l_sessionCookieId = getCookieValue(pRequest, "JSESSIONID");
String l_persistentCookieId = getCookieValue(pRequest, "MY_SESSION_COOKIE");
// If a session cookie has been created
if (l_sessionCookieId != null)
{
// If there isn't already a persistent session cookie
if (l_persistentCookieId == null)
{
addCookie(pResponse, "MY_SESSION_COOKIE", l_sessionCookieId, 1800);
}
}
// If a persistent session cookie has been created
if (l_persistentCookieId != null)
{
HashMap<String, HttpSession> l_activeUsers = (HashMap<String, HttpSession>) pRequest.getServletContext().getAttribute("activeUsers");
// Get the existing session
l_session = l_activeUsers.get(l_persistentCookieId);
}
// Otherwise a session has not been created
if (l_session == null)
{
// Create a new session
l_session = pRequest.getSession();
}
//If the user info is in session, move forward to another page
String forward = "/pages/displayUserInfo.jsp";
//Get the user
User user = (User) l_session.getAttribute("user");
//If there's no user
if (user == null)
{
// Put the user in session
if (username != null && password != null)
{
l_session.setAttribute("user", new User(username, password));
}
// Ask again for proper login
else
{
forward = "/pages/login.jsp";
}
}
//Forward
this.getServletContext().getRequestDispatcher(forward).forward( pRequest, pResponse );
}
MY_SESSION_COOKIE cookie 将保存 JSESSIONID cookie 的值。当 JSESSIONID cookie 被销毁时,MY_SESSION_COOKIE 仍然存在会话 ID。
JSESSIONID 已随 Web 浏览器会话消失,但我们选择使用持久且简单的 cookie,以及放入应用程序上下文中的所有活动会话的映射。持久 cookie 允许我们在地图中找到正确的会话。
不要忘记 BalusC 制作的这些有用的方法来添加/获取/删除 cookie:
/**
*
* @author BalusC
*/
public static String getCookieValue(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
/**
*
* @author BalusC
*/
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
/**
*
* @author BalusC
*/
public static void removeCookie(HttpServletResponse response, String name) {
addCookie(response, name, null, 0);
}
}
最后一个解决方案在 localhost 上使用 glassfish 进行了测试,在 windows 上使用 chrome 用于 webbrowser。它只依赖于一个cookie,你不需要数据库。但实际上,我不知道这种机制的局限性是什么。我只花了一夜的时间来解决这个问题,不知道它是好是坏。
谢谢
我还在学习中,如果我的答案有任何错误,请告诉我。谢谢,@+
正确答案有很多缺陷,请参阅我的评论。事情其实比较简单。您将需要一个持久数据存储(例如 SQL 数据库)。您也可以使用ServletContext
,但用户将在服务器重新启动或应用程序重新部署后注销。HashMap
如果您使用in ,请不要忘记正确同步,ServletContext
因为它可能会被更多线程同时访问。
不要破解服务器的会话和它的 ID,它不在你的控制之下,如果在服务器过期原始会话后出现带有 JSESSIONID 的请求,一些服务器会更改会话 ID。滚动你自己的饼干。
基本上你需要:
javax.servlet.Filter
检查登录过滤器实现可能如下所示:
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// Java 1.8 stream API used here
Cookie loginCookie = Arrays.stream(req.getCookies()).filter(c -> c.getName()
.equals("MY_SESSION_COOKIE")).findAny().orElse(null);
// if we don't have the user already in session, check our cookie MY_SESSION_COOKIE
if (req.getSession().getAttribute("currentUser") == null) {
// if the cookie is not present, add it
if (loginCookie == null) {
loginCookie = new Cookie("MY_SESSION_COOKIE", UUID.randomUUID().toString());
// Store that cookie only for our app. You can store it under "/",
// if you wish to cover all webapps on the server, but the same datastore
// needs to be available for all webapps.
loginCookie.setPath(req.getContextPath());
loginCookie.setMaxAge(DAYS.toSeconds(1)); // valid for one day, choose your value
resp.addCookie(loginCookie);
}
// if we have our cookie, check it
else {
String userId = datastore.getLoggedUserForToken(loginCookie.getValue());
// the datastore returned null, if it does not know the token, or
// if the token is expired
req.getSession().setAttribute("currentUser", userId);
}
}
else {
if (loginCookie != null)
datastore.updateTokenLastActivity(loginCookie.getValue());
}
// if we still don't have the userId, forward to login
if (req.getSession().getAttribute("currentUser") == null)
resp.sendRedirect("login.jsp");
// else return the requested resource
else
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
用户登录后,您应该将 MY_SEESSION_COOKIE 的值与 一起添加到数据存储中,userId
并在注销时将其删除。您还必须将到期日期存储到数据存储并在接受令牌之前对其进行检查,您不得依赖浏览器尊重 maxAge 属性。
并且不要忘记添加一些数据存储清理以防止未完成的 cookie 永远存在。
上面的代码没有在现实生活中测试过,可能会有一些怪癖,但基本的想法应该是可行的。它至少比公认的解决方案好很多。