Shiro权限管理6.Shiro认证思路分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Shiro权限管理6.Shiro认证思路分析相关的知识,希望对你有一定的参考价值。


下面来说一下Shiro的认证。
如何来做Shiro的认证呢?首先回顾一下之前剖析的Shiro的HelloWorld程序中有关认证的部分代码:

//获取当前的Subject
Subject currentUser = SecurityUtils.getSubject();
//测试当前用户是否已经被认证(即是否已经登录)
if (!currentUser.isAuthenticated())
//将用户名与密码封装为UsernamePasswordToken对象
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);//记录用户
try
currentUser.login(token);//调用Subject的login方法执行登录
catch (UnknownAccountException uae)
log.info("There is no user with username of " + token.getPrincipal());
catch (IncorrectCredentialsException ice)
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
catch (LockedAccountException lae)
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");

在上面的代码中,首先获取当前用户的Subject对象,然后通过Subject的isAuthenticated判断用户是否已经登录。如果没有登录,就需要创建一个UsernamePasswordToken进行账号密码的校验,在构造方法中传入账号和密码,然后将该token传入Subject的login方法即可进行校验。



那么要注意的是,账号和密码一般都是从前端的用户填写的form表单中获取的,验证用户登录是否成功,需要从数据库取出相关账号密码进行比对,才能确定用户是否可以登录系统。而在Shiro是如何通过UsernamePasswordToken进行账号密码校验的呢?


回顾一下之前的架构:


【Shiro权限管理】6.Shiro认证思路分析_SecurityManager


注意其中的Realm,在Shiro的架构中,负责和数据库交互的对象就是Realm对象。当校验账号与密码时,会使用Realm获取数据表中对应的记录,而比较密码的工作是Shiro来帮我们完成的(相关校验细节在下面)。



所以在Shiro中,完整的认证过程如下:


1.获取当前的Subject,调用SecurityUtils.getSubject()进行获取。


2.测试当前的用户是否已经被认证,即是否已经登录。调用Subject的isAuthenticated方法。


3.若没有被认证,则把用户名和密码封装为UsernamePasswordToken对象。其中用户名与密码的获取流程如下:


1)用户打开填写用户名密码的表单页面


2)用户输入完账号密码后点击“登录”,会将请求提交到SpringMVC的Handler(Controller方法)


3)在相关的Controller请求处理方法中,通过形参或者HttpServletRequest对象获取前端页面表单中的账号密码。


4.执行登录,调用Subject的login方法(参数为实现了AuthenticationToken接口的实现类,其中UsernamePasswordToken就是这样的类)。


5.自定义Realm的方法,从数据库获取对应的记录,返回给Shiro。


要实现自定义的Realm方法去获取数据库中的记录,具体过程如下:


1)创建类并实现Realm接口(授权/认证),如果我们只需要认证过程,继承AuthenticatingRealm类。


观察Realm接口的组成:


【Shiro权限管理】6.Shiro认证思路分析_Realm_02


在上面的结构中,Realm实现认证的实际上是org.apache.shiro.realm.AuthenticatingRealm类,


所以如果我们仅需要认证的话,单独继承AuthenticatingRealm类即可。


2)实现doGetAuthenticationInfo(AuthenticationToken)方法。


6.由Shiro进行密码校验。



Shiro如何通过传入的token进行密码校验的呢?又是如何利用我们自定义的Realm进行数据库密码匹配的呢?来查看一下源代码:


currentUser.login(token);


上面当前用户的Subject的login方法如下:


public void login(AuthenticationToken token) throws AuthenticationException 
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
//下面代码省略...

可以看到校验时是使用的securityManager的login方法。而在securityManager的login中:


public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException 
AuthenticationInfo info;
try
info = authenticate(token);
catch (AuthenticationException ae)
//下面代码省略...

//下面代码省略...

调用了其父类的authenticate方法:


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

可以看到其调用了认证器的authenticate方法,而在认证器的authenticate方法中调用了


doAuthenticate方法:


info = doAuthenticate(token);

而doAuthenticate逻辑如下所示:


public AuthenticationInfo doAuthenticate(AuthenticationToken token) throws AuthenticationException 
assertRealmsConfidured();
Collection<Realm> realms = getRealms();
if(realms.size() == 1 )
return doSingleRealmAuthentication(realms,iterator().next,authenticationToken);
else
return doMutiRealmAuthentication(realms,authenticationToken);

可以看到,在doAuthenticate方法中,首先会获取所有的Realm,然后根据Realm的数量来决定使用


单个Realm的校验方法,还是多个Realm的校验方法。


在配置了单个Realm时,调用的是doSingleRealmAuthentication方法,在该方法中会通过Realm根据token获取AuthenticationInfo对象:


AuthenticationInfo info = realm.getAuthenticationInfo(token);

而在getAuthenticationInfo方法中执行了doGetAuthenticationInfo方法来获取AuthenticationInfo


对象。


info = doGetAuthenticationInfo(token);

doGetAuthenticationInfo方法就是AuthenticationRealm抽象类的抽象方法,而该方法就是上面的校验过程中,实现认证的自定义Realm需要实现的方法。


所以在仅实现认证的情况下,创建的自定义Realm只需要单独继承AuthenticatingRealm类,并


实现其doGetAuthenticationInfo(AuthenticationToken)方法即可。



在下篇总结中,将动手实现一个简单的基于自定义Realm认证的登录认证实例。


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

以上是关于Shiro权限管理6.Shiro认证思路分析的主要内容,如果未能解决你的问题,请参考以下文章

基于url权限管理 shiro--基础

springmvc集成shiro登录失败处理

SpringBoot整合Shiro实现权限控制

SpringBoot整合Shiro实现权限控制

SpringBoot整合Shiro实现权限控制

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