现有的后台系统已经是前后端分离架构,后台用的springboot,前台用的基于vue的element UI。一直以为支持跨域的,之前调试的时候也碰到过自己的前端开发环境不能链接生产后台服务,但是用postman等工具却没有问题。当时没有解决,连接本地调试后台就没有问题。后来用新的uniapp代码连接,login接口可以用,其他接口登录时后台始终是不行,始终会被shiro拦截返回401代码让重新登录。尝试了很多种办法,包括:
1,让前端带上cookie
2,让后台不拒绝登录
怎么都不行,然后认证看session和cookie的一些介绍,结合源代码跟踪,终于知道了,现有的浏览器都是支持w3c的一些规定,跨域的cookie无法被自动带入后台,后台shiro目前只是支持cookie。要改成真正跨域的前后端分离,必须实用token方式,就是前端单独记录login接口返回的token,并且每次请求再带上token,后台除了login接口生产返回token之外,还需要改造session管理器,还要支持之前的前端系统访问需求。
一、首先改写后台:
网上搜了很多,无意中看到的一个解决方案,彻底帮我解决了后台的问题,在此对原创的杰子学编程表示感谢:Shiro重构:整合token和cookie实现登陆及验证 - 知乎
1,在shiroconfigurateion.java中启用自定义的 CustomerWebSessionManager
会话管理器,设定会话超时及保存
@Bean
public SessionManager sessionManager() {
// DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
DefaultWebSessionManager sessionManager = new CustomerWebSessionManager();
if(sessionRedis) sessionManager.setSessionDAO(redisSessionDao); //aws需要
sessionManager.setGlobalSessionTimeout(7200000);
Cookie cookie=sessionManager.getSessionIdCookie();
//cookie.setSecure(true);//添加安全标志,不确定能起作用。cth 2020.11.26
cookie.setMaxAge(31536000);
cookie.setHttpOnly(false);
return sessionManager;
}
2,编写CustomerWebSessionManager:
登录session开始记录sessionid,如果请求头中包含DEVICE=MOBILE,则将sessionId放在header中返回:
/**
* 会话创建
* Stores the Session's ID, usually as a Cookie, to associate with future requests.
*
* @param session the session that was just {@link #createSession created}.
*/
@Override
protected void onStart(Session session, SessionContext context) {
super.onStart(session, context);
if (!WebUtils.isHttp(context)) {
log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response " +
"pair. No session ID cookie will be set.");
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(context);
HttpServletResponse response = WebUtils.getHttpResponse(context);
if (isSessionIdCookieEnabled()) {
Serializable sessionId = session.getId();
storeSessionId(sessionId, request, response);
} else {
log.debug("Session ID cookie is disabled. No cookie has been set for new session with id {}", session.getId());
}
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
/**
* 存储会话id到response header中
*
* @param currentId 会话ID
* @param request HttpServletRequest
* @param response HttpServletResponse
*/
private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
if (currentId == null) {
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
}
String idString = currentId.toString();
//增加判断,如果请求头中包含DEVICE=MOBILE,则将sessionId放在header中返回
if (StringUtils.hasText(request.getHeader(DEVICE)) && MOBILE.equals(request.getHeader(DEVICE))) {
response.setHeader(AUTH_TOKEN, idString);
} else {
Cookie template = getSessionIdCookie();
Cookie cookie = new SimpleCookie(template);
cookie.setValue(idString);
cookie.saveTo(request, response);
}
log.trace("Set session ID cookie for session with id {}", idString);
}
3,Login接口返回token:
Map<String,Object> data1 = new HashMap<>();
data1.put("token",response.getHeader("token"));
result.setData(data1);
4,改写CustomerWebSessionManager获取sessionid方法,如果请求头中带有token,则取token,否则还是调用之前的方法getReferencedSessionId
/**
* 重写父类获取sessionID的方法,若请求为APP或者H5则从请求头中取出token
*
* @param request 请求参数
* @param response 响应参数
* @return id
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID. Returning null.");
return null;
}
HttpServletRequest httpRequest = WebUtils.toHttp(request);
if (StringUtils.hasText(httpRequest.getHeader(AUTH_TOKEN))) {
//从header中获取token
String token = httpRequest.getHeader(AUTH_TOKEN);
// 每次读取之后都把当前的token放入response中
HttpServletResponse httpResponse = WebUtils.toHttp(response);
if (StringUtils.hasText(token)) {
httpResponse.setHeader(AUTH_TOKEN, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
//sessionIdUrlRewritingEnabled的配置为false,不会在url的后面带上sessionID
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
return token;
}
return getReferencedSessionId(request, response);
}
至此,后台改写完成
二、编写前端代码
之前的代码还是cookie的方式,不写改可以直接用,但是需要测试一下
新的前端是基于uniapp。
1,登录后保存token:setStorageSync
const result = await this.$send.ajax('/login', data);
if (!result.data.status) {
this.$api.alerts('登录提示', result.msg, false);
return;
} else {
console.log('用户登录:'+data.username)
uni.setStorageSync('user', data);
//登录页处理跨域
uni.setStorageSync('token', result.data.data.token);
uni.reLaunch({
url: '../main/main'
});
return;
}
2,后续所有接口请求头都带上token
var globalData = {
header: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
'device': 'mobile'
},
}
uni.getStorage({
key: 'token',
success: function(res) {
globalData.header.token = res.data
}
});
uni.request({
url: uri,
data: datar,
method: methodtype,
header: globalData.header,
withCredentials: true,
sslVerify: false,
success: (res) => {
},
fail: (err) => {
},
complete: () => {
/* uni.hideLoading(); */
}
至此,前端也完成。
测试之后,完美实现token和cookie双支持,前端cookie的代码就不写了。有兴趣要的可以反馈给我。