1. 程式人生 > 實用技巧 >理解一下Shiro的登入過程

理解一下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的登入邏輯基本完成。