當(dāng)線上集群時(shí)候镶殷,會(huì)出現(xiàn)session共享問題禾酱。
雖然Tomcat提供了session copy的功能,但是缺點(diǎn)比較明顯:
1:當(dāng)Tomcat多的時(shí)候绘趋,session需要大量同步到多臺(tái)集群上颤陶,占用內(nèi)網(wǎng)寬帶
2:同一個(gè)用戶session,需要在多個(gè)Tomcat中都存在陷遮,浪費(fèi)內(nèi)存空間
如果要替換掉Tomcat的session共享指郁,替代方案應(yīng)該滿足:
1:數(shù)據(jù)共享
2:內(nèi)存存儲(chǔ)
3:key\value結(jié)構(gòu)
基于Redis實(shí)現(xiàn)共享session登錄
本文由凱哥Java(gz#h:kaigejava),個(gè)人blog:www#kaigejava#.com拷呆。發(fā)布于簡書
再來回顧下將驗(yàn)證碼保存在session中業(yè)務(wù)流程
我們在session中存放 因?yàn)閟ession的特點(diǎn)闲坎,每次訪問都是一個(gè)新的sessionId.我們可以直接使用code作為key.思考:那么如果換成了Redis,還能使用code作為可以嗎茬斧?
將用戶信息存放在session中流程:
用戶信息在session中存放:session.setAttribute("user", user); 同樣思考:那么如果換成了Redis腰懂,還能使用user作為可以嗎?
將code和user信息存放在Redis中,流程如下:
驗(yàn)證碼數(shù)據(jù)結(jié)構(gòu)是:string類型的
用戶對象數(shù)據(jù)類型是:hash類型的
根據(jù)上面分析项秉,我們修改原來代碼:
需要考慮的是:Redis的key規(guī)則绣溜、過期時(shí)間
1:發(fā)送驗(yàn)證碼的時(shí)候,將驗(yàn)證碼存放到Redis中時(shí)候娄蔼,需要考慮過期時(shí)間怖喻。其核心代碼如下:
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
2:用戶登錄的時(shí)候,將校驗(yàn)驗(yàn)證碼以及用戶信息存放到Redis中后,返回token
需要考慮的:
1:token不能重復(fù)
2:用戶過期時(shí)間
3:登錄成功后,要將token返回給前端
4:用戶只要訪問岁诉,Redis中的過期時(shí)間就要延長-在攔截器中處理的
用戶登錄核心代碼修改:
<pre class="brush:as3;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248);">//2.1:校驗(yàn)驗(yàn)證碼是否正確
//String code = (String) session.getAttribute("code");
String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (StringUtils.isEmpty(code) || !code.equals(loginForm.getCode())) {
return Result.fail("驗(yàn)證碼錯(cuò)誤!");
}
//2.2:根據(jù)手機(jī)號(hào)查詢,如果不存在锚沸,創(chuàng)建新用戶
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id", "phone", "nick_name");
queryWrapper.eq("phone", phone);
User user = this.getOne(queryWrapper);
if (Objects.isNull(user)) {
log.info("新建用戶");
//新建用戶
user = createUserWithPhone(phone);
}
//2.3:保存用戶到session中
UserDTO userDTO = new UserDTO();
userDTO.setId(user.getId());
userDTO.setIcon(user.getIcon());
userDTO.setNickName(user.getNickName());
//session.setAttribute("user", userDTO);
//2.3.1:獲取隨機(jī)的token,作為用戶登錄的令牌
String token = UUID.randomUUID().toString(true);
//2.3.2:將用戶以hash類型存放到Redis中==》將user對象轉(zhuǎn)換成map
//user對象里有非string類型的字段,用這個(gè)方法會(huì)報(bào)錯(cuò)的
// Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);
Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
, CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_TOKEN_KEY+token,userMap);
//LOGIN_USER_TOKEN_TTL
stringRedisTemplate.expire(LOGIN_USER_TOKEN_KEY+token,LOGIN_USER_TOKEN_TTL,TimeUnit.MINUTES);
//2.3.3: 將token返回
return Result.ok(token);
</pre>
需要注意:
在使用stringRedisTemplate存放hash對象的時(shí)候,對象中所有的key只能是string類型涕癣,如果存在非string類型會(huì)報(bào)錯(cuò)的哗蜈。所以這里使用了hootool的BeanUtil工具類:
<pre class="brush:as3;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248);">Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
, CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
</pre>
攔截器修改代碼:
因?yàn)閿r截器是我們自定義的,所以不能被spring容器管理的坠韩,RedisTemplate就不能自動(dòng)注入了距潘。我們就使用有參構(gòu)造器,傳遞:
<pre class="brush:as3;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248);">public class LoginRedisInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
/**
* 因?yàn)檫@個(gè)類不能被spring管理只搁,所以不能直接注入RedisTemplate對象音比。通過構(gòu)造函數(shù)傳遞
* @param stringRedisTemplate
*/
public LoginRedisInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1:從請求中獲取到token
String token = request.getHeader("authorization");
if(StringUtils.isEmpty(token)){
response.setStatus(401);
return false;
}
//2:基于token獲取redis中用戶對象
String key = LOGIN_USER_TOKEN_KEY+token;
Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3:判斷
if(userMap.isEmpty()){
response.setStatus(401);
return false;
}
//將map轉(zhuǎn)對象
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(user);
//刷新token的過期時(shí)間
stringRedisTemplate.expire(key,LOGIN_USER_TOKEN_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
</pre>
總結(jié):
在使用Redis替換session的時(shí)候,需要考慮的問題:
1:選擇合適的數(shù)據(jù)結(jié)構(gòu)
2:選擇合適的key
3:選擇合適的存儲(chǔ)粒度