前言:
專案需要使用者重啟瀏覽器後,還能記錄使用者登入狀態。專案鑑權使用了shiro框架,發現rememberMe功能剛好可以實現需求。按照教程把功能實現後,順帶閱讀了一下原始碼,在這裡做下閱讀記錄。
必要知識:
眾所周知,前端訪問後端介面後,後端會向前端cookie寫個sessionid作為會話標記。session有效期為這次關閉瀏覽器,所以只要重啟時,儲存下來,就能實現記錄狀態的功能了。
在shiro提供的SecurityManager中,網站開發,我們常用DefaultWebSecurityManager,它繼承於DefaultSecurityManager。DefaultSecurityManager是shiro自帶實現的最基礎但已直接可用的SecurityManager,它包含了shiro所有主要的鑑權流程。
shiro如何記錄使用者狀態:
使用者登陸:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
...
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
複製程式碼
在使用者登入成功後,會有一個後置處理:
protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
rememberMeSuccessfulLogin(token, info, subject);
}
複製程式碼
它的內部,就是來向前端cookie中記錄當前登陸狀態,
protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
rmm.onSuccessfulLogin(subject, token, info);
...
}
複製程式碼
DefaultWebSecurityManager在構造時,預設會設定一個RememberMeManager
public DefaultWebSecurityManager() {
super();
...
setRememberMeManager(new CookieRememberMeManager());
}
複製程式碼
具體執行cookie記錄(看原始碼註釋: 不管有沒有,先刪除一下,然後判斷現在是否需要rememberMe)
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);
//now save the new identity:
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
...
}
複製程式碼
-
刪除cookie的操作,就是把當前key的cookie的maxAge設定為0,然後重新寫回瀏覽器
public void removeFrom(HttpServletRequest request, HttpServletResponse response) { String name = getName(); String value = DELETED_COOKIE_VALUE; String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions String domain = getDomain(); String path = calculatePath(request); int maxAge = 0; //always zero for deletion int version = getVersion(); boolean secure = isSecure(); boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly); log.trace("Removed '{}' cookie by setting maxAge=0", name); } 複製程式碼
-
shiro預設是按token實現RememberMeAuthenticationToken這個介面,並設定isRememberMe為true來判斷是否要記錄狀態的。
1.我們可以讓自己的token實現這個介面
2.也可以自己寫一個RememberMeManager的實現,重寫isRememberMe,然後替換預設的。
protected boolean isRememberMe(AuthenticationToken token) { return token != null && (token instanceof RememberMeAuthenticationToken) && ((RememberMeAuthenticationToken) token).isRememberMe(); } 複製程式碼
-
前端最終記錄的就是憑證組
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { PrincipalCollection principals = getIdentityToRemember(subject, authcInfo); rememberIdentity(subject, principals); } 複製程式碼
-
shiro會把憑證組序列化後,再加密
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); if (getCipherService() != null) { bytes = encrypt(bytes); } return bytes; } 複製程式碼
-
預設使用了AES加密
public AbstractRememberMeManager() { this.serializer = new DefaultSerializer<PrincipalCollection>(); AesCipherService cipherService = new AesCipherService(); this.cipherService = cipherService; setCipherKey(cipherService.generateNewKey().getEncoded()); } 複製程式碼
-
在最終寫回前端時,shiro還會把加密後的值base64格式化一下,防止一些加密演算法加密出奇怪的值來影響使用
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) { ... //base 64 encode it and store as a cookie: String base64 = Base64.encodeToString(serialized); Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); } 複製程式碼
以上,即使瀏覽器重啟,也是會記錄下使用者前一次的登陸資訊了,下次訪問伺服器時,cookie已經帶上了使用者資訊
shiro如何重新讀取使用者狀態
shiro預設會把subject存在當前執行緒中,如果沒有,則會去建立建一個
public Subject createSubject(SubjectContext subjectContext) {
...
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);
...
}
複製程式碼
預設會把subject儲存在session中(也會有快取或者自己寫的儲存機制等),如果沒有,它就會去getRememberedIdentity()方法中獲取
protected SubjectContext resolvePrincipals(SubjectContext context) {
PrincipalCollection principals = context.resolvePrincipals();
if (CollectionUtils.isEmpty(principals)) {
log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity.");
principals = getRememberedIdentity(context);
...
}
複製程式碼
最終就是從前端cookie中獲取到上面步驟儲存的內容,解密反序列化,得到使用者憑證組資訊(整個邏輯與上面同理相反,就不贅述了)
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
...
}
複製程式碼
rememberMe與普通登陸的差別
使用rememberMe的功能時,路徑攔截如果使用authc攔截器,還是會被攔截,需要使用user攔截器才能被通過。
這樣的好處是,可以把重要的,比如說支付之類,需要每次登陸(防止陌生人使用你的電腦),而一些訊息瀏覽的介面(不特別重要),可以讓使用者開啟瀏覽器就能看到
區分攔截的原理:
為何rememberMe的使用者無法訪問authc攔截的內容,只能訪問user攔截的呢!
前文提到,如果當前執行緒沒有subject,shiro會去建立。
預設subject會儲存在session中,並且會有一個標記值authenticated。
而rememberMe的使用者資訊是從cookie中解析出來的,session是剛新建的,裡面沒有登陸標記。
所以最終的subject與登陸後的subject都有憑證資訊,但是登陸標記不一樣。
public Subject createSubject(SubjectContext context) {
...
//從session中獲取登陸標記(獲取不到則為false)
boolean authenticated = wsc.resolveAuthenticated();
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
request, response, securityManager);
}
複製程式碼
shiro儲存在session的登陸標記的預設key
/**
* The session key that is used to store whether or not the user is authenticated.
*/
public static final String AUTHENTICATED_SESSION_KEY = DefaultSubjectContext.class.getName() + "_AUTHENTICATED_SESSION_KEY";
複製程式碼
authc標記使用的FormAuthenticationFilter攔截器,用了預設的鑑權方法。如果isAuthenticated不是true,就認為沒登陸,所以rememberMe的方式不能通過。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
複製程式碼
而user標記使用的UserFilter攔截器,重寫了鑑權方法,它只是判斷了subject中是否有使用者憑證資訊,所以rememberMe的方式才能被通過。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginRequest(request, response)) {
return true;
} else {
Subject subject = getSubject(request, response);
// If principal is not null, then the user is known and should be allowed access.
return subject.getPrincipal() != null;
}
}
複製程式碼