java 分布式会话_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 session 和 HTTPServletRequest 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 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 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 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 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。

js

/

true

true

false

cookie

cache

wang.depp.session.entity.User

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 attributes;

}

public class CookieStore implements SessionStore {

private Map undecodedCookies = new HashMap<>();

private Map 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 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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值