• Java 实现一个自定义分布式 Session


    HTTP 是无状态协议,所以服务端如果需要记住登录用户,就需要维护一个 SessionId(Cookie) - Session 的键值对。Session 存放用户信息对象。用户信息对象作为 Session 的一个 Attribute。当浏览器请求中包含 Cookie 时,服务器就能识别出具体是哪个用户了。

    默认 SessionId 与 Session 的键值对由服务器来维护,Session 的过期时间默认为 30 分钟(可通过 Debug 查看 maxInactiveInterval 的值)。

    使用 HttpSession

    下面是一个简单的使用 Session 来保存用户登录状态的例子,相关代码我放到了 GitHub

    设置 Attribute(登录时)

    	@PostMapping("/signin")
    	public ModelAndView doSignin(@RequestParam("email") String email, @RequestParam("password") String password, HttpSession session) {
    		try {
    			User user = userService.signin(email, password);
    			session.setAttribute(KEY_USER, user);
    		} catch (RuntimeException e) {
    			return new ModelAndView("signin.html", Map.of("email", email, "error", "Signin failed"));
    		}
    		return new ModelAndView("redirect:/profile");
    	}
    

    获取 Attribute(判断是否已经登录)

    	@GetMapping("/profile")
    	public ModelAndView profile(HttpSession session) {
    		User user = (User) session.getAttribute(KEY_USER);
    		if (user == null) {
    			return new ModelAndView("redirect:/signin");
    		}
    		return new ModelAndView("profile.html", Map.of("user", user));
    	}
    

    删除 Attribute(退出时)

    	@GetMapping("/signout")
    	public String signout(HttpSession session) {
    		session.removeAttribute(KEY_USER);
    		return "redirect:/signin";
    	}
    

    这里的 HttpSession session 可以用 HTTPServletRequest request 代替,此时使用 request.getSession().getAttribute()HttpSession sessionHTTPServletRequest request 可以认为是方法默认就包含的参数。

    Session 的生命周期是半小时,如果半小时后访问时,服务器将重新建立连接,将发送新的 SessionId 到浏览器,再次访问时, 新 Session 中将没有 User,此时登录将失效。

    浏览器 Cookie 样式:

    Cookie: JSESSIONID=C8698B74AFAD403C6E28D77B75373500
    

    此部分代码对应 v1

    使用 Redis

    当存在跨域问题时,即多个服务都需要用到 Session 判断登录状态时,就需要将 Session 在每个服务中复制一份,或做成分布式 Session。一般使用 Redis 实现。

    下面使用 Redis 来维护这个 SessionId - Session 的键值对,或者说维护一个 SessionId - Attributes 的键值对。

    public class BaseController {
    
        final Logger logger = LoggerFactory.getLogger(getClass());
    
        final long EXPIRE_TIME = 1800;
    
        public static HttpServletRequest getRequest() {
            ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            return attrs.getRequest();
        }
    
        protected void setAttribute(String name, Object value) {
            String sessionId = getRequest().getSession().getId();
            Map<String, Object> attributes = new HashMap<>();
            attributes.put(name, value);
            RedisUtils.setKey(sessionId, JsonUtils.getJson(attributes), EXPIRE_TIME, TimeUnit.SECONDS);
        }
    
        protected Object getAttribute(String name) {
            String sessionId = getRequest().getSession().getId();
            String attributesJson = RedisUtils.getKey(sessionId);
            Map<String, Object> attributes = JsonUtils.fromJson(attributesJson, Map.class);
            return attributes.get(name);
        }
    
        protected User getKeyUser(String name) {
            Object user = getAttribute(name);
            return JsonUtils.fromJson(user.toString(), User.class);
        }
    
        protected void removeAttribute(String name) {
            String sessionId = getRequest().getSession().getId();
            String attributesJson = RedisUtils.getKey(sessionId);
            Map<String, Object> attributes = JsonUtils.fromJson(attributesJson, HashMap.class);
            attributes.remove(name);
            RedisUtils.setKey(sessionId, JsonUtils.getJson(attributes), EXPIRE_TIME, TimeUnit.SECONDS);
        }
    
    }
    

    自定义 RedisUtils,使用静态方法

    @Slf4j
    @Component
    public class RedisUtils {
        private static StringRedisTemplate stringRedisTemplate;
    
        @Autowired
        private StringRedisTemplate autowiredStringRedisTemplate;
    
        @PostConstruct
        private void init() {
            stringRedisTemplate = this.autowiredStringRedisTemplate;
        }
    
        public static void setKey(String key, String value, long timeout, TimeUnit unit) {
            stringRedisTemplate.opsForValue().set(addKeyPrefix(key), value, timeout, unit);
        }
    
        public static String getKey(String key) {
            return stringRedisTemplate.opsForValue().get(addKeyPrefix(key));
        }
    
        public static Boolean deleteKey(String key) {
            return stringRedisTemplate.opsForValue().getOperations().delete(addKeyPrefix(key));
        }
        
        public static Long incrementKey(String key) {
            return stringRedisTemplate.opsForValue().increment(addKeyPrefix(key));
        }
    
        private static String addKeyPrefix(String key) {
            return String.format("session:%s", key);
        }
    }
    

    UserController

    public class UserController extends BaseController {	
    	@PostMapping("/signin")
    	public ModelAndView doSignin(@RequestParam("email") String email, @RequestParam("password") String password) {
    		try {
    			User user = userService.signin(email, password);
    			setAttribute(KEY_USER, user);
    		} catch (RuntimeException e) {
    			return new ModelAndView("signin.html", Map.of("email", email, "error", "Signin failed"));
    		}
    		return new ModelAndView("redirect:/profile");
    	}
    
    	@GetMapping("/profile")
    	public ModelAndView profile() {
    		User user = getKeyUser(KEY_USER);
    		if (user == null) {
    			return new ModelAndView("redirect:/signin");
    		}
    		return new ModelAndView("profile.html", Map.of("user", user));
    	}
    
    	@GetMapping("/signout")
    	public String signout() {
    		removeAttribute(KEY_USER);
    		return "redirect:/signin";
    	}
    }
    

    此部分代码对应 v2

    自定义 Session

    上面这种方式实现了一个简单的分布式 Session,我们可以自定义 Session 来对其进行一定优化,使其具有以下特点:

    • 封装 Attribute 的设置与获取的实现细节
    • 可以自定义 Cookie
    • 做一个二级缓存 Attributes,自定义 Session 中存放一份,Redis 再存放一份。

    需要利用下面这几个原生类:

    HttpSession
    HttpServletRequestWrapper
    HttpServletResponseWrapper
    

    设计

    1、设置自定义 Session、Request 和 Response

    public class WrapperSession implements HttpSession {
        private Map<StoreType, SessionStore> sessionStores;
    }
    
    public class WrapperSessionServletRequest extends HttpServletRequestWrapper {
        private WrapperSession wrapperSession;
    }
    
    public class WrapperSessionServletResponse extends HttpServletResponseWrapper {
        private WrapperSession session;
    }
    

    2、使用 session-config.xml 配置 cookie 和 cache,一个 entry 对应一个 SessionConfigEntry。

    <?xml version="1.0" encoding="UTF-8"?>
    <sessionConfig>
        <entries>
            <entry name="sessionId">
                <key>js</key>
                <path>/</path>
                <httponly>true</httponly>
                <readonly>true</readonly>
                <encrypt>false</encrypt>
                <storeType>cookie</storeType>
            </entry>
            
           	<entry name="__user__">
                <storeType>cache</storeType>
                <type>wang.depp.session.entity.User</type> <!--类型用于 String 转换 对象-->
            </entry>
        </entries>
     </sessionConfig>
    
    public class SessionConfigEntry {
        private String name;
        private String key;
        private StoreType storeType;
        private String domain;
        private String path;
    	...
    }
    

    3、使用 CookieStore 存放 Cookie,使用 CacheStore 存放 attributes,默认直接从 CacheStore 中取,CacheStore 从 Redis 缓存中读取。

    public class CacheStore implements SessionStore, SessionCacheContainerAware {
        private final WrapperSessionServletRequest wrapperRequest;
        private volatile Map<String, Object> attributes;
    }
    
    public class CookieStore implements SessionStore {
        private Map<String, String> undecodedCookies = new HashMap<>();
        private Map<String, Attribute> attributes = new HashMap<>();
    }
    

    链路调用

    1、项目启动时根据 session-config.xml 中初始化 SessionConfigEntry

    public class WrapperSessionFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
             WrapperSessionServletRequest krqRequest = new WrapperSessionServletRequest((HttpServletRequest) request);
             WrapperSessionServletResponse krqResponse = new WrapperSessionServletResponse((HttpServletResponse) response);
        }
    }
        public void init() {
            initSessionStore();
            this.sessionId = getSessionId(); // 从 CookieStore 的 attributes 中获取 sessionId
            generateTrackId();
        }
        
        private void initSessionStore() {
            for (SessionStore sessionStore : sessionStores.values()) {
                sessionStore.init(); // 分别调用子类的 init() 方法
            }
        }
    

    2、请求时,拦截,查找 SessionId 在 Redis 是否有对应的 Attributes,设置时先设置到 SessionStore

    public class CacheStore implements SessionStore, SessionCacheContainerAware {
    
        private final WrapperSessionServletRequest wrapperRequest;
    
        private volatile Map<String, Object> attributes;
        
        @Override
        public void setAttribute(SessionConfigEntry sessionConfigEntry, Object value) {
            value = RedisUtils.getKey(wrapperRequest.getSession().getId());; // 设置前,先从 Redis 写入 attributes
            if (null == value) { // 如果不存在,删除
                attributes.remove(sessionConfigEntry.getName());
            } else {
                attributes.put(sessionConfigEntry.getName(), value);  // 如果存在,将更新
            }
        }
    }
    

    3、返回前端前,将 Attributes 更新到 Redis

    public class WrapperSessionServletResponse extends HttpServletResponseWrapper {
        @Override
        public PrintWriter getWriter() throws IOException {
            getSession().commit(); // 延长 session 的时间
            return super.getWriter();
        }
    }
    	@Override
        public void commit() {
            writeToCache();
        }
        
        private void writeToCache() {
            if (attributes.entrySet().size() > 0) {
                ObjectMapper mapper = new ObjectMapper();
                String value = null;
                try {
                    value = mapper.writeValueAsString(attributes);
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
                RedisUtils.setKey(wrapperRequest.getSession().getId(), value, wrapperRequest.getSession().getMaxInactiveInterval());
            }
        }
    

    4、获取时,直接从 SessionStore 中获取,默认将从 Redis 中读取一次,读取后将不再读取,因为以后都就将写入 Attributes

        ...
        @Override
        public Object getAttribute(SessionConfigEntry sessionConfigEntry) {
            loadCache(); // 先从 Redis 写入 attributes,当 readFromCache() 方法调用后,此时将不再从 Redis 中获取。如果当前对象一直存活,直接写入到 attribute,将不用从 Redis 中读取
            return attributes.get(sessionConfigEntry.getName());
        }
    

    使用

    UserController

    public class UserController extends BaseController {
    	@PostMapping("/signin")
    	public ModelAndView doSignin(@RequestParam("email") String email, @RequestParam("password") String password) {
    		try {
    			User user = userService.signin(email, password);
    			setAttribute(KEY_USER, user);
    		} catch (RuntimeException e) {
    			return new ModelAndView("signin.html", Map.of("email", email, "error", "Signin failed"));
    		}
    		return new ModelAndView("redirect:/profile");
    	}
    
    	@GetMapping("/profile")
    	public ModelAndView profile() {
    		User user = (User) getAttribute(KEY_USER);
    		if (user == null) {
    			return new ModelAndView("redirect:/signin");
    		}
    		return new ModelAndView("profile.html", Map.of("user", user));
    	}
    
    	@GetMapping("/signout")
    	public String signout() {
    		removeAttribute(KEY_USER);
    		return "redirect:/signin";
    	}
    }
    

    BaseController

    public class BaseController {
    
        // 获取当前 HttpServletRequest
        public static HttpServletRequest getRequest() {
            ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            return attrs.getRequest();
        }
    
        public static void setAttribute(String name, Object value) {
            getRequest().getSession().setAttribute(name, value);
        }
    
        public static Object getAttribute(String name) {
            return getRequest().getSession().getAttribute(name);
        }
    
        public static void removeAttribute(String name) {
            getRequest().getSession().removeAttribute(name);
        }
    }
    
    

    此部分代码对应 v3

    结语

    自定义分布式 Session 一般实现在网关中,网关接口对外暴露,请求先调用网关,网关请求只能内网访问的业务系统接口。网关和业务系统规定相应的调用规则(如:添加指定 Header),网关来负责验证登录状态。

    Redis 可以实现集群保证可用性。当不使用分布式 Session 时,可以使用 JSON Web Token

  • 相关阅读:
    float转varchar
    我的优化经验:内链是SEO的基础
    转:2008年微软Windows硬件工程(WinHEC)大会
    sql语句去掉前面的0(前导零,零前缀)
    去掉ID重复的数据
    蛙蛙推荐:蛙蛙牌firefox插件
    每日阅读20081127
    网赚经验之谈:成为高手之路
    (chinaz)巧妙选购付费链接
    把某个表的数据导出成insert语句(数据导出 insert语句)
  • 原文地址:https://www.cnblogs.com/deppwang/p/13880290.html
Copyright © 2020-2023  润新知