上回书和上上回书说到redis的用法还有在代码里怎么操作Redis数据库,学完了得用啊。怎么用啊?这得从项目架构说起了。
mmall是一个简单的用SSM搭建起来的基本只能本地玩耍的电商DEMO,最简单的架构版本V1.0是这样婶的:
user123访问网址发送请求,nginx把请求发送到Tomcat,Tomcat再去访问数据库或者ftpserver;session保存在Tomcat里;
如果是访问人少,一台服务器当然可以顶得住,请求服务器的多了,我们可以给这个服务器升级,提高它的纵向扩展能力:升级机器的内存,CPU,硬盘机械改固态但带来的还有成本指数级升高。
升级一台服务器成本高,那用几个普通的服务器做成集群不就可以了吗?于是,就有了下面的机构:
这个架构版本,称为V1.1版本吧。
这个版本看上去没毛病,实际是不能使用的。想象下这个业务场景:userA登录请求,nginx发送给TomcatA;用户再进行下单请求,Nginx发送给TomcatB;B里没有session,A里有session,这里就会校验到用户未登录,实际上用户登录了,session保存在A里,请求的却是B。
既然这个版本不能满足业务需求,就继续升级吧,于是就有了下面的版本V2.0:
user无论请求到哪台服务器,我们都会把session信息放到redis session server上。tomcat服务器都会从Redis session server上读取session信息。
这样一来,Session登录信息存储及读取的问题就解决了。但是还有一个问题:服务器定时任务并发的问题。这个问题怎么产生的?怎么解决?请看下回分解。
空谈误国,代码兴邦。
util,util还是Util。util是什么?是你写完永远不会去看源码拿来就用的东西。
cookie的path,domain属性没有深入研究,其实要是不做这个DEMO,cookie都碰不到。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class CookieUtil {
private final static String COOKIE_DOMAIN = ".happymmall.com";
private final static String COOKIE_NAME = "mmall_login_token";
// 把request对象里的所有cookie保存到cookie数组里,遍历数组拿到指定的cookie后返回其值
public static String readLoginToken (HttpServletRequest request) {
Cookie[] cks = request.getCookies();
if (cks != null) {
for (Cookie ck : cks) {
log.info("read cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
log.info("return cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
return ck.getValue();
}
}
}
return null;
}
// 根据传入的tooken新建cookie对象,将其保存到response对象里
public static void writeLoginToken (HttpServletResponse response, String token) {
Cookie ck = new Cookie(COOKIE_NAME, token);
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/"); // 代表设置在根目录
ck.setHttpOnly(true); // 不能通过脚本访问cookie
// 如果maxage不设置,cookie就不会写入硬盘而是写在内存里,只在当前页面有效
ck.setMaxAge(60 * 60 * 24 * 265);// -1 永久 单位是秒
log.info("write cookieName:{}, cookieValue: {}", ck.getName(), ck.getValue());
response.addCookie(ck);
}
// 从request里拿到cookie数组,找到指定的cookie,设置删除后添加到response里
public static void delLoginToken (HttpServletRequest request, HttpServletResponse response) {
Cookie[] cks = request.getCookies();
if (cks != null) {
for (Cookie ck : cks) {
if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/");
ck.setMaxAge(0);// 0为删除cookie
log.info("del cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
response.addCookie(ck);
return;
}
}
}
}
}
接下来看个登录接口,看下是怎么实现单点登录的。
小白千万不要怕,心理千万不要这么想:(唉,单点登录啊,这是什么流弊的技术啊这个该怎么用啊等等心中一万个曹尼玛。)我只想说,娃儿,莫慌,莫慌。莫慌。
知道什么是服务器吧?服务器是什么?说白了就是一个电脑,不过性能高些而已。
知道什么是部署吧?部署是什么?说白了就是复制粘贴,把自己的本地代码放到服务器上运行跑起来。
有些词看起来高大上,其实并没有。有些听上去高大上的东西是骗投资人和消费者的,IT行业的高大上的东西,是老鸟们装逼用的。装逼是人类的刚需。
userA在登录操作的时候,nginx把请求分发到了tomcatA,在A里把cookie保存到客户端,用户信息保存到redis里。A服务器的cookie只能在A里使用,为了让集群服务器都能使用,cookie的域名(domain属性)在util里写死掉,这样所有以写死的域名结尾的域名都能访问该域名。多个服务器共享一个cookie,DEMO里单点登录用的还是共享cookie。
todo 研究下真正的单点登录:
https://blog.csdn.net/qq_40241957/article/details/88371061
对照着V1.0架构图和V2.0架构图,我们可以清楚的看到,原来的session是保存在tomcat服务器里的,现在放在了redis里面,redis成了"session server"。
V1.0的登录接口:
@RequestMapping(value = "login.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse login (String username, String password, HttpSession session){
ServerResponse response = iUserService.login(username,password);
if (response.isSuccess()){
session.setAttribute(Const.CURRENT_USER,response.getData());
}
return response;
}
V2.0的登录接口:
@RequestMapping(value = "login.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse login (String username, String password, HttpSession session,
HttpServletResponse httpServletResponse){
//HttpServletResponse httpServletResponse, HttpServletRequest httpServletRequest){
//service-->mybatis-->dao
ServerResponse response = iUserService.login(username,password);
if (response.isSuccess()){
// 重构后的对业务代码仍然有侵入
// todo 使用spring session 进行解耦
CookieUtil.writeLoginToken(httpServletResponse, session.getId());
// 本来保存在session里的用户信息 放到数据库里 有效期 30min
RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
}
return response;
}
用户登录后,在页面上点点点,买买买,和后台交互的不亦乐乎,像这样:
/**
* 获取用户信息
* @return
*/
@RequestMapping(value = "get_user_info.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse getUserInfo(HttpServletRequest httpServletRequest){
//User user = (User) session.getAttribute(Const.CURRENT_USER);
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
if (StringUtils.isEmpty(loginToken)) {
return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
}
String userJsonStr = RedisPoolUtil.get(loginToken);
User user = JsonUtil.string2Obj(userJsonStr, User.class);
if (user != null){
return ServerResponse.createBySuccess(user);
}
return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
}
但是别忘了在登录的时候,我们redis保存的用户信息是有有效期的:
RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
时间半小时。半小时啊半小时,也就是用户最多只能玩耍半小时过了半小时就得重新登录。怎么办?能不能改下代码,加个东西让用户每请求一次后台就重置下用户的有效期?
当然可以。那个东西叫拦截器:
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class SessionExporeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
// 拦截器,用户登录后每次请求后台都会重置token有效期为30min
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
if (StringUtils.isNotEmpty(loginToken)) {
String userJsoinStr = RedisPoolUtil.get(loginToken);
User user = JsonUtil.string2Obj(userJsoinStr, User.class);
if (user != null) {
RedisPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
web.xml里也要配置下:
sessionExpireFilter
com.mmall.controller.common.SessionExporeFilter
sessionExpireFilter
*.do
好了,功能实现了,接下来该做什么呢?
奥,还有一个彩蛋。
这个彩蛋涉及到用户重置密码这个业务。
最简单的前台页面是,用户点击忘记密码,输入用户名,问题答案,点击提交后输入新的密码就能重置密码;在后台涉及到两个接口:忘记密码&重置密码。简单地回顾下逻辑:(详细的请移步mallV1.0)
mmallV1.0
1 忘记密码checkAnswer
传入用户名 问题 答案 校验
设置有有效期的 forgetToken token放在服务器的 GuavaCache 里
返回forgetToken
token的作用是 防止其他人拿到这个forgetToken去恶意请求接口修改他人的密码
2 重置密码 forgetResetPassword
传入用户名 新密码 forgetToken 校验
forgetToken超过了有效期就会被清除 以此校验前端传来的forgetToken
校验成功后 修改密码
现在集群来了,原来的不能再放到服务器上了:
1 用户点击忘记密码,请求服务器1,它将token保存在本服务器的 GuacaCache 里
2 用户提交新密码 请求服务器2,这个服务器里的 GuavaCache 里没有保存 第一步的 token 提交就会报错 token无效或者过期
怎么办?当然还是把将生成的token放到redis里:
String forgetToken = UUID.randomUUID().toString();
// 原来的 token 保存在服务器上
//TokenCache.setKey(TokenCache.TOKEN_PREFIX+username,forgetToken);
// 现在放到redis里面
RedisPoolUtil.setEx(Const.TOKEN_PREFIX+username, forgetToken, 60*30);
用的时候也是从redis里读:
//String token = TokenCache.getKey(TokenCache.TOKEN_PREFIX+username);
String token = RedisPoolUtil.get(Const.TOKEN_PREFIX+username);
项目地址如下,分支用dev:
https://github.com/SilentJhin/mmall/tree/dev
收工。
自己写的东西一定是有不完善的地方(内容,技术,代码等等),如果有看不惯的,来评论。我一定回。