16-java安全——shiro1.2.4用户认证流程分析

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了16-java安全——shiro1.2.4用户认证流程分析相关的知识,希望对你有一定的参考价值。

1. shiro安全框架

shiro 是apache开源的一个安全框架,它将软件系统的安全认证相关功能抽取出来,例如把用户身份安全认证,权限授权、加密、会话管理等功能组成了一个通用的安全认证框架,然后开发人员通过shiro提供的API接口可以使用身份认证,权限管理等功能来保护web应用程序。

shiro中有两个概念比较重要,一个是身份认证,另一个是授权:

身份认证就是判断一个用户是否为合法用户的处理过程。简单来理解就是系统通过用户输入的用户名和口令是否正确来校验用户的身份信息。

所谓授权就是访问控制,shiro在进行身份认证后需要分配权限来指定哪些系统资源可以访问,哪些系统资源不可以被访问,也就是控制能访问哪些资源

2. shiro的核心架构

Subject

Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,可以理解为主体就是当前用户,可以是一个通过浏览器请求的用户或者一个运行的用户认证的程序。Subject在shiro中是一个接口,定义了很多认证授相关的方法,外部程序会先通过subject进行认证授,而subject则通过SecurityManager安全管理器进行认证授权。

SecurityManager

SecurityManager即安全管理器,主要对所有subject进行安全管理,它是shiro的核心,通过SecurityManager可以完成subject的认证、授权等操作。

Authenticator

Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,实质上SecurityManager安全管理器会通过Authenticator来进行认证,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。

Authorizer

Authorizer即授权器,用户认证通过时,访问某些资源SessionManager需要通过Authorizer进行授权,判断用户是否有此功能的操作权限。

Realm

Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。

CacheManager和Cryptography

CacheManager即缓存管理,将用户权限数据存储在缓存提高性能。

Cryptography即密码管理,shiro提供了一套加密和解密的组件,例如散列、加/解密等功能。

3. shiro中的认证

shiro认证的关键对象:

Subject(主体):访问系统的用户,例如用户认证的程序都可以成为主体

Principal(身份信息):是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。

credential(凭证信息): 是只有主体自己知道的安全信息,如密码、证书等。

shiro认证流程如下图所示:

4. 编写第一个shiro程序

新建一个maven项目,引入shiro的依赖:

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.4</version>
        </dependency>

在项目的resources目录下创建一个ini文件存储用户身份数据

示例程序

package com.test;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;


public class ShiroTest1 
    public static void main(String[] args) 
        //1. 创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //2. 给安全管理器设置Realm
        IniRealm iniRealm = new IniRealm("classpath:user.ini");
        securityManager.setRealm(iniRealm);
        //3. SecurityUtils给全局安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        //4.关键对象subject主体
        Subject subject = SecurityUtils.getSubject();
        //5.创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
        //6. 进行身份认证
        try 
            subject.login(token);
        catch (UnknownAccountException e)
            System.out.println("用户名错误");
            e.printStackTrace();
        catch (IncorrectCredentialsException e)
            System.out.println("密码错误");
            e.printStackTrace();
        
    

如果程序运行没有抛异常说明没有问题。

5. shiro认证流程分析

现在我们来分析一下shiro的认证流程,因为在代码中是调用了Subject类的login方法来认证的,因此我们从login方法开始分析。

DelegatingSubject类的login方法实际上是调用了DefaultSecurityManager安全管理器的login方法来进行用户认证

    public void login(AuthenticationToken token) throws AuthenticationException 
        clearRunAsIdentitiesInternal();
		//用户认证
        Subject subject = securityManager.login(this, token);

		//省略部分代码......

    

继续跟进DefaultSecurityManager的login方法,可以看到login方法内部调用了authenticate方法进行认证,实质上SecurityManager安全管理器会使用Authenticator认证器来进行认证

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException 
        AuthenticationInfo info;
        try 
			//调用了一个authenticate方法进行认证
            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;
    

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

    public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException 

        if (token == null) 
            throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
        

        log.trace("Authentication attempt received for token []", token);

        AuthenticationInfo info;
        try 
			//调用了doAuthenticate方法认证
            info = doAuthenticate(token);
            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);
            
            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;
    

