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)。

http_session_cookie_auth.png

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 中左边树的最后一行)即可。

http_session_tomcat_view_sessions.gif

Figure 2: Tomcat 7 中查看有效的 Session 对象(activeSessions 对应的数字,这个例子中为 2)

参考:https://stackoverflow.com/questions/4069444/getting-a-list-of-active-sessions-in-tomcat-using-java

Author: cig01

Created: <2017-03-01 Wed>

Last updated: <2020-03-03 Tue>

Creator: Emacs 27.1 (Org mode 9.4)