shiro 认证

一、背景

       我们可以使用 shiro 进行认证操作,下面粘贴的是 LoginController 的代码,模拟用户登录的请求操作:

@Controller
@Slf4j
public class LoginController {

	@RequestMapping("/login")
	public String login(User user) {
		if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
			return "error";
		}
		//用户认证信息
		Subject subject = SecurityUtils.getSubject();
		UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
				user.getUserName(),
				user.getPassword());
		usernamePasswordToken.setRememberMe(true);
		try {
			//进行验证,这里可以捕获异常,然后返回对应信息
			subject.login(usernamePasswordToken);
		} catch (UnknownAccountException e) {
			log.error("用户名不存在!", e);
			return "error";
		} catch (AuthenticationException e) {
			log.error("账号或密码错误!", e);
			return "error";
		} catch (AuthorizationException e) {
			log.error("没有权限!", e);
			return "error";
		}
		return "shiro_index";
	}
}

二、源码追踪分析       

       我们先获取用户输入的 userName passWord ,然后将 userName passWord 这两个参数传入到 UsernamePasswordToken 中获取 token 对象,最后调用SecurityUtils.getSubject() login() 方法将 token 传入做系统校验。

       我们进入源码查看下 login() 方法底层是如何实现的,可以看到主要还是调用了 securityManager 安全管理器的 login() 方法。

 public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        // 主要是这块的方法
        Subject subject = securityManager.login(this, token);

        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() 方法是如何实现的,可以看到在这个方法中定义了 AuthenticationInfo 对象来接收从 Relam 传来的认证信息。

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

        我们继续进入源码查看 authenticate() 方法是如何实现的,我们发现这个方法内部调用了 authenticator 对象的 authenticate() 方法。

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

       我们继续进入源码查看 authenticator 对象的 authenticate() 方法是如何实现的。我们发现在这个方法的内部调用了 doAuthenticate() 方法。

 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);
            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() 方法是如何实现的。我们发现在这个方法的内部调用了 doAuthenticate() 方法。 

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

       这个 assertRealmsConfigured() 方法是判断 relam 是否存在,若不存在就会抛出异常,它会根据 relam 的个数来判断执行哪个方法,我在上一篇文章中只定义了一个 relam ,所以它会执行 doSingleRealmAuthentication() 这个方法,并且会将 relam token 传入进去。

       我们继续进入源码查看  doSingleRealmAuthentication() 方法是如何实现的, 我们可以看到,在这里它会先判断 realm 是否支持 token ,若支持,则接下来执行 getAuthenticationInfo() 方法。

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

       我们继续进入源码查看  getAuthenticationInfo()  方法是如何实现的, getCachedAuthenticationInfo() 方法是从 shiro 缓存中读取用户信息,如果没有,才会从 relam 中获取,如果是第一次登录,缓存中指定没有我们的认证信息,所以会执行 doGetAuthenticationInfo() 这个方法。

 public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //otherwise not cached, perform the lookup:
            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;
    }

     我们继续进入源码查看  doGetAuthenticationInfo()  方法是如何实现的,我们发现其实现类有如下几个,其中 CustomRealm 就是我们自定义实现的。

       我们再来看一下,我们自定义的 CustomRealm 中的 doGetAuthenticationInfo() 代码 ,这个方法就是需要查询数据库中的数据并进行一个简单的校验,最后封装成 SimpleAuthorizationInfo 对象再返回去。

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		if(StringUtils.isEmpty(authenticationToken.getPrincipal())) {
			return null;
		}
		// 获取用户信息
		String userName = authenticationToken.getPrincipal().toString();
		
		User user = userService.selectByUserName(userName);
		// 用户是否存在
		if(user == null) {
			throw new UnknownAccountException();
		}
		// 是否激活
		if(user !=null && user.getStatus().equals("0")){
			throw new  DisabledAccountException();
		}
		// 是否锁定
		if(user!=null && user.getStatus().equals("3")){
			throw new  LockedAccountException();
		}
		// 若存在将此用户存放到登录认证info中,无需做密码比对shiro会为我们进行密码比对校验
		if(user !=null && user.getStatus().equals("1")){
			ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUserName()+ "salt");
			/** 这里验证authenticationToken和simpleAuthenticationInfo的信息,构造方法支持三个或者四个参数,
			*	第一个参数传入userName或者是user对象都可以。
			*	第二个参数传入数据库中该用户的密码(记得是加密后的密码)
			*	第三个参数传入加密的盐值,若没有则可以不加
			*	第四个参数传入当前Relam的名字
			**/
			SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, user.getPassword().toString(),credentialsSalt, getName());
			return simpleAuthenticationInfo;
		}
		return null;
	}

       截至到目前为止,我们算是获取到了认证信息了,接下来就是看下 shiro 是怎么进行认证的,我们返回去再看下 AuthenticatingRealm 类的 getAuthenticationInfo() 方法,我们可以看到,获取完信息之后就需要进行密码匹配,会调用 assertCredentialsMatch() 方法。

 public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //otherwise not cached, perform the lookup:
            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() 方法看下它是如何实现的,首先获取一个 CredentialsMatcher 对象,翻译成汉语就是凭证匹配器,这个类的作用就是将用户输入的密码以某种方式计算加密。之后会调用 doCredentialsMatch() 方法。

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

       我们进入到 doCredentialsMatch() 方法,我们可以看到,这里用 equals 方法对 token 中加密的密码和从数据中取出来的 info 中的密码进行对比,若相同则返回 true ,失败就返回 false ,并抛出 AuthenticationException ,并将 info 返回到 defaultSecurityManager 中。

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = hashProvidedCredentials(token, info);
        Object accountCredentials = getCredentials(info);
        return equals(tokenHashedCredentials, accountCredentials);
    }

三、常见异常

       1、DisabledAccountException:禁用的账号

       2、LockedAccountException:锁定的账号

       3、UnknownAccountException:错误的账号

       4、ExcessiveAttemptsException:登录失败次数过多

       5、IncorrectCredentialsException:错误的凭证

       6、ExpiredCredentialsException:过期的凭证

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值