shiro认证流程分析

        在上一篇博文 springboot整合shiro入门 中,简单介绍了如何使用shiro进行认证和授权,下面通过debug的方式(示例代码还是上一篇博客使用的代码),分析一下shiro是如何进行认证的:

      首先,回顾一下,处理登录的方法:

    @PostMapping("/doLogin")
    @ResponseBody
    public String doLogin(String username,String password){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            return "登录成功";
        }catch (UnknownAccountException|IncorrectCredentialsException e){
            e.printStackTrace();
            return "账号或密码错误";
        }catch (LockedAccountException e){
            e.printStackTrace();
            return "账号已被锁定,请联系管理员";
        }catch (AuthenticationException e){
            e.printStackTrace();
            return "未知异常,请联系管理员";
        }

    }

可以发现,登录过程,调用的方法是:

subject.login(token);

这里要debug的话,有两种方式:

(1)通过一步步debug跟进去看看它做了什么,这种方法比较通用,但是如果跟得很深的话,很容易晕。。。

(2)另外一种就是比较取巧的方法,因为认证,肯定是要拿到用户名和密码进行比较的,而我们的用户名和密码都封装到了          UsernamePasswordToken这个类中,那么我们完全可以把断点打在UsernamePasswordToken的获取用户名和密码的方法上,然后看看方法调用栈即可知道其调用了哪些类的哪些方法了:

 

现在就可以启动项目,访问登录接口进行登录认证了,然后发现很顺利的进入了获取用户名的断点了:

那么,我们从doLogin的subject.login(token)方法,一路看到UsernamePasswordToken获取用户名的方法,看看它做了什么:

1. 首先是调用主体subject的login方法,把UsernamePasswordToken作为参数传入进去了:

subject.login(token);

2.进入到 DefaultSecurityManager 的login方法:

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

其中,info就是要返回的认证信息,看一下DefaultSecurityManager的继承关系,会发现它继承了SessionsSecurityManager,而SessionsSecurityManager继承了AuthenticatingSecurityManager,所以接下来它调用的是父类的认证方法

3.调用父类AuthenticatingSecurityManager的authenticate()方法:

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

发现它调用的是authenticator的authenticate()方法,那么authenticator是什么呢?它其实是ModularRealmAuthenticator,在构造方法里面进行了初始化:

4. 调用ModularRealmAuthenticator的doAuthenticate()方法:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        //获取所有的realm
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

它会先获取所有的realm,然后判断,如果只有个一个realm的话,就执行doSingleRealmAuthentication()方法,否则的话就执行doMultiRealmAuthentication()方法,这里因为只有一个realm,所以执行的doSingleRealmAuthentication

5.调用ModularRealmAuthenticator的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;
    }

可以发现,它会调用:

AuthenticationInfo info = realm.getAuthenticationInfo(token);

这里的realm,就是我们自定义的realm,而realm.getAuthenticationInfo(token);会先从缓存中取用户信息,如果没有,就会调用doGetAuthenticationInfo()方法:

info = doGetAuthenticationInfo(token);

而这个doGetAuthenticationInfo()就是我们自定义realm时重写的方法。

6.执行自定义realm的doGetAuthenticationInfo方法,获取用户信息:

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        //这里模拟数据库查询用户,根据用户名查询
        User dbUser = userService.getUserByUsername(username);
        if (dbUser == null){
            //账号不存在
            throw new UnknownAccountException();
        }
        if (dbUser.getEnable()==0){
            //账号被锁定
            throw new LockedAccountException();
        }
        return new SimpleAuthenticationInfo(dbUser, dbUser.getPassword(), getName());
    }

到了这里,就会根据前端传入的用户名到数据库查询用户信息,封装成SimpleAuthenticationInfo返回。

其实,现在只是验证了该用户是否存在,以及账号是否被锁定等,并没有通过验证,因为密码都还没有比对。

所以放开获取用户名的断点,看看接下来的密码是如何比对的,现在到了获取密码的这个断点:

 

回到AuthenticatingRealm的getAuthenticationInfo()方法,

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //从缓存中获取用户信息

        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //如果缓存中没有,则调用自定义realm的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;
    }

可以看到,有这么一行代码:

//断言密码是匹配的
 assertCredentialsMatch(token, info);

那么,它肯定会在这里进行密码比对的,判断密码是否正确,我们跟下去看一下assertCredentialsMatch()方法:

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

其中,cm是SimpleCredentialsMatcher类的实例,所以会调用它的doCredentialsMatch进行密码匹配:

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        //获取前端传入的密码,也就是用户输入的密码
        Object tokenCredentials = getCredentials(token);
        //获取数据库或者内存中存储的密码
        Object accountCredentials = getCredentials(info);
        //比对两个密码,判断是否一致
        return equals(tokenCredentials, accountCredentials);
    }

       这里思路也很清晰,就是获取用户输入的密码和存储的密码,然后比较两个密码是否相同。如果密码相同,那么身份认证就成功完成了。否则就会抛出异常。

 

到这里,就完成了身份认证。

简单总结一下认证流程:

1、调用Subject.login()方法
2、委托给DefaultSecurityManager的login方法
3、DefaultSecurityManager进一步委托给ModularRealmAuthenticator,调用其doAuthenticate()方法:
4、ModularRealmAuthenticator则会获取到所有的realm,来获取用户信息
5、调用具体的realm的getAuthenticationInfo获取用户信息,缓存有则返回,否则通过doGetAuthenticationInfo来获取用户信息
6、获取完用户信息之后,则会调用SimpleCredentialsMatcher的doCredentialsMatch()方法进行密码匹配,如果密码相同,那么身份认证就成功完成了,否则就会抛出异常。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值