一. 为什么要使用Cookie,Session或者Token
- http协议是无状态的,对于服务器而言,每次请求都是一个全新的请求
- 实际应用中,服务器要验证请求对应的用户信息,请求是否合法等
- 可以通过Cookie和Session验证用户信息
- 也可以通过token验证用户身份
二. Cookie和Session关联
http_cookie_session.png
- Session保存在服务器中,用于记录会话,而Cookie保存在浏览器中,用于跟踪服务器的会话
- 每个session都有一个Id,新建session的时候,服务器会自动生成一个sessionid
- 服务器设置Set-Cookie响应头,将sessionid返回给浏览器
- 浏览器根据Set-Cookie响应头,新建一个Cookie,name为jsessionid(Java Servlet默认值,也可以自定义),value为session的id
- 浏览器的每次请求,都会携带Cookie值
- 服务器根据jsessionid这个Cookie找到对应的session,获取用户信息,判断请求是否合法
- 如果浏览器禁用了Cookie,那么可以通过URL重定向的方式,将SessionID编码到URL中,获取服务器Session
1. Tomcat Session和Cookie源码剖析
1.1 Tomcat Request类
主要是通过cookie获取session,如果session不存在的话,则创建一个
//Tomcat Request类
public class Request implements HttpServletRequest {
/**
* 获取请求对应的Session
* 如果session不存在,则创建一个
*/
@Override
public HttpSession getSession() {
Session session = doGetSession(true);
if (session == null) {
return null;
}
return session.getSession();
}
/**
* 获取Session
* @param create 是否创建
*/
protected Session doGetSession(boolean create) {
//获取请求上下文
Context context = getContext();
if (context == null) {
return null;
}
if ((session != null) && !session.isValid()) {
//session过期了
session = null;
}
if (session != null) {
return session;
}
Manager manager = context.getManager();
if (manager == null) {
return null;
}
if (requestedSessionId != null) {
//请求携带的requestedSessionId,也就是Cookie中的jsessionid
try {
//ManagerBase对象中获取Session
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
session = null;
}
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
session.access();
return session;
}
}
if (!create) {
return null;
}
if (response != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)
&& response.getResponse().isCommitted()) {
throw new IllegalStateException(
sm.getString("coyoteRequest.sessionCreateCommitted"));
}
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
} else if (("/".equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie())) {
//从Cookie中取出SessionId
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
for (Container container : getHost().findChildren()) {
Manager m = ((Context) container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException e) {
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
sessionId = null;
}
session = manager.createSession(sessionId);
//生成一个关联Session的Cookie
if (session != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)) {
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
//将cookie添加到response中
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
session.access();
return session;
}
}
1.2 Tomcat ManagerBase类
维护了所有的session
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
//保存sessiond的map
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
/**
* 通过sessionid获取session
*/
public HashMap<String, String> getSession(String sessionId) {
//从map中获取
Session s = sessions.get(sessionId);
if (s == null) {
if (log.isInfoEnabled()) {
log.info(sm.getString("managerBase.sessionNotFound", sessionId));
}
return null;
}
Enumeration<String> ee = s.getSession().getAttributeNames();
if (ee == null || !ee.hasMoreElements()) {
return null;
}
//将session中的值放到另一个map中
//并返回新的map
HashMap<String, String> map = new HashMap<>();
while (ee.hasMoreElements()) {
String attrName = ee.nextElement();
map.put(attrName, getSessionAttribute(sessionId, attrName));
}
return map;
}
}
1.3 tomcat Response类
主要通过过Set-Cookie请求头设置浏览器Cookie,还可以设置Cookie中维护的SessionId
/**
* tomcat Response类
*/
public class Response implements HttpServletResponse {
/**
* 添加cookie到浏览器
*/
@Override
public void addCookie(final Cookie cookie) {
if (included || isCommitted()) {
return;
}
cookies.add(cookie);
String header = generateCookieString(cookie);
//设置Set-Cookie响应头
addHeader("Set-Cookie", header, getContext().getCookieProcessor().getCharset());
}
/**
* 将cookie添加到response中
*/
public void addSessionCookieInternal(final Cookie cookie) {
if (isCommitted()) {
return;
}
String name = cookie.getName();
//在Set-Cookie响应头中设置cookie
final String headername = "Set-Cookie";
final String startsWith = name + "=";
String header = generateCookieString(cookie);
boolean set = false;
MimeHeaders headers = getCoyoteResponse().getMimeHeaders();
int n = headers.size();
for (int i = 0; i < n; i++) {
if (headers.getName(i).toString().equals(headername)) {
if (headers.getValue(i).toString().startsWith(startsWith)) {
headers.getValue(i).setString(header);
set = true;
}
}
}
if (!set) {
addHeader(headername, header);
}
}
//如果浏览器禁用了Cookie
//那么可以使用该方法,将sessionId编码到URL中
@Override
public String encodeRedirectURL(String url) {
if (isEncodeable(toAbsolute(url))) {
return toEncoded(url, request.getSessionInternal().getIdInternal());
} else {
return url;
}
}
}
三. Token的使用
Token又被称为令牌,主要用于校验用户身份,也是我们当前推荐使用的一种认证方式
Token相比于Cookie的优势
- 无状态,服务器上不需要存储用户对应的Session数据,Token可以包含用户信息
- 支持跨域访问,Cookie默认是不支持跨域访问,允许跨域可能会导致CSRF攻击
- 解耦,将授权服务和应用服务解耦,只要token正确
- 安全性更高,可以采用各种加密方式来加密token,也可以有效避免CSRF攻击
- 有利于前后端分离,H5,APP各个应用都可以使用同一的授权验证模块