使用Shiro框架进行权限控制,在处理登录逻辑时我们一般是这样做的:
(1)在web.xml中使用filter指定shiro拦截哪些请求:
XHTML
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
shiroFilter
*.html
*.json
*.jsp
REQUEST
FORWARD
(2)controller中使用Subject的login方法进行登录:
Java /**
* 登录校验
*
* @param username
* 用户名
* @param password
* 密码
* @param request
* @param redirectAttributes
* @return 重定向到主页
*/
@RequestMapping("/user/user/check.html")
public ModelAndView loginCheck(@RequestParam("username") String username, @RequestParam("password") String password,
HttpServletRequest request, RedirectAttributes redirectAttributes) {
String msg = "";
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, EncryptionUtil.sha256Hex(password));
try {
subject.login(token);
UsrUser userBO = userManager.selectByName(username);
// request.getSession().setAttribute("userBO", userBO); // 登录成功之后加入session中
subject.getSession().setAttribute("USERNAME", userBO.getUsername());
ModelAndView mAndView = new ModelAndView("redirect:/user/index.html");
return mAndView;
} catch (IncorrectCredentialsException e) {
msg = "登录密码错误. Password for account " + token.getPrincipal() + " was incorrect.";
System.out.println(msg);
} catch (ExcessiveAttemptsException e) {
msg = "登录失败次数过多";
System.out.println(msg);
} catch (LockedAccountException e) {
msg = "帐号已被锁定. The account for username " + token.getPrincipal() + " was locked.";
System.out.println(msg);
} catch (DisabledAccountException e) {
msg = "帐号已被禁用. The account for username " + token.getPrincipal() + " was disabled.";
System.out.println(msg);
} catch (ExpiredCredentialsException e) {
msg = "帐号已过期. the account for username " + token.getPrincipal() + " was expired.";
System.out.println(msg);
} catch (UnknownAccountException e) {
msg = "帐号不存在. There is no user with username of " + token.getPrincipal();
System.out.println(msg);
} catch (UnauthorizedException e) {
msg = "您没有得到相应的授权!" + e.getMessage();
System.out.println(msg);
}
redirectAttributes.addFlashAttribute("error", "登录失败,请重新登录");
return new ModelAndView("redirect:/user/user/login.html");
}
(3)在自定义的Realm中通过doGetAuthenticationInfo方法将数据库中正确的账号、密码注入到Shiro管理:
Javapackage cn.zifangsky.shiro;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import cn.zifangsky.manager.UsrUserManager;
import cn.zifangsky.model.UsrFunc;
import cn.zifangsky.model.UsrUser;
import cn.zifangsky.model.bo.UsrRoleBO;
import cn.zifangsky.model.bo.UsrUserBO;
public class CustomRealm extends AuthorizingRealm {
@Resource(name = "usrUserManager")
private UsrUserManager userManager;
/**
* 注入权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = null;
UsrUserBO userBO = (UsrUserBO) principalCollection.fromRealm(getName()).iterator().next();
if(userBO != null){
info = new SimpleAuthorizationInfo();
List roleBOs = userBO.getUsrRoleBOs();
if(roleBOs != null && roleBOs.size() > 0){
List roleNames = new ArrayList<>(); //所有角色名
Set funcCodes = new HashSet<>(); //所有权限的code集合
for(UsrRoleBO roleBO : roleBOs){
roleNames.add(roleBO.getRolename());
List funcs = roleBO.getFuncs();
if(funcs != null && funcs.size() > 0){
for(UsrFunc f : funcs){
funcCodes.add(f.getCode());
}
}
}
info.addRoles(roleNames);
info.addStringPermissions(funcCodes);
}
}
return info;
}
/**
* 注入数据库中的认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); //获取用户名
UsrUser userBO = userManager.selectByName(username);
SimpleAuthenticationInfo info = null;
if(userBO != null){
info = new SimpleAuthenticationInfo(userBO, userBO.getPassword(), getName());
}
return info;
}
}
从上面的代码可以看出,我们在controller中使用Subject的login方法将前台表单填写的账号密码传递到了shiro中,接着又在自定义的Realm中通过doGetAuthenticationInfo方法将数据库中正确的账号、密码注入到Shiro。那么在后面的流程中Shiro又是在哪里进行的密码校验工作呢?
下面,我将通过debug操作跟大家一起大致分析下:
在doGetAuthenticationInfo方法的某一行打上断点,开始单步调试
程序继续执行AuthenticatingRealm类的getAuthenticationInfo方法:
在这里,程序从568行开始继续执行,一直到578行调用当前类中的assertCredentialsMatch方法进行密码校验:
很显然,在controller中登录时,使用了UsernamePasswordToken也就是AuthenticationToken的子类来接收登录参数,同时在CustomRealm中的doGetAuthenticationInfo方法中返回值是AuthenticationInfo的子类——SimpleAuthenticationInfo
因此,现在我们就可以得知: 在这里的assertCredentialsMatch方法校验密码时,token参数包含了登录表单传递进来的待认证的信息,而info参数则是数据库中正确的账号信息
接着,在下面的代码中通过getCredentialsMatcher方法获取了一个CredentialsMatcher对象,然后再调用doCredentialsMatch方法来校验token和info里面的密码字段是否一样,如果一样则不做其他额外操作,不一样时则抛出异常。当然,我们可以在controller中捕捉到这个密码校验失败的异常
那么问题来了,在这里getCredentialsMatcher方法返回的那个CredentialsMatcher到底是从哪里传递进来的呢?
可以发现,当前类中的getCredentialsMatcher方法返回的是一个名为“credentialsMatcher”的属性,接着通过观察源码中的构造方法可以发现shiro给credentialsMatcher属性默认传递是一个 SimpleCredentialsMatcher对象
最后,我们再通过源码看看SimpleCredentialsMatcher这个类里面的doCredentialsMatch方法到底是如何来进行密码比较的:
可以看出,最终还是调用的SimpleCredentialsMatcher这个类里面的equals方法比较的两个密码字段。当然,在这个equals方法里面由于考虑的情况比较多,因此最终的判断逻辑也是比较复杂的
总结:
从上面的一系列的流程可以看出,整个登录流程中的密码校验逻辑其核心是: 在SimpleCredentialsMatcher这个类的doCredentialsMatch方法中分别将待认证的密码以及数据库中的正确密码分别从token和info中取出,再判断二者是否一样,如果不一样就返回false,最后在AuthenticatingRealm这个类的assertCredentialsMatch方法中抛出IncorrectCredentialsException异常
因此,在知道了完整的密码判断逻辑之后,同时我们又知道待认证的密码和数据库中的密码都是简单的字符串。那么我们完全可以在自定义Realm中复写父类AuthenticatingRealm的assertCredentialsMatch方法,直接获取token和info中的两个密码并比较,如果二者不一样同样抛出IncorrectCredentialsException异常即可
具体的代码如下:
Javapackage cn.zifangsky.shiro;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import cn.zifangsky.manager.UsrUserManager;
import cn.zifangsky.model.UsrFunc;
import cn.zifangsky.model.UsrUser;
import cn.zifangsky.model.bo.UsrRoleBO;
import cn.zifangsky.model.bo.UsrUserBO;
public class CustomRealm extends AuthorizingRealm {
@Resource(name = "usrUserManager")
private UsrUserManager userManager;
/**
* 注入权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = null;
UsrUserBO userBO = (UsrUserBO) principalCollection.fromRealm(getName()).iterator().next();
if(userBO != null){
info = new SimpleAuthorizationInfo();
List roleBOs = userBO.getUsrRoleBOs();
if(roleBOs != null && roleBOs.size() > 0){
List roleNames = new ArrayList<>(); //所有角色名
Set funcCodes = new HashSet<>(); //所有权限的code集合
for(UsrRoleBO roleBO : roleBOs){
roleNames.add(roleBO.getRolename());
List funcs = roleBO.getFuncs();
if(funcs != null && funcs.size() > 0){
for(UsrFunc f : funcs){
funcCodes.add(f.getCode());
}
}
}
info.addRoles(roleNames);
info.addStringPermissions(funcCodes);
}
}
return info;
}
/**
* 注入数据库中的认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); //获取用户名
UsrUser userBO = userManager.selectByName(username);
SimpleAuthenticationInfo info = null;
if(userBO != null){
info = new SimpleAuthenticationInfo(userBO, userBO.getPassword(), getName());
}
return info;
}
/**
* 实际做密码校验的逻辑
*/
@Override
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info)
throws AuthenticationException {
//表单中的密码
String tokenCredentials = String.valueOf((char[])token.getCredentials());
//数据库中的密码
String accountCredentials = (String)info.getCredentials();
//认证失败,抛出密码密码不正确的异常
if(!accountCredentials.equals(tokenCredentials)){
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
}
}
注:至于为什么从token中获取密码和从info中获取密码的方式不一样,大家在debug模式下自行观察token和info中的密码存放形式就明白了
至此,我的整个分析过程就结束了。感兴趣的同学可以自行使用debug模式观察下整个登录流程,用于加深映像!