调用了ModularRealmAuthenticator类的doAuthenticate方法进行认证

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException 
		assertRealmsConfigured();
		//拿到realm
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) 
			//这里会调用doSingleRealmAuthentication方法
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
         else 
            return doMultiRealmAuthentication(realms, authenticationToken);
        
    

 assertRealmsConfigured是一个断言函数,用于校验是否配置了Realm,然后调用getRealms方法获取Realm,接着根据realms的个数来调用不同的方法,由于在程序中我们只配置了一个realm,因此这个if判断会满足条件调用doSingleRealmAuthentication方法。

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) 
        //是否支持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调用了AuthenticatingRealm类的getAuthenticationInfo方法获取认证信息

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException 
		//先从缓存中获取认证信息
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) 
            //如果缓存为空,则会调用doGetAuthenticationInfo方法进行认证
            info = doGetAuthenticationInfo(token);
            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;
    

调用了SimpleAccountRealm类的doGetAuthenticationInfo方法

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException 
        //取出token信息
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //进行用户名校验
		SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) 
			//用户名是否被锁定
            if (account.isLocked()) 
                throw new LockedAccountException("Account [" + account + "] is locked.");
            
			//密码是否过期
            if (account.isCredentialsExpired()) 
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            

        

        return account;
    

经过多层调用,最终调用了SimpleAccountRealm类的doGetAuthenticationInfo方法来进行用户认证,但是该方法只完成了用户名的校验。然后返回,调用AuthenticatingRealm类的cacheAuthenticationInfoIfPossible方法把用户认证信息放到缓存中。

调用assertCredentialsMatch方法进行密码认证

    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException 
        //拿到密码匹配器
		CredentialsMatcher cm = getCredentialsMatcher();
        if (cm != null) 
			//如果密码匹配器不为空,则调用doCredentialsMatch方法进行密码校验
            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.";
                //如果密码认证不通过抛出IncorrectCredentialsException异常
                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.");
        
    

在doCredentialsMatch方法中调用了equals方法进行匹配

从流程分析可知,最终进行用户名认证的实际上是在SimpleAccountRealm类的doGetAuthenticationInfo方法进行的,而密码的认证则是在AuthenticatingRealm类的assertCredentialsMatch方法中完成的。并且SimpleAccountRealm类继承自抽象类AuthorizingRealm,如果要实现自定义的认证流程的话,例如对数据库的用户进行认证,就可以自定义一个Realm类继承AuthorizingRealm类重写doGetAuthenticationInfo就可以实现了。

在示例程序中创建了一个默认的安全管理器DefaultSecurityManager来设置Realm,关于安全管理器的作用前面已经介绍过了,然后使用了全局的安全工具类SecurityUtils来设置安全管理器和获取主体Subject。再通过主体Subject调用login方法进行用户认证,需要明白的一点是,这里所谓的获取主体Subject其实就是获取当前的认证的用户(也可以是认证的程序)。

6. 关于shiro认证中的Subject主体

Subject主体在shiro中有着非常重要的作用,为什么要创建Subject主体来进行用户认证?

在Subject接口的注释中有两句话很重要

 大概的意思就是:Subject是 Shiro 的单用户安全功能的主要机制,Shiro把应用程序中用户的状态和安全操作,例如身份验证(登录/注销)、授权(访问控制)和会话访问等等这些操作都抽象成了一个Subject,然后通过Subject来完成这些安全操作。

这也是为什么程序需要通过 SecurityUtils安全工具类的getSubject方法来获取Subject来调用login方法完成用户认证操作。

参考资料:

https://shiro.apache.org/documentation.html

https://blog.csdn.net/A233666/article/details/113436604

以上是关于16-java安全——shiro1.2.4用户认证流程分析的主要内容,如果未能解决你的问题,请参考以下文章

16-java安全——shiro1.2.4用户认证流程分析

16-java安全——shiro1.2.4用户认证流程分析

17-java安全——shiro1.2.4反序列化分析(CVE-2016-4437)

17-java安全——shiro1.2.4反序列化分析(CVE-2016-4437)

17-java安全——shiro1.2.4反序列化分析(CVE-2016-4437)

17-java安全——shiro1.2.4反序列化分析(CVE-2016-4437)