理解一下Shiro的登录过程

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学、会话管理和可用于安全的任何应用程序。

Controller中的登录语句:

Subject subject = SecurityUtils.getSubject();//获取当前用户
//封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(account,pwd);
 try {
     subject.login(token);//执行登录方法,没有异常就说明ok了
 } catch (UnknownAccountException e) {//用户名不存在
     model.addAttribute("msg","用户名或密码错误");
return "front/login";
 } catch (IncorrectCredentialsException e){//密码错误
     model.addAttribute("msg","用户名或密码错误");
return "front/login";
 }catch (Exception e){
     e.printStackTrace();
return "front/login";
 }

首先是把用户名和密码封装到token中,UsernamePasswordToken是一个简单的用户名/密码认证令牌,有4个成员变量, 分别保存用户名,密码,记住我,主机地址。

然后通过SecurityUtils.getSubject()得到subject,Subject 是表示单个应用程序用户的状态和安全操作。这些操作包括身份验证(登录/注销)、授权(访问控制)和会话的访问。

SecurityUtils.getSubject():

public static Subject getSubject() {
    Subject subject = ThreadContext.getSubject(); //从线程上下文获取subject
    if (subject == null) { //如果为空则通过Subject.Builder()得到SecurityManager 
        subject = (new Subject.Builder()).buildSubject(); 
        ThreadContext.bind(subject); //线程上下文绑定subject,下次就可以直接获取
    }
    return subject;
}

由于在项目中配置的是DefaultWebSecurityManager,所以 Subject.Builder()会得到DefaultWebSecurityManager。下面是该类的继承关系:

 buildSubject()的方法会调用DefaultWebSecurityManager的父类DefaultSecurityManager的createSubject()方法:

public Subject createSubject(SubjectContext subjectContext) {
    //创建subjectContext的副本,避免对实参的修改
    SubjectContext context = copy(subjectContext);
    //确保上下文有一个SecurityManager实例,如果没有,则添加一个:
    context = ensureSecurityManager(context);
    //解析相关的会话(通常基于引用的会话ID),并将其放在前面的上下文中
    context = resolveSession(context);
    //解析Principals
    context = resolvePrincipals(context);
    //具体创建subject的方法doCreateSubject
    Subject subject = doCreateSubject(context);
    save(subject);
    return subject;
}

该方法会使用DefaultSubjectFactory来创建subject,最终返回一个DelegatingSubject( principals(身份标识) , authenticated(是否已被认证), host(主机), session(会话), sessionCreationEnabled(是否允许创建会话), securityManager(安全管理器,shiro的核心) ,与这些东西相绑定)。

最后通过subject(DelegatingSubject)的login方法完成登录功能。

接下来主要看看login的主要过程:

public void login(AuthenticationToken token) throws AuthenticationException {
    clearRunAsIdentitiesInternal();  
    Subject subject = securityManager.login(this, token); //调用securityManager的login
    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;
    }
}

securityManager.login(this, token):

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        info = authenticate(token); //调用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
    }

authenticate(token):

public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token); 
    }

authenticator默认实现是 ModularRealmAuthenticator

 public AuthenticatingSecurityManager() {
        super();
        this.authenticator = new ModularRealmAuthenticator();
    }

authenticator.authenticate(token) :  (ModularRealmAuthenticator继承自AbstractAuthenticator,也就是调用AbstractAuthenticator的authenticate()方法)

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    if (token == null) {
        throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
    }
    log.trace("Authentication attempt received for token [{}]", token);
    AuthenticationInfo info;
    try {
        info = doAuthenticate(token);  //调用doAuthenticate()方法
        if (info == null) {
            String msg = "No account information found for authentication token [" + token + "] by this " +
                "Authenticator instance.  Please check that it is configured correctly.";
            throw new AuthenticationException(msg);
        }
    } catch (Throwable t) {
        AuthenticationException ae = null;
        if (t instanceof AuthenticationException) {
            ae = (AuthenticationException) t;
        }
        if (ae == null) {
            //Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more
            //severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate:
            String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +
                "error? (Typical or expected login exceptions should extend from AuthenticationException).";
            ae = new AuthenticationException(msg, t);
            if (log.isWarnEnabled())
                log.warn(msg, t);
        }
        try {
            notifyFailure(token, ae);
        } catch (Throwable t2) {
            if (log.isWarnEnabled()) {
                String msg = "Unable to send notification for failed authentication attempt - listener error?.  " +
                    "Please check your AuthenticationListener implementation(s).  Logging sending exception " +
                    "and propagating original AuthenticationException instead...";
                log.warn(msg, t2);
            }
        }
        throw ae;
    }
    log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
    notifySuccess(token, info);
    return info;
}

doAuthenticate(token) (该方法由ModularRealmAuthenticator实现):

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();  
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) { //由于项目中只配置了一个Realm,因此会调用下面的方法
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

doSingleRealmAuthentication(realms.iterator().next(), authenticationToken):

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;
}

realm.getAuthenticationInfo(token):该方法由AuthenticatingRealm实现(自定义realm继承AuthorizingRealm,AuthorizingRealm继承AuthenticatingRealm)

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        //otherwise not cached, perform the lookup:
        info = doGetAuthenticationInfo(token); //这里就是调用自定义realm的doGetAuthenticationInfo()方法
        log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
        if (token != null && info != null) {
            cacheAuthenticationInfoIfPossible(token, info);  
        }
    } else {
        log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }

    if (info != null) {
        assertCredentialsMatch(token, info); //密码验证
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }

    return info;
}

doGetAuthenticationInfo:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("开始认证 "+new Date().toString());
    SimpleAuthenticationInfo info = null;
    String account = (String) token.getPrincipal();//得到账号
    Author user = authorService.queryAuthorByAccount(account);//根据账号查找对应用户
    if(user==null) throw new UnknownAccountException();
    info = new SimpleAuthenticationInfo(user.getName(), user.getPwd(), ByteSource.Util.bytes(user.getName()), getName());
    return info;
}

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.");
    }
}

CredentialsMatcher使用的是HashedCredentialsMatcher

@Bean(name="userRealm")
public UserRealm userRealm(){
    HashedCredentialsMatcher matcher =new HashedCredentialsMatcher();
    matcher.setHashIterations(2);
    matcher.setHashAlgorithmName("md5");
    return new UserRealm(matcher);
}

然后使用匹配器doCredentialsMatch(token, info),验证密码是否正确:

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = hashProvidedCredentials(token, info); //把当前用户传入的密码加密
    Object accountCredentials = getCredentials(info); //数据库中已经加密过的密码  
    return equals(tokenHashedCredentials, accountCredentials);//判断值是否一致,是则登录成功
}

至此,Shiro的登录逻辑基本完成。

 

posted on 2020-11-13 18:45  菜鸟向前冲冲冲  阅读(435)  评论(0编辑  收藏  举报