为什么使用session
HTTP协议是无状态的,每次客户端想要与服务端通信,都必须与服务端连接,这意味着:请求一次,客户端与服务端就连接一次,下一次的请求与上一次的请求没有关系。这种无状态的方式存在一个问题:怎么判断两次请求是同一个人?因此,需要通过session、cookie或token方式,进行用户判断、鉴权。
session引入方案
session保存在服务器端,如果同一时间太多用户访问服务器,则会导致服务器端的内存压力过大。 因此,我们考虑使用redis,redis用于缓存用户的session,后期redis也可以通过主从模式进行拓展。 此外,我们采用sessionId,前后端的请求通过该字段来识别用户的身份。
具体地,我们会在filter中截取用户的请求数据,在interceptor中判断用户的身份,并保存对应数据在request中,方便后续的后端操作。 其中,filter中截取用户数据可以参考https://blog.nowcoder.net/n/8dc8fb4c78994d30a6439125d1c78001。
拦截器改造
在interceptor中,我们会使用2个拦截器,其中一个拦截器是获取数据,解析数据成JSONObject对象,另一个拦截器则会判定用户的登录情况。
引入JsonInterceptor
具体地,在JsonInterceptor中,也就是第一个拦截器中,我们通过fastJson对前端请求的数据进行解析,再存入到request中。具体代码如下:
public class JsonInterceptor implements HandlerInterceptor {
/**
* 读取请求
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String body = ((RequestWrapper) request).getBody();
JSONObject jsonObject;
try {
// 前后端交互要求为json字符串
jsonObject = JSONObject.parseObject(body);
}catch (Exception e){
throw new AppTransException(ErrorEnum.REQUEST_JSON_ERROR);
}
// 获取header
// 获取交互数据的userSessionId字段,并在request中进行设置,若无,则为""
JSONObject headerParams = jsonObject.getJSONObject(RequestConstant.HEADER);
headerParams = Optional.ofNullable(headerParams).orElse(new JSONObject());
Object userSessionId = headerParams.getOrDefault(RequestConstant.USER_SESSION_ID, "");
// 设置的userSessionId属性在LoginInterceptor中使用
request.setAttribute(RequestConstant.USER_SESSION_ID, userSessionId);
// 获取body,body的数据用于dto的赋值使用
JSONObject bodyParams = jsonObject.getJSONObject(RequestConstant.BODY);
bodyParams = Optional.ofNullable(bodyParams).orElse(new JSONObject());
request.setAttribute(RequestConstant.BODY, bodyParams);
return true;
}
}
引入LoginInterceptor
在第二个拦截器中,我们会对请求数据的userSessionId进行解析,判断这属于无状态的请求、有状态的请求,再执行相关的代码逻辑。
其中,无状态的请求(不需要用户登录也可完成)则会直接返回true,有状态的请求包含两种,(1)登录请求;(2)非登录请求。对于登录请求,我们会新生成一个userSessionId,而非登录请求,我们会判断session是否仍在有效期内,若不在有效期内,则会抛用户登录已过期的相关错误。 具体的代码如下:
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 存储NO_SESSION的URL,这些并不需要通过校验
*/
private static final Set<String> NO_SESSION_URI = new ConcurrentSkipListSet<>();
static {
NO_SESSION_URI.add("/upload/img");
NO_SESSION_URI.add("/upload/file");
}
/**
* 对未登录的请求进行阻断,
* 非session请求可以通过
* 登录请求会设置userSessionId
* 非登录请求会刷新userSessionId
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
// 判断是否为NO_SESSION的请求路径,是的话直接返回true
if(isNoSessionRequest(requestURI)){
return true;
}
// 判断是否为NEW_SESSION的请求路径,如果是,则需要赋值新的userSessionId,以及设置新的sessionCtx,然后返回true
if(isLoginRequest(requestURI)){
String userSessionId = RequestUtils.genNewUserSessionId(RequestConstant.SESSION_ID_LENGTH, RequestConstant.USER_SESSION_ID + "_");
// 设置context,该context用于储存数据
SessionCtx ctx = new SessionCtx();
request.setAttribute(RequestConstant.USER_SESSION_ID, userSessionId);
request.setAttribute(RequestConstant.SESSION_CONTEXT, ctx);
RedisUtils.setValueByKeyExpired(userSessionId, RedisPrefixEnum.SESSION_PREFIX, RequestConstant.SESSION_TIMEOUT, ctx);
return true;
}
// 判断当前的登录时间是否过期,未过期则重新赋值登录时间,然后返回true,否则直接返回false
String userSessionId = request.getAttribute(RequestConstant.USER_SESSION_ID).toString();
// 如果没有userSessionId,则抛session不存在异常
if(StringUtil.isEmpty(userSessionId)){
throw new AppTransException(ErrorEnum.SESSION_TIMEOUT, "用户未登录,请重试");
}
SessionCtx ctx = RedisUtils.getValueByKey(userSessionId, RedisPrefixEnum.SESSION_PREFIX, SessionCtx.class);
// ctx不存在,则表明登录已过期
if(null == ctx){
throw new AppTransException(ErrorEnum.SESSION_TIMEOUT);
}
request.setAttribute(RequestConstant.SESSION_CONTEXT, ctx);
// 发起请求后,重新设置过期时间
RedisUtils.setValueByKeyExpired(userSessionId, RedisPrefixEnum.SESSION_PREFIX, RequestConstant.SESSION_TIMEOUT, ctx);
return true;
}
/**
* 判断是否为登录请求
* @param requestURI
* @return
*/
private boolean isLoginRequest(String requestURI){
return RequestUrlEnum.LOGIN_REQUEST.getValue().equals(requestURI);
}
/**
* 判断是否为noSession请求,这些请求并不需要userSessionId
* @param requestURI
* @return
*/
private boolean isNoSessionRequest(String requestURI){
return requestURI != null && NO_SESSION_URI.contains(requestURI);
}
}
在这里,我们使用sessionCtx来保存每一个用户在服务器端存储的数据,并存储到redis中,由redis进行管理。
具体的效果:
在登录后,我们会将userSessionId发送给前端。前端在获取userSessionId后,之后的请求则会夹带这个userSessionId,来表明是由该用户发起的请求。
总结
我们使用session充当用户的交互数据。此外,我们引用redis,将session数据存储在redis中,方便后期的交互、使用。