shiro源码分析篇3:用户登录缓存登录信息
来源:互联网 发布:网络安全技术一般包括 编辑:程序博客网 时间:2024/05/22 07:43
上篇讲了shiro是如何过滤请求链接,判断用户是否已经登录。
这篇就是讲解shiro用户登录时,如何把登录信息缓存起来,下次用户登录其他需要登录的链接时,如何判断已经登录了。
RetryLimitHashedCredentialsMatcher自定义的登录凭据,也就是登录的处理方案
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private Logger logger = LoggerFactory.getLogger(RetryLimitHashedCredentialsMatcher.class); private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); //retry count + 1 AtomicInteger retryCount = passwordRetryCache.get(username); if(retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw logger.info("===========尝试超过5次=============="); throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry count passwordRetryCache.remove(username); } return matches; }}
UserRealm:Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;
UserController:用户登录功能
@Controllerpublic class UserController { @RequestMapping("DoLogin") public ModelAndView login(String username,String password){ UsernamePasswordToken token = new UsernamePasswordToken(username,password); Subject subject = SecurityUtils.getSubject(); if(subject.isAuthenticated()){ return new ModelAndView("admin"); } try { subject.login(token); if(subject.isAuthenticated()){ return new ModelAndView("admin"); } }catch (Exception e){ } return new ModelAndView("login"); }}
源码请点击:https://github.com/smallleaf/cacheWeb
浏览器输入http://localhost:8080/DoLogin?username=admin&password=admin
断点打到DelegatingFilterProxy.doFilter。和UserController.login
我们走起
请求肯定是会先经过DelegatingFilterProxy进行shiro过滤的过滤功能的。和上篇一样。
这里我们分析,为什么没有登录的时候。subject.getPrincipal() ==null。
我们debug进入AbstractShiroFIlter.doFilterInternal。
final Subject subject = createSubject(request, response);
最后进入
public Subject createSubject(SubjectContext subjectContext) { //create a copy so we don't modify the argument's backing map: SubjectContext context = copy(subjectContext); //ensure that the context has a SecurityManager instance, and if not, add one: context = ensureSecurityManager(context); //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the //process is often environment specific - better to shield the SF from these details: context = resolveSession(context); //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first //if possible before handing off to the SubjectFactory: context = resolvePrincipals(context); Subject subject = doCreateSubject(context); //save this subject for future reference if necessary: //(this is needed here in case rememberMe principals were resolved and they need to be stored in the //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation). //Added in 1.2: save(subject); return subject; }
这里上篇已经分析了。我们要分析的地方就是context = resolvePrincipals(context);为什么subject.getPrincipal() ==null。
进入
public PrincipalCollection resolvePrincipals() { PrincipalCollection principals = getPrincipals(); if (CollectionUtils.isEmpty(principals)) { //check to see if they were just authenticated: AuthenticationInfo info = getAuthenticationInfo(); if (info != null) { principals = info.getPrincipals(); } } if (CollectionUtils.isEmpty(principals)) { Subject subject = getSubject(); if (subject != null) { principals = subject.getPrincipals(); } } if (CollectionUtils.isEmpty(principals)) { //try the session: Session session = resolveSession(); if (session != null) { principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY); } } return principals; }
PrincipalCollection principals = getPrincipals();没有登录这里一定null。
AuthenticationInfo info = getAuthenticationInfo();同样也null。
Subject subject = getSubject();同样为null。未创建嘛。
Session session = resolveSession();如果这里不是第一次请求这个网站那么此时session不为null。为什么我上篇已经分析了。这里再提一下,第一次服务器返回一个session给客户端,并且缓存到服务器,以后每次客户端都是拿这个sessionId去请求。
session不为null。那么
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);我们从session中取出PRINCIPALS_SESSION_KEY。
如果PRINCIPALS_SESSION_KEY该session中存在改值,是不是principals不为null。那么是不是意味着已经登录。
于是接下来,我们的任务就是找到哪里,保存这个principals到session中,并且缓存下来,这样我们的目的就已经达到了。
未登录当然是找不到的。
登录请求链接是不会被shiro所拦截的,因此执行完这个基本的过滤器,之后就返回到我们登录的接口了。
subject.login(token);
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals: principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } if (principals == null || principals.isEmpty()) { String msg = "Principals returned from securityManager.login( token ) returned a null or " + "empty value. This value must be non null and populated with one or more elements."; throw new IllegalStateException(msg); } this.principals = principals; this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; } Session session = subject.getSession(false); if (session != null) { this.session = decorate(session); } else { this.session = null; } }
主要看:Subject subject = securityManager.login(this, token);
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { try { onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; //propagate } Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn; }
info = authenticate(token);就是进行登录信息的验证。
Subject loggedIn = createSubject(token, info, subject);登录成功后,创建subject,那么就是这里进行session保存的。
先看如何进行身份验证。
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
这里我们的Realm只有UserRealm。所以执行:doSingleRealmAuthentication
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } return info; }
这里回头看看UserRealm。看看它的继承链
public class UserRealm extends AuthorizingRealmpublic abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware
AuthenticationInfo info = realm.getAuthenticationInfo(token);
其实就是父类AuthenticatingRealm.getAuthenticationInfo
private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) { AuthenticationInfo info = null; Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache(); if (cache != null && token != null) { log.trace("Attempting to retrieve the AuthenticationInfo from cache."); Object key = getAuthenticationCacheKey(token); info = cache.get(key); if (info == null) { log.trace("No AuthorizationInfo found in cache for key [{}]", key); } else { log.trace("Found cached AuthorizationInfo for key [{}]", key); } } return info; }
先从缓存中获取这Realm信息,如果没有就执行.
info = doGetAuthenticationInfo(token);这里doGetAuthenticationInfo是一个抽象方法也就是UserRealm实现的。取到这个认证信息后。进行信息比较
assertCredentialsMatch(token, info);
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { //not successful - throw an exception to indicate this: String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + "credentials during authentication. If you do not wish for credentials to be examined, you " + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }
首先获得凭证匹配器。也就是我们自定义的RetryLimitHashedCredentialsMatcher。
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); //retry count + 1 AtomicInteger retryCount = passwordRetryCache.get(username); if(retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw logger.info("===========尝试超过5次=============="); throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry count passwordRetryCache.remove(username); } return matches; }
其实就是在进行比较时,做了一些个性化操作。
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
这里我就不分析了,无非就是进行认证信息加密啥的,然后和服务器也就是Realm返回的信息比对,看看是不是登录成功了。
这里我们假设用户名密码正确返回ture。
认证成功就到了Subject loggedIn = createSubject(token, info, subject);
public Subject createSubject(SubjectContext subjectContext) { //create a copy so we don't modify the argument's backing map: SubjectContext context = copy(subjectContext); //ensure that the context has a SecurityManager instance, and if not, add one: context = ensureSecurityManager(context); //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the //process is often environment specific - better to shield the SF from these details: context = resolveSession(context); //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first //if possible before handing off to the SubjectFactory: context = resolvePrincipals(context); Subject subject = doCreateSubject(context); //save this subject for future reference if necessary: //(this is needed here in case rememberMe principals were resolved and they need to be stored in the //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation). //Added in 1.2: save(subject); return subject; }
又回到了这里。
public Subject createSubject(SubjectContext context) { if (!(context instanceof WebSubjectContext)) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); 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); }
createSubject就是初始化一些信息,到WebDelegatingSubject然后返回。
重点到 save(subject);这里就是我们要缓存session的地方了。
protected void save(Subject subject) { this.subjectDAO.save(subject); } public Subject save(Subject subject) { if (isSessionStorageEnabled(subject)) { saveToSession(subject); } else { log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " + "authentication state are expected to be initialized on every request or invocation.", subject); } return subject; } protected void saveToSession(Subject subject) { //performs merge logic, only updating the Subject's session if it does not match the current state: mergePrincipals(subject); mergeAuthenticationState(subject); }
mergePrincipals(subject);也就是保存principals的地方。
protected void mergePrincipals(Subject subject) { //merge PrincipalCollection state: PrincipalCollection currentPrincipals = null; //SHIRO-380: added if/else block - need to retain original (source) principals //This technique (reflection) is only temporary - a proper long term solution needs to be found, //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible // //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 + if (subject.isRunAs() && subject instanceof DelegatingSubject) { try { Field field = DelegatingSubject.class.getDeclaredField("principals"); field.setAccessible(true); currentPrincipals = (PrincipalCollection)field.get(subject); } catch (Exception e) { throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e); } } if (currentPrincipals == null || currentPrincipals.isEmpty()) { currentPrincipals = subject.getPrincipals(); } Session session = subject.getSession(false); if (session == null) { if (!CollectionUtils.isEmpty(currentPrincipals)) { session = subject.getSession(); session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } //otherwise no session and no principals - nothing to save } else { PrincipalCollection existingPrincipals = (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if (CollectionUtils.isEmpty(currentPrincipals)) { if (!CollectionUtils.isEmpty(existingPrincipals)) { session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); } //otherwise both are null or empty - no need to update the session } else { if (!currentPrincipals.equals(existingPrincipals)) { session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } //otherwise they're the same - no need to update the session } } }
currentPrincipals = subject.getPrincipals();返回当前的principals。
Session session = subject.getSession(false);不创建session。直接获取。
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
这里是不是很眼熟。我们回到上面过滤是取principals的地方。
if (CollectionUtils.isEmpty(principals)) { //try the session: Session session = resolveSession(); if (session != null) { principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY); } }
这里就把认证成功的信息保存到了session当中,我们只需要知道缓存的地方就可以。
我们这里提到的session已经不是sevlert中原生态的session了。已经是被shiro给封装过了的。
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);public void setAttribute(Object attributeKey, Object value) throws InvalidSessionException { if (value == null) { removeAttribute(attributeKey); } else { sessionManager.setAttribute(this.key, attributeKey, value); } }public void setAttribute(SessionKey sessionKey, Object attributeKey, Object value) throws InvalidSessionException { if (value == null) { removeAttribute(sessionKey, attributeKey); } else { Session s = lookupRequiredSession(sessionKey); s.setAttribute(attributeKey, value); onChange(s); } } protected void onChange(Session session) { sessionDAO.update(session); } public void update(Session session) throws UnknownSessionException { doUpdate(session); if (session instanceof ValidatingSession) { if (((ValidatingSession) session).isValid()) { cache(session, session.getId()); } else { uncache(session); } } else { cache(session, session.getId()); } }
这里我们已经发现,缓存更新了session。
那么当我们访问http://localhost:8080/admin
会根据sessionId,只要不换浏览器不清缓存,不退出登录,session不过期。那么这个sessionId有效,浏览器发起请求会根据这个sessionId从缓存中取出session。再调用
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
再进行登录判断:
return subject.getPrincipal() != null;
返回ture。跳到请求页面。
上篇和这一篇,相信大家应该很请求shiro这个认证过程如何实现。无非就是根据sessionId去缓存中取session,判断是否已经登录,登录返回请求页面。失败进入登录页面。
登录成功保存session到缓存中,下次用户即取出已经登录的session
了。
下篇着重讲解,shiro如何使用ehcache缓存的。做个简单例子,用一个map做个类似的简单缓存进行替换。
下下篇就用redis进行替换,解决session跨域问题。
菜鸟不易,望有问题指出,共同进步。
- shiro源码分析篇3:用户登录缓存登录信息
- Shiro源码分析----登录流程
- shiro源码分析篇2:请求过滤,登录判断
- Shiro实现用户登录
- shiro登录过程分析
- shiro登录过程分析
- Shiro实现用户自动登录
- shiro控制用户唯一登录
- 登录用户缓存试验
- Shiro 登录认证源码详解
- Shiro登录验证源码解析
- shiro源码分析篇4:自定义缓存
- springmvc用户登录信息
- 查看用户登录信息
- springmvc+shiro用户登录后获取用户
- 用户登录源码
- 用户登录:Spring Security3源码分析-UsernamePasswordAuthenticationFilter分析
- 4、Shiro+Oracle实现用户登录认证
- MySQLWorkbench根据已有EER图导出sql脚本文件
- 新疆大学(新大)OJ xju 1010: 四个年级 C++ STL map 将4层循环优化成2层循环可解
- 洛谷P3933 Chtholly Nota Seniorious 【二分 + 贪心 + 矩阵旋转】
- Ubuntu14.04安装OpenCV
- 常见清除浮动方法总结
- shiro源码分析篇3:用户登录缓存登录信息
- 个人总结11
- Ubuntu 16.04 系统快捷键推荐设置
- Go语言switch语句
- Catalan数
- linux1
- Visual Studio 2010 + MPI + 环境搭建
- Android驱动模型(kernel-hal-framework-app)
- 6.3 代理的部署位置