Session-based (Cookie-Based) Authentication
Table of Contents
1. Session 简介
HTTP 协议是无状态的,浏览器(客户端)向 Web 服务器发起的多个请求之间都是独立的。但有时,我们希望服务器记住客户端的状态,如购物网站记住用户的登录状态。
Session 在是一种用来在客户端与服务器端之间保持状态的解决方案。其基于思路是: 客户端第一次访问时,服务器端生成一个 session id 返回给客户端(当然服务器端也会把这个 session id 存在内存中),客户端得到这个 session id 后,在后续的每个请求中会把这个 session id 传回服务器,这样服务器查询自己内存就知道这个客户端曾经访问过。
Session 可以用来实现用户的登录认证,后文将介绍它。不过 Session 的用处还有很多,比如可以实现统计用户访问次数,这里有一个简单例子:https://docstore.mik.ua/orelly/java-ent/servlet/ch07_05.htm。
说明:服务器端生成的 session id 传回客户端后,往往会保存在 cookie 中,所以 Session-based 认证也称为 Cookie-Based 认证。
2. 基于 Session(Cookie)的认证
下面介绍基于 Session 来实现 REST APIs 认证过程的例子。图 1 (摘自:https://auth0.com/blog/angularjs-authentication-with-cookies-vs-token/ )左子图是基于 Session 实现认证的基本流程(服务器需要保存 session 本身,验证时服务器需要查找 session 是否存在),右子图是基于 Token 实现认证的基本流程(服务并不需要保存 token 本身,token 中包含了过期时间、消息签名等信息,服务器验证一下其有效性即可,可参考 JWT,JSON Web Token)。

Figure 1: 基于 Session(左图)和基于 Token(右图)的认证
2.1. 客户端实现
客户端的步骤很简单:
第一步:登录(获取 session id)。
把用户名和密码,通过 POST 请求发送到认证 url。
如通过 POST 发送数据 { "username": "myuser", "password": "mypassword" } 到 rest/auth/session 。
服务器端验证用户名和密码,如果正确,生成 session 对象,并把其 id(即 session id,它具有很好的随机性)返回给客户端。如,返回类似下面的 json 数据:
{
"session": {
"name":"JSESSIONID",
"value":"9brW9p1hugTVFQo9rbLE0I4Y3SeC0UtweiRtgr"
},
"loginInfo": {
"loginCount":2,
"previousLoginTime":"2014-11-12T08:34:39.812+0000"
}
}
客户端收到服务器返回的 session id 后,把它保存到 cookie 中,以便后续使用。
注:这个过程可以用 Javascript 代码显式地实现,也可以通过下面方法自动实现:服务器在返回登录成功的 HTTP header 中增加下面 Set-Cookie 相关内容:
Set-Cookie: JSESSIONID=9brW9p1hugTVFQo9rbLE0I4Y3SeC0UtweiRtgr; Path=/; HttpOnly
那么,浏览器接受到这个响应报文后就会自动把 session id 保存到 cookie 中。
第二步,客户端在调用 REST API 时,需要在 HTTP 请求报文头的 Cookie 字段中把登录时得到的 session id 附上,传回给服务器。即 HTTP 请求报文头中要包含下面内容:
Cookie: JSESSIONID=9brW9p1hugTVFQo9rbLE0I4Y3SeC0UtweiRtgr
如果客户端通过 jQuery 调用 REST API,则通过配置 xhrFields: { withCredentials: true } 可以实现发送请求时自动附上 Cookie 到请求报文头。如:
$.ajax({
url: your_url,
xhrFields: {
withCredentials: true
}
......
});
服务器会验证这个 session id 关联的 session 对象是否存在于服务器内存中(即检查客户端是否登录过),不存在(未登录)就报错,存在就通过。
第三步,客户端登出。
发送 DELETE 请求(请求报文头的 Cookie 字段中有 session id)到认证 url,如 rest/auth/session 。
2.2. 服务器端实现
下面介绍一个服务器端认证相关 REST API 的实现。
login url 后端响应的实现:
@Path("/rest/auth")
public class AuthService {
private final String userID = "admin";
private final String password = "password";
@POST
@Path("/session")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response login(@Context HttpServletRequest req,
UserCredential userCredential) { // userCredential保存着前端传过来的json数据
String username = userCredential.getUsername(); // 用户在前端输入的用户名
String password = userCredential.getPassword(); // 用户在前端输入的密码
if (username.equals(userID) && password.equals(password)) { // 一般要去数据库验证用户名这密码是否正确,这里为简单只允许admin/password通过
// create session
HttpSession session = Req.getSession(); // getSession()找不到session时就会在服务器创建一个session,如果创建新session则会自动把session id放入响应报文头的Set-Cookie字段中
session.setAttribute("user", userName);
// set session to be expired in 30 minutes
session.setMaxInactiveInterval(30 * 60);
String sessionId = session.getId()
// 返回包含session id的json数据给前端
} else {
// 返回用户名或密码不匹配的错误
}
}
}
logout url 后端响应的实现:
@DELETE
@Path("/session")
@Consumes(MediaType.APPLICATION_JSON)
public void logout(@Context HttpServletRequest req) {
HttpSession session = req.getSession(false);
// getSession(false)中false是意思是找不到session就返回null,而不是创建新session
// getSession由web服务器实现,它的具体过程大致为:首先去客户端的请求报文中查找session id(往往在Cookie中查找),
// 然后在web服务器内存中查找以这个session id为key的session对象是否存在,存在就返回它。
// invalidate the session if exists
if (session != null) {
String userName = (String) session.getAttribute("user");
session.invalidate(); // 关键步骤,让session失效!
logger.info("{} logout.", userName);
} else {
logger.warn("No session found");
}
}
所有的 REST APIs 可以通过在 web.xml 中配置下面的 filter 来确保用户登录后才能使用:
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AuthFilter implements Filter {
private final static Logger logger = LoggerFactory.getLogger(AuthFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String uri = req.getRequestURI();
HttpSession session = req.getSession(false);
// getSession(false)中false是意思是找不到session就返回null,而不是创建新session
// getSession由web服务器实现,它的具体过程大致为:首先去客户端的请求报文中查找session id(往往在Cookie中查找),
// 然后在web服务器内存中查找以这个session id为key的session对象是否存在,存在就返回它。
if (uri.endsWith("/rest/auth/session")) {
// 请求 /rest/auth/session 时(登录时),可能没有认证,这时显然没有session,应该放过检查
chain.doFilter(request, response);
} else {
// 如果在服务器端找不到相应session,说明还没有登录过,返回登录页面
if (session == null) {
logger.warn("Unauthorized access request, uri={}", uri);
res.sendRedirect("/login.html");
} else {
chain.doFilter(request, response);
}
}
}
@Override
public void destroy() {
}
}
2.3. Secure 和 HttpOnly 标记
服务器返回的 Set-Cookie 报文头中可以为 Cookie 指定标记 Secure 或者 HttpOnly ,如:
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
如果指定了标记 Secure ,那么浏览器(从 Chrome 52 和 Firefox 52 开始),对 http 的网站不会设置 Cookie,只对安全的(即启用了 https)网站才设置 Cookie。
如果指定了标记 HttpOnly ,那么这个 Cookie 将不能通过 JavaScript 的 Document.cookie API 访问(也就是说客户端无法直接读取 Cookie 中的内容了),当然它们可以被浏览器自动送回服务器。
参考:HTTP cookies
3. Tips
3.1. 查看 Web 服务器创建的 Session
应用代码中,在 HttpServletRequest 对象上调用 getSession() 后,如果 Web 服务器找不到当前 HttpServletRequest 对象关联的 Session 对象,那么就会创建一个新的 Session 对象。
如何查看 Web 服务器中有多少个有效的 Session 对象呢?下面以 Tomcat 7 为例进行说明。
首先打开 jconsole ,连接上 Tomcat 进程,切换到 MBeans 标签下,然后按图 2 所示进行展开后,可找到 activeSessions 属性,它就是当前有效的 Session 数量。除此外,我们还可以查看所有的 session id,使用操作 listSessionIds (图 2 中左边树的最后一行)即可。

Figure 2: Tomcat 7 中查看有效的 Session 对象(activeSessions 对应的数字,这个例子中为 2)
参考:https://stackoverflow.com/questions/4069444/getting-a-list-of-active-sessions-in-tomcat-using-java