Shiro在請求頭中獲取sessionId以及rememberMe資訊
本文介紹的內容需要對Shiro有一定了解,學習Shiro可檢視跟開濤我學Shiro
解決問題步驟
- 重寫
DefaultWebSessionManager
命名為DefaultHeaderSessionManager
; - 重寫
CookieRememberMeManager
命名為HeaderRememberMeManager
; - 重寫
ShiroFilterFactoryBean
,修改其中的預設Filters; - 修改配置檔案,指定為重寫的類。
重寫DefaultWebSessionManager
DefaultWebSessionManager:用於Web環境的實現,可以替代ServletContainerSessionManager,自己維護著會話,直接廢棄了Servlet容器的會話管理。
DefaultWebSessionManager預設實現中,是通過Cookie確定sessionId,重寫時,只需要把獲取sessionid的方式變更為在request header中獲取即可。
新建 DefaultHeaderSessionManager
DefaultSessionManager
以及 implements WebSessionManager
//省略 import 資訊
/**
* @author Created by yangyang on 2018/1/18.
* e-mail :[email protected] ; tel :18580128658 ;QQ :296604153
*/
public class DefaultHeaderSessionManager extends DefaultSessionManager implements WebSessionManager {
}
request header中,我使用x-auth-token進行sessionid標識,接下來直接展示當前類的詳細實現
//省略 import 資訊
public class DefaultHeaderSessionManager extends DefaultSessionManager implements WebSessionManager {
// slf4j logback
private static final Logger log = LoggerFactory.getLogger(DefaultHeaderSessionManager.class);
private final String X_AUTH_TOKEN = "x-auth-token";
// 請求頭中獲取 sessionId 並把sessionId 放入 response 中
private String getSessionIdHeaderValue(ServletRequest request, ServletResponse response) {
if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null.");
return null;
} else {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 在request 中 讀取 x-auth-token 資訊 作為 sessionId
String sessionId = httpRequest.getHeader(this.X_AUTH_TOKEN);
// 每次讀取之後 都把當前的 sessionId 放入 response 中
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (StringUtils.isNotEmpty(sessionId)) {
httpResponse.setHeader(this.X_AUTH_TOKEN, sessionId);
log.info("Current session ID is {}", sessionId);
}
return sessionId;
}
}
//獲取sessionid
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = this.getSessionIdHeaderValue(request, response);
//DefaultWebSessionManager 中程式碼 直接copy過來
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
//不會把sessionid放在URL後
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, Boolean.FALSE);
return id;
}
// 移除sessionid 並設定為 deleteMe 標識
private void removeSessionIdHeader(HttpServletRequest request, HttpServletResponse response) {
response.setHeader(this.X_AUTH_TOKEN, "deleteMe");
}
/**
* 把sessionId 放入 response header 中
* onStart時呼叫
* 沒有sessionid時 會產生sessionid 並放入 response header中
*/
private void storeSessionId(Serializable currentId, HttpServletRequest ignored, HttpServletResponse response) {
if (currentId == null) {
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
} else {
String idString = currentId.toString();
response.setHeader(this.X_AUTH_TOKEN, idString);
log.info("Set session ID header for session with id {}", idString);
log.trace("Set session ID header for session with id {}", idString);
}
}
// 建立session
protected Session createExposedSession(Session session, SessionContext context) {
if (!WebUtils.isWeb(context)) {
return super.createExposedSession(session, context);
} else {
ServletRequest request = WebUtils.getRequest(context);
ServletResponse response = WebUtils.getResponse(context);
SessionKey key = new WebSessionKey(session.getId(), request, response);
return new DelegatingSession(this, key);
}
}
protected Session createExposedSession(Session session, SessionKey key) {
if (!WebUtils.isWeb(key)) {
return super.createExposedSession(session, key);
} else {
ServletRequest request = WebUtils.getRequest(key);
ServletResponse response = WebUtils.getResponse(key);
SessionKey sessionKey = new WebSessionKey(session.getId(), request, response);
return new DelegatingSession(this, sessionKey);
}
}
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.");
} else {
HttpServletRequest request = WebUtils.getHttpRequest(context);
HttpServletResponse response = WebUtils.getHttpResponse(context);
Serializable sessionId = session.getId();
this.storeSessionId(sessionId, request, response);
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
}
//獲取sessionid
public Serializable getSessionId(SessionKey key) {
Serializable id = super.getSessionId(key);
if (id == null && WebUtils.isWeb(key)) {
ServletRequest request = WebUtils.getRequest(key);
ServletResponse response = WebUtils.getResponse(key);
id = this.getSessionId(request, response);
}
return id;
}
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
return this.getReferencedSessionId(request, response);
}
protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
super.onExpiration(s, ese, key);
this.onInvalidation(key);
}
protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) {
super.onInvalidation(session, ise, key);
this.onInvalidation(key);
}
private void onInvalidation(SessionKey key) {
ServletRequest request = WebUtils.getRequest(key);
if (request != null) {
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID);
}
if (WebUtils.isHttp(key)) {
log.debug("Referenced session was invalid. Removing session ID header.");
this.removeSessionIdHeader(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key));
} else {
log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response pair. Session ID cookie will not be removed due to invalidated session.");
}
}
protected void onStop(Session session, SessionKey key) {
super.onStop(session, key);
if (WebUtils.isHttp(key)) {
HttpServletRequest request = WebUtils.getHttpRequest(key);
HttpServletResponse response = WebUtils.getHttpResponse(key);
log.debug("Session has been stopped (subject logout or explicit stop). Removing session ID cookie.");
this.removeSessionIdHeader(request, response);
} else {
log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response pair. Session ID cookie will not be removed due to stopped session.");
}
}
public boolean isServletContainerSessions() {
return false;
}
重寫CookieRememberMeManger
預設情況下,Shiro會把rememberMe資訊放入set-cookie中,儲存在瀏覽器上。這裡,重寫Cookie方式,把rememberMe資訊放入response header中。
建立 HeaderRememberMeManager
類並extends AbstractRememberMeManager
,程式碼如下
//省略 import 資訊
public class HeaderRememberMeManager extends AbstractRememberMeManager {
private static final transient Logger log = LoggerFactory.getLogger(HeaderRememberMeManager.class);
// header 中 固定使用的 key
public static final String DEFAULT_REMEMBER_ME_HEADER_NAME = "remember-me";
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
log.debug(msg);
}
} else {
HttpServletResponse response = WebUtils.getHttpResponse(subject);
String base64 = Base64.encodeToString(serialized);
// 設定 rememberMe 資訊到 response header 中
response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, base64);
}
}
private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
ServletRequest request = subjectContext.resolveServletRequest();
if (request == null) {
return false;
} else {
Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
return removed != null && removed;
}
}
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
} else {
WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (this.isIdentityRemoved(wsc)) {
return null;
} else {
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
// 在request header 中獲取 rememberMe資訊
String base64 = request.getHeader(DEFAULT_REMEMBER_ME_HEADER_NAME);
if ("deleteMe".equals(base64)) {
return null;
} else if (base64 != null) {
base64 = this.ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
return null;
}
}
}
}
private String ensurePadding(String base64) {
int length = base64.length();
if (length % 4 != 0) {
StringBuilder sb = new StringBuilder(base64);
for (int i = 0; i < length % 4; ++i) {
sb.append('=');
}
base64 = sb.toString();
}
return base64;
}
protected void forgetIdentity(Subject subject) {
if (WebUtils.isHttp(subject)) {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
this.forgetIdentity(request, response);
}
}
public void forgetIdentity(SubjectContext subjectContext) {
if (WebUtils.isHttp(subjectContext)) {
HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
this.forgetIdentity(request, response);
}
}
private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
//設定刪除標示
response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, "deleteMe");
}
}
重寫ShiroFilterFactoryBean
預設Fileter鏈中,user名稱的過濾器在為登陸狀態下會返回到登入介面,這裡修改一下,為登陸狀態直接放回Json字串,不用跳轉至登入頁面。
如果使用了authc過濾 需要對重寫 FormAuthenticationFilter ,為了適配App客戶端,這裡不推薦使用authc,可以在必須重新驗證使用者登陸資訊時(使用rememberMe資訊登陸無效)預先請求一下服務端或者通過記錄的x-auth-token有效期進行判斷。
新建 MyUserFilter
類 extends UserFilter
public class MyUserFilter extends org.apache.shiro.web.filter.authc.UserFilter {
// isAccessAllowed return false 執行
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 這裡也可以不用儲存 儲存當前request 可在登陸後重新請求當前 request
this.saveRequest(request);
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.getWriter().write("{\"code\":-1,\"message\":\"no.login\"}");
return false;
}
}
新建 MyDefaultFilter
enum
// 程式碼 為修改部分 只需要更改 user 執行為新建的 Filter
// 具體程式碼 可檢視 org.apache.shiro.web.filter.mgt.DefaultFilter
public enum MyDefaultFilter {
user(MyUserFilter.class);
private final Class<? extends Filter> filterClass;
private MyDefaultFilter(Class<? extends Filter> filterClass) {
this.filterClass = filterClass;
}
}
新建 MyDefaultFilterChainManager
類 extends DefaultFilterChainManager
public class MyDefaultFilterChainManager extends DefaultFilterChainManager {
protected void addDefaultFilters(boolean init) {
//使用我們建立的 DefaultFilter
MyDefaultFilter[] var2 = MyDefaultFilter.values();
int var3 = var2.length;
for (int var4 = 0; var4 < var3; ++var4) {
MyDefaultFilter defaultFilter = var2[var4];
super.addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
}
}
}
新建 MyShiroFilterFactoryBean
類 extends ShiroFilterFactoryBean
public class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean {
protected FilterChainManager createFilterChainManager() {
// 只要修改這裡 使用我們建立的 DefaultFilterChainManager
MyDefaultFilterChainManager manager = new MyDefaultFilterChainManager();
//省略程式碼 請在 ShiroFilterFactoryBean 中 copy
}
// 預設的private方法 需在 ShiroFilterFactoryBean 中 copy
}
修改配置資訊,指向重寫類
這裡只展示和上述有關的配置資訊,採用的註解@Bean 方式
@Bean(name = "rememberMeManager")
public HeaderRememberMeManager rememberMeManager() {
HeaderRememberMeManager headerRememberMeManager = new HeaderRememberMeManager();
// base64Encoded 自行生成一個 用於rememberMe加密
headerRememberMeManager.setCipherKey(base64Encoded);
return headerRememberMeManager;
}
@Bean
public DefaultHeaderSessionManager defaultWebSessionManager(SessionDAO sessionDAO) {
DefaultHeaderSessionManager defaultHeaderSessionManager = new DefaultHeaderSessionManager();
// 設立不使用 調取器驗證 session 是否過期 作者使用了 redis ,這裡根據SessionDAO實際情況設定
defaultHeaderSessionManager.setSessionValidationSchedulerEnabled(false);
defaultHeaderSessionManager.setSessionDAO(sessionDAO);
return defaultHeaderSessionManager;
}
@Bean(name = "shiroFilter")
public MyShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
// 使用自行建立的 FactoryBean
MyShiroFilterFactoryBean shiroFilterFactoryBean = new MyShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilterChainDefinitions("/api/v1/login = anon\n" +
"/ = anon\n" +
"/api/v1/website/article/** = anon\n" +
"/api/v1/** = cors,user\n");
return shiroFilterFactoryBean;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager( DefaultHeaderSessionManager sessionManager,
RememberMeManager rememberMeManager) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//其他配置這裡未列出
//DefaultHeaderSessionManager 重寫的 sessionManager
defaultWebSecurityManager.setSessionManager(sessionManager);
// rememberMeManager 重寫的 rememberMeManager
defaultWebSecurityManager.setRememberMeManager(rememberMeManager);
SecurityUtils.setSecurityManager(defaultWebSecurityManager);
return defaultWebSecurityManager;
}
完成以上配置資訊就可以在 把sessionid或者rememberMe寫入response header中和在 request header中讀取,session超時自動銷燬時間前端需和服務端保持一致,rememberMe有效時間由前端自行控制。
結束
本文介紹了一些專案中使用shiro的技巧,如果有錯誤或者有更好的方式,希望能與筆者聯絡,筆者QQ:296604153。