saas分布式session实现方式
在用户登录时,为当前登录用户生成一个全局唯一的token,并通过redis以这个token为 key登录用户信息为value保存到redis,并将token写到域名下的cookies中达到分布式session共享。
具体实现时序图如下:

前端请求访问处理流程:

分布式session实现类图:

主要类讲解
1.分布式session管理接口:TokenManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
public interface TokenManager<U> {
String createAndSaveToken(U userInfo);
void extendUserToken();
void loginOff();
U getUserByToken(Class<U> clazz);
default U getAndExtendUserByToken(Class<U> clazz){ U currentUser = getUserByToken(clazz); saveCurrentLoginUser(currentUser); extendUserToken(); return currentUser; }
void saveCurrentLoginUser(U currentUser); }
|
2.用户登录处理扩展接口:TokenUserLoginHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public interface TokenUserLoginHandler<U> {
default String loginAndCreateToken(U loginUser){ return StringConvertUtils.shortUuid(); }
default int extend(U loginUser){ return 0; }
default void loginOff(U loginUser){} }
|
3.redis实现分布式session配置类:RedisTokenManagerConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "openerp.auth.token") public class RedisTokenManagerConfig {
private String keyPrefix = "openerp:token:";
private int expires = 60 * 60 ;
private String cookieKey = "saasToken";
public String getKeyPrefix() { return keyPrefix; }
public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; }
public int getExpires() { return expires; }
public int getExpires(String token) { return token.contains(AuthorizationConstants.IS_APP_KEY) ? expires * 24 * 7 : expires; }
public void setExpires(int expires) { this.expires = expires; }
public String getCookieKey() { return cookieKey; }
public void setCookieKey(String cookieKey) { this.cookieKey = cookieKey; } }
|
4.redis实现分布式session类: RedisTokenManager
获取当前请求上下文中的token:先从请求参数中获取,再从header中获取,最后从cookie中获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
| import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Objects;
public class RedisTokenManager<U> implements TokenManager<U> {
private Logger logger = Logger.getLogger(RedisTokenManager.class);
private AbstractRedisCache redisUtils; private RedisTokenManagerConfig tokenManagerConfig; private ThreadLocal<U> currentLoginUser = new ThreadLocal<>(); private TokenUserLoginHandler<U> tokenUserLoginHandler;
public RedisTokenManager(AbstractRedisCache redisUtils, RedisTokenManagerConfig tokenManagerConfig) { this( redisUtils,tokenManagerConfig,new TokenUserLoginHandler<U>(){}); }
public RedisTokenManager(AbstractRedisCache redisUtils, RedisTokenManagerConfig tokenManagerConfig, TokenUserLoginHandler<U> tokenUserLoginHandler) { this.redisUtils = redisUtils; if (tokenManagerConfig == null) { tokenManagerConfig = new RedisTokenManagerConfig(); } this.tokenManagerConfig = tokenManagerConfig; this.tokenUserLoginHandler = tokenUserLoginHandler; }
@Override public void extendUserToken() { String token = getToken(); String tokenKey = createTokenKey(token); if (redisUtils.exists(tokenKey)) { U u = getCurrentLoginUser(); if (Objects.nonNull(u)){ int extendTime = this.tokenUserLoginHandler.extend(u); redisUtils.expire(tokenKey, extendTime); } else { redisUtils.expire(tokenKey, tokenManagerConfig.getExpires()); } saveToCookie(token); } }
@Override public void loginOff() { String token = getToken(); if (StringUtils.isNotBlank(token)){ String tokenKey = createTokenKey(token); loginOff(tokenKey); clearCookieToken(); U u = getCurrentLoginUser(); if (Objects.nonNull(u)){ this.tokenUserLoginHandler.loginOff(u); saveCurrentLoginUser(null); } } }
private void loginOff(String token) { String tokenKey = createTokenKey(token); redisUtils.del(tokenKey); }
@Override public U getUserByToken(Class<U> clazz) { String token = getToken(); String tokenKey = createTokenKey(token); String userStr = redisUtils.get(tokenKey, ""); if (StringUtils.isNotBlank(userStr)) { return JSONObject.parseObject(userStr, clazz); } return null; }
@Override public String createAndSaveToken(U userInfo) { try { String token = createToken(userInfo); saveToCookie(token); return token; } catch (Exception e) { logger.error("token异常createAndSaveToken():" + e); throw e; } }
private void saveToCookie(String token) { logger.info("开始创建token:" + token); HttpServletResponse resp = getResponse(); Cookie cookie = new Cookie(tokenManagerConfig.getCookieKey(), token);
cookie.setMaxAge(tokenManagerConfig.getExpires(token)); cookie.setPath("/"); resp.addCookie(cookie); }
private void clearCookieToken() { HttpServletResponse resp = getResponse(); Cookie cookie = new Cookie(tokenManagerConfig.getCookieKey(), ""); cookie.setMaxAge(0); cookie.setPath("/"); resp.addCookie(cookie); }
private String createTokenKey(String token) { return tokenManagerConfig.getKeyPrefix() + token; }
private String createToken(U userInfo) { Objects.requireNonNull(userInfo, "用户信息不能为空"); String token = this.tokenUserLoginHandler.loginAndCreateToken(userInfo); String tokenKey = createTokenKey(token); logger.info("用户登录信息缓存{}:"+JSONObject.toJSONString(userInfo)); redisUtils.set(tokenKey, JSONObject.toJSONString(userInfo), tokenManagerConfig.getExpires(token)); return token; }
private HttpServletResponse getResponse() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); }
private HttpServletRequest getRequest() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); }
private String getToken() { HttpServletRequest request = getRequest();
String token = request.getParameter(tokenManagerConfig.getCookieKey()); if (StringUtils.isBlank(token)) { token = request.getHeader(tokenManagerConfig.getCookieKey()); } if (StringUtils.isBlank(token)) { token = getCookie(request, tokenManagerConfig.getCookieKey()); }
return token;
}
private String getCookie(HttpServletRequest request, String cookieKey) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return ""; } for (Cookie cookie : cookies) { if (cookie.getName().equals(cookieKey)) { return cookie.getValue(); } }
return ""; }
@Override public void saveCurrentLoginUser(U currentUser) { currentLoginUser.set(currentUser); }
private U getCurrentLoginUser(){ return currentLoginUser.get(); } }
|
创建并保存token:生成token,保存登录用户信息到redis中和保存token到cookie中
1 2 3 4 5 6 7 8 9 10 11
| @Override public String createAndSaveToken(U userInfo) { try { String token = createToken(userInfo); saveToCookie(token); return token; } catch (Exception e) { logger.error("token异常createAndSaveToken():" + e); throw e; } }
|
3.禁止用户多地登录扩展处理器:DuplicateCheckUserLoginHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils;
@Slf4j public class DuplicateCheckUserLoginHandler implements TokenUserLoginHandler<AuthUser> { private AbstractRedisCache redisUtils; private RedisTokenManagerConfig tokenManagerConfig; private final String userTokenRedisKeyTemplate = "logincheck:"; private final String appUserTokenRedisKeyTemplate = "app:logincheck:";
public DuplicateCheckUserLoginHandler(AbstractRedisCache redisUtils, RedisTokenManagerConfig tokenManagerConfig) { this.redisUtils = redisUtils; this.tokenManagerConfig = tokenManagerConfig; }
@Override public String loginAndCreateToken(AuthUser loginUser) { String loginKey = getRedisKey(loginUser.getUserId(), loginUser.isApp()); String token = redisUtils.get(loginKey,""); if (StringUtils.isNotBlank(token)){ redisUtils.del(createTokenKey(token)); log.info(String.format("用户%s对应的token[%s]被强制登出",loginUser.getUserName(),token)); } String newToken = StringConvertUtils.shortUuid(); if (loginUser.isApp()){ newToken = AuthorizationConstants.IS_APP_KEY + newToken; } redisUtils.set(loginKey,newToken,getExpires(loginKey)); return newToken; }
@Override public int extend(AuthUser loginUser) { String loginKey = getRedisKey(loginUser.getUserId()); if (redisUtils.exists(loginKey)) { redisUtils.expire(loginKey, getExpires(loginKey)); } return loginUser.isApp() ? tokenManagerConfig.getExpires() * 24 * 7 : tokenManagerConfig.getExpires(); }
@Override public void loginOff(AuthUser loginUser) { String loginKey = getRedisKey(loginUser.getUserId()); redisUtils.del(loginKey); }
private String getRedisKey(long userId){ return this.getRedisKey(userId, false); }
private String getRedisKey(long userId, boolean isApp){ if (isApp) { return appUserTokenRedisKeyTemplate + userId; } else { return userTokenRedisKeyTemplate + userId; } }
private String createTokenKey(String token) { return tokenManagerConfig.getKeyPrefix() + token; }
private int getExpires(String key){ return key.contains("app") ? tokenManagerConfig.getExpires() * 24 * 7 : tokenManagerConfig.getExpires(); }
}
|
RedisTokenManager的spring上下文加载器:AppAutoConfiguraction
1 2 3 4 5
| @Bean public TokenManager<AuthUser> tokenManager(AbstractRedisCache abstractRedisCache,RedisTokenManagerConfig redisTokenManagerConfig){ DuplicateCheckUserLoginHandler duplicateCheckUserLoginHandler = new DuplicateCheckUserLoginHandler(abstractRedisCache, redisTokenManagerConfig); return new RedisTokenManager<>(abstractRedisCache,redisTokenManagerConfig,duplicateCheckUserLoginHandler); }
|