由于单点环境下,session直接存储在同一台服务下,用户登录直接获取session是没什么问题。但是在集群环境下,还是这种做法的话,由于session存储在不同服务上。假设有A和B两台服务器做成集群,它们负载均衡,如果登录请求是在A服务器下进行的,A服务下做保存session的操作,而后续的请求到B服务下,这时在B服务上获取不到对应的session(因为session是保存在A服务下),所以这时候就会提示用户未登录,这样对用户是不友好的。
目前解决这种单点方式有很多,比如通过nginx的Ip hash,根据hash值分配用户只能请求某台服务器,但是这种做法是有缺陷的,因为根据这种计算结果,并不会均衡的分配请求。当然也可以使用spring session框架来零侵入的实现单点登录问题等。
这里,我用的是jedis+filter+cookie+json 来原生实现单点登录。
首先,用户登陆的时候,在cookie中写入相应的信息:
public static void writeLoginToken(HttpServletResponse response, String token){ Cookie ck = new Cookie(COOKIE_NAME,token); ck.setDomain(COOKIE_DOMAIN); ck.setPath("/");//代表设置在根目录 ck.setHttpOnly(true); //单位是秒。 //如果这个maxage不设置的话,cookie就不会写入硬盘,而是写在内存。只在当前页面有效。 ck.setMaxAge(60 * 60 * 24 * 365);//如果是-1,代表永久 log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); response.addCookie(ck); }然后,将当前sessionId写入到redis中
RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);这里redis的超时时间为1800秒即30分钟。
这样,当下一次,用户访问B服务器的时候,可以通过读取我们写入cookie中的值,来获取token的值。
public static String readLoginToken(HttpServletRequest request){ Cookie[] cks = request.getCookies(); if(cks != null){ for(Cookie ck : cks){ log.info("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); if(StringUtils.equals(ck.getName(),COOKIE_NAME)){ log.info("return cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); return ck.getValue(); } } } return null; }最后通过该token,去redis获取用户的信息。整体逻辑如下:
public ServerResponse addCategory(HttpServletRequest httpServletRequest, String categoryName, @RequestParam(value = "parentId", defaultValue = "0") int parentId) { String loginToken = CookieUtil.readLoginToken(httpServletRequest); if (StringUtils.isEmpty(loginToken)) { return ServerResponse.createByErroMessage("用户未登陆,无法获取到用户的个人信息"); } String userStr = RedisPoolUtil.get(loginToken); User user = JsonUtil.string2Obj(userStr, User.class); if (user == null) { return ServerResponse.createByCodeErroMessage(ResponseCode.NEED_LOGIN.getCode(), "用户未登录"); } ServerResponse checkResult = userService.checkAdmin(user); if (!checkResult.isSuccess()) { return ServerResponse.createByErroMessage("当前用户不是管理员,无权进行此操作"); } return categoryService.addCategory(categoryName, parentId); }但是到里,我们只是设置了token的过期时间,但实际的过程中,用户一旦有活动,都需要重置token过期时间,所以需要些个拦截器来重置过期时间:
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String loginToken = CookieUtil.readLoginToken(httpServletRequest); if (StringUtils.isNotEmpty(loginToken)) { String userStr = RedisPoolUtil.get(loginToken); User user = JsonUtil.string2Obj(userStr, User.class); if (user != null) { RedisPoolUtil.expire(loginToken, 60 * 30); } } filterChain.doFilter(servletRequest, servletResponse); }以上就是使用redis来解决单点登陆的问题,token值也可以使用自定义的uuid,只要有写入cookie当中就可以。
具体实现可参看个人github: https://github.com/Mrfirewind/mmall_learning