问题描述
前后端分离,验证码存储到session中,postman测试可以获取到session中的验证码,但是在另一台电脑上获取session中的验证码却始终为null,试了各种跨域、携带cookie都不管用。
前言
一个之前完成的项目说要加些新需求要我们做,我们肯定不能拒绝啊,开干。
打开项目,运行,浏览器输入地址,输入用户名、密码、验证码,点击登录,???验证码已失效???啥情况?断点一看,好家伙,获取存储在session中的验证码一直是null,再将存储验证码的session和登录时从请求获取的session一对比,根本不是同一个session,它就一直在变!然后我自己在postman中测试是没有问题的。
哦,懂了,是前后端分离跨域的问题,设置好后,再试,还是不行!设置允许携带cookie,再试,依旧不行。。。
百度找了好久前后端分离session处理,网上大部分都是说前端请求加上withCredentials: true ,后端在拦截器或过滤器设置
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept,token");
response.setHeader("Access-Control-Allow-Credentials","true");
但是不行。就很奇怪,之前都没这种问题啊,怎么这次代码都没改过,就会有这种问题?
因为这个项目一开始是别人已经做好的,然后给我们公司修改一些细节,bug之类的,然后整个项目也没用shiro和redis。权限控制,存储信息也都是用session的,然后我想过用redis来存储验证码吧,但是用redis的话,要改挺多配置的,挺麻烦的,而且这个项目本来就挺乱的,就放弃了。
暂时找不到什么好的办法,就只能先将验证码验证去掉,因为甲方部署这个项目是将前端后端都部署到一台机器的,也不存在跨域问题,所以我们自己这边测试的话,就先不用验证码了。
这个项目分为app端登录和web端登录,它app端登录是将token存储到数据库中,每次请求都是根据token去数据库查询这个token是否过期。web端则是将token存储到session中,结果可想而知,web端登录进去后,好多请求都报权限不足的错误。后面干脆把web端也和app端一样,都将token存储到数据库中,就不用session了,权限问题得以解决。
后面我就想,能不能也让验证码的session像token一样,保存。登录的时候,将这个sessionid直接设置到请求头中,那样应该可以吧?仔细想了下,感觉可行,那么这样的话,问题就主要在如何根据sessionid来获取session。
解决
1、SessionContextUtils 用于获取session
import javax.servlet.http.HttpSession;
import java.util.HashMap;
/**
* SessionContext用于获取session
* 根据sessionId获取session
*/
public class SessionContextUtils {
private static SessionContextUtils instance;
private HashMap<String, HttpSession> sessionMap;
private SessionContextUtils() {
sessionMap = new HashMap<String,HttpSession>();
}
public static SessionContextUtils getInstance() {
if (instance == null) {
instance = new SessionContextUtils();
}
return instance;
}
public synchronized void addSession(HttpSession session) {
if (session != null) {
sessionMap.put(session.getId(), session);
}
}
public synchronized void delSession(HttpSession session) {
if (session != null) {
sessionMap.remove(session.getId());
}
}
public synchronized HttpSession getSession(String sessionID) {
if (sessionID == null) {
return null;
}
return sessionMap.get(sessionID);
}
}
2、SessionListener 用于监听session
import com.utils.SessionContextUtils;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
/**
* 监听session
*/
@WebListener
public class SessionListener implements HttpSessionListener {
private SessionContextUtils sessionContext= SessionContextUtils.getInstance();
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
sessionContext.addSession(session);
}
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
sessionContext.delSession(session);
}
}
3、在启动类加上@ServletComponentScan注解
4、验证码生成
@ResponseBody
@GetMapping(value = "/get/code")
public JSONObject getCode(HttpServletRequest request, HttpServletResponse response) {
try {
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
// 生成随机字串(VerifyCodeUtils验证码生成工具类就不贴出来了,网上随便一找好多的)
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
// 存入会话session
HttpSession session = request.getSession(true);
// 删除以前的
session.removeAttribute("verCode");
session.removeAttribute("codeTime");
session.setAttribute("verCode", verifyCode.toLowerCase());
session.setAttribute("codeTime", LocalDateTime.now());
// 生成图片
int w = 200, h = 50;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
//注意,这里是将生成的图片转为base64的格式将返回给前端,而不是直接给张图片的地址。
//这样获取验证码就是一个请求,而不是一张图片的地址,因为是一个请求,所以我们可以返回sessionid给前端。
VerifyCodeUtils.outputImage(w, h, stream, verifyCode);
try{
String encode = Base64.encode(stream.toByteArray());
Map<String,Object> map = new HashMap<>();
map.put("img","data:image/png;base64,"+ encode);
//返回sessionid给前端,前端只需要在调用登录接口的时候将这个sessionid设置到请求头中即可
map.put("sessionId",session.getId());
return success(map,"");
}catch (Exception e){
}finally{
stream.close();
}
} catch (Exception e) {
System.out.println("验证码获取错误");
}
return failure("");
}
5、登录接口
@ResponseBody
@RequestMapping(value = "/login", method = RequestMethod.POST)
public JSONObject login(@NotBlank(message = "用户名不能为空") String username, @NotBlank(message = "密码不能为空") String password,String code, HttpServletRequest request) {
Map<String, Object> data = new HashMap<>();
//获取请求头中的sessionId,然后根据sessionId来获取session
String sessionId = request.getHeader("sessionId");
SessionContextUtils sessionContext= SessionContextUtils.getInstance();
HttpSession session = sessionContext.getSession(sessionId);
// 验证码
Object verCode = session.getAttribute("verCode");
if (null == verCode) {
data.put("msg", "验证码已失效,请重新输入");
data.put("type", 3);
return failure(data);
}
String verCodeStr = verCode.toString();
LocalDateTime localDateTime = (LocalDateTime) session.getAttribute("codeTime");
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zdt = localDateTime.atZone(zoneId);
Date codeDateTime = Date.from(zdt.toInstant());
if (verCodeStr == null || StringUtils.isEmpty(code) || !verCodeStr.equalsIgnoreCase(code)) {
data.put("msg", "验证码错误");
data.put("type", 3);
return failure(data);
} else if (((System.currentTimeMillis()) - (codeDateTime.getTime())) > (1 * 60 * 1000)) {
data.put("msg", "验证码已过期,请重新输入");
data.put("type", 3);
return failure(data);
} else {
//验证成功,删除存储的验证码
session.removeAttribute("verCode");
session.removeAttribute("codeTime");
}
//省略其他代码
return success(map, "登录成功");
}
大功告成,重启项目,测试,可以了,无论是postman还是另一台电脑测试都没问题。