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跨域问题。


菜鸟不易,望有问题指出,共同进步。

原创粉丝点击