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