現有的背景系統已經是前後端分離架構,背景用的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的代碼就不寫了。有興趣要的可以回報給我。