微服务架构下的session一致性
Posted SpringCloud社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务架构下的session一致性相关的知识,希望对你有一定的参考价值。
本文由宜信-高级架构师-梁鑫投稿,之前在社区分享过两篇文章,分别介绍了一下在公司项目中搭建springcloud框架的经验和我们自己研发的几个微服务组件。在这个过程中,我们还需要解决微服务架构中特别需要注意的一个问题————session一致性。在此,抱着学习的态度把我的解决方案跟大家再次分享一下。
一.背景.绕不开的session一致性
采用微服务架构以后,把原先单一的节点拆解成了多个微服务节点。在采用微服务架构之前,我们的项目普遍采用的都是分布式集群架构,多数的公司项目都采用IP哈希的方式进行session的跟踪,这样做非常简单,只需要在nginx简单配置即可,但我们采用springcloud微服务架构之后,session一致性保持就成了我们必须要解决的问题。
二.Session一致性的常见方式
简单说一下session和session一致性。服务为访问他的用户构造了一组信息,称之为会话(session),当该用户在限定时间内每次发起http访问时,服务端能自动感知到是该用户在发起访问,称之为会话保持(session一致性)
2.1、IP哈希
2.2、Session复制
把每个用户的session都同步复制到集群中的每一个服务节点,这样无论用户访问哪个服务节点,都能获取到自己的session信息。
2.3、Session客户端存储
把session信息保存到客户端的cookie中。
2.4、Session分布式存储
把session信息保存到后端的其它存储中,例如mysql,redis,memcached等。
三.微服务架构下的session一致性
3.1 原理图
3.2 基于shiro的session
我们采用了shiro作为权限控制组件;
...
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setCacheManager(redisCacheManager());
sessionManager.setGlobalSessionTimeout(60000);
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionIdCookie(simpleCookie());
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(redisCacheManager());
securityManager.setRealm(getShiroRealm());
return securityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
Map<String, String> chains = new LinkedHashMap<String, String>();
chains.put("/autoconfig", "anon");
chains.put("/beans", "anon");
chains.put("/configprops", "anon");
chains.put("/dump", "anon");
chains.put("/env", "anon");
chains.put("/health", "anon");
chains.put("/info", "anon");
chains.put("/metrics", "anon");
chains.put("/mappings", "anon");
chains.put("/shutdown", "anon");
chains.put("/trace", "anon");
shiroFilter.setFilterChainDefinitionMap(chains);
return shiroFilter;
}
...
构造ShiroUser作为session的主要保存信息,用户的ID,账号,名称等;
public class ShiroUser implements Serializable {
private static final long serialVersionUID = -4661753370573516137L;
private Integer id; // 主键ID
private String username; // 账号
private String name; // 姓名
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
集成公司ldap;
/**
* 建立连接
*/
public void connect() {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
env.put(Context.PROVIDER_URL, URL + BASEDN);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, ldapUserName); // 管理员
env.put(Context.SECURITY_CREDENTIALS, ldapPassword); // 管理员密码
try {
ctx = new InitialLdapContext(env, connCtls);
logger.info("连接成功");
} catch (AuthenticationException e) {
logger.error("连接失败", e);
}
}
/**
* 用户验证
*/
public boolean validate(String userName, String password) {
try {
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userName);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
ctx.reconnect(null);
return true;
} catch (AuthenticationException e) {
logger.error("连接失败", e);
}
return false;
}
3.3 采取redis存储session
加入redis存储,构造RedisCacheManager;
public class RedisCacheManager implements CacheManager {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return new GantryCache<K, V>(name, redisTemplate);
}
...
}
构造RedisSessionDAO,通过dao对象最终对redis进行操作,需要包含redisTemplate对象;
@Component
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {
private static int expireTime = 1800;
private static String prefix = "gantry-session:";
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 创建session,保存到数据库
*/
@Override
protected Serializable create(Session session) {
redisTemplate.opsForValue().set(prefix + sessionId.toString(), session);
return sessionId;
}
/**
* 获取session
*/
@Override
protected Session query(Serializable sessionId) {
if (session == null) {
session = (Session) redisTemplate.opsForValue().get(prefix + sessionId.toString());
}
return session;
}
/**
* 更新session的最后一次访问时间
*/
@Override
protected void uppdate(Session session) {
String key = prefix + session.getId().toString();
if (!redisTemplate.hasKey(key)) {
redisTemplate.opsForValue().set(key, session);
}
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
}
/**
* 删除session
*/
@Override
protected void delete(Session session) {
redisTemplate.delete(prefix + session.getId().toString());
}
}
Shiro的SessionManager对象支持注入RedisSessionDAO对象,从而使用redis存储session;
...
@Resource
private RedisSessionDAO sessionDAO;
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
...
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
3.4 构建cookie
在服务端创建cookie,注意cookie的httpOnly属性,只有当httpOnly属性设置为false时,才能通过javascript获取cookie值;
...
@Bean
public SimpleCookie simpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie("sid");
simpleCookie.setHttpOnly(false);
simpleCookie.setMaxAge(-1);
simpleCookie.setName("GANTRYSESSIONID");
return simpleCookie;
}
...
在javascript中获取cookie;
...
function getCookie(cookieName) {
var strCookie = document.cookie;
var arrCookie = strCookie.split("; ");
for (var i = 0; i < arrCookie.length; i++) {
var arr = arrCookie[i].split("=");
if (cookieName == arr[0]) {
return arr[1];
}
}
return "";
}
...
3.5 微服务节点间sessionid的传递
在cookie中获取sessionid,在url中拼接jsessionid;
...
function openNewService(url) {
var id = getCookie("GANTRYSESSIONID")
window.open(url + ";jsessionid=" + id);
}
...
3.6 在微服务节点B接收sessionid并在redis中获取session
获取并保存sessionid;
...
@RequestMapping("/")
public String index(HttpServletRequest request) {
String sessionId = request.getRequestedSessionId();
request.getSession().setAttribute("SHIROSESSIONID", sessionId);
return "index.html";
}
...
通过sessionid从redis中获取session;
...
@Autowired
private RedisCacheManager redisCacheManager;
@RequestMapping(value = "/someMethod", produces = "application/json;charset=UTF-8")
@ResponseBody
public String someMethod(HttpServletRequest request) throws IOException {
String SHIROSESSIONID = (String) request.getSession().getAttribute("SHIROSESSIONID");
String sessionId = request.getRequestedSessionId();
if (redisCacheManager == null) {
...
}
Object shiroCacheObject = redisCacheManager.getCache("*");
if (shiroCacheObject == null) {
...
}
ShiroCache shiroCache = (ShiroCache) shiroCacheObject;
Object sessionObject = shiroCache.get(SHIROSESSIONID);
if (sessionObject == null) {
...
}
Session session = (Session) sessionObject;
ShiroUser user = (ShiroUser) session.getAttribute("user");
...
}
...
以上是关于微服务架构下的session一致性的主要内容,如果未能解决你的问题,请参考以下文章