前言
之前写项目用了 Shiro 框架,来进行安全验证以及权限管理。当时项目赶得急,没怎么深入了解,只能说能跑能改,不过在使用的过程中发现 Shiro 确实很优秀。现在回过头来学习原理,读读源码,深入的学习下。·
本篇博文主要写的是关于使用 Shiro 起步时最重要的一块,找了一些资料,力求写得简单明了。
简介
Realm:域,Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro 。当配置 Shiro时,你必须至少指定一个 Realm ,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。
Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。如果缺省的 Realm 不能满足需求,你还可以插入代表自定义数据源的自己的 Realm 实现。
功能
Realm能做的工作主要有以下几个方面:
身份验证(
getAuthenticationInfo
方法)验证账户和密码,并返回相关信息权限获取(
getAuthorizationInfo
方法) 获取指定身份的权限,并返回相关信息令牌支持(
supports
方法)判断该令牌(Token)是否被支持令牌有很多种类型,例如:HostAuthenticationToken(主机验证令牌),UsernamePasswordToken(账户密码验证令牌)
这里主来说明一下关于前两点验证方面的逻辑,因为令牌一般用的都是 UsernamePasswordToken,哪怕用 HostAuthenticationToken,也没必要细讲,这个函数很少用到。
身份验证
我们看到第一个方法就是我们上面说的“验证账户和密码,并返回相关信息”的方法。从方法的名字上看,只有取得验证信息的意思,其实这里面还包括了进行验证的逻辑。
看Javadoc,这个方法的作用是:根据传进来的 Token,返回用户的验证信息。下面说明一下 Token 和 用户验证信息 。
Token:就是要拿来进行验证的信息,例如:如果是 UsernamePasswordToken 的话,这个 Token 的内容就是“用户提交的用户名和密码”。
来看下 UsernamePasswordToken 的属性。
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken { private String username; private char[] password; private boolean rememberMe; private String host; ...
用户验证信息:就是用户验证通过后,返回给系统的信息。例如:用户登录验证的话,一般来说,返回给系统的“用户验证信息”就应该是这个用户的“用户名和密码”。但也可以返回其它信息,例如返回用户的“邮箱地址和登录密码”信息,做为“用户验证信息”。 那么返回给谁呢,Shiro 中的三大组件之一的 Subject。
不细谈,这么说吧,Subject:即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是 Shiro 的“用户”概念。
上面说了“根据传进来的Token”和“返回用户的验证信息”,但没有说验证的过程,这个过程也是在这个方法中进行。我们看一下源码:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
// doGetAuthenticationInfo方法的内容,由各个子类来实现。
// 主要是用来取得我们保存的“用户验证信息”,例如DB里保存的密码(具体看JdbcRealm的方法实现)
if (info == null) {
info = doGetAuthenticationInfo(token);
...
}
// 在这里,把用户提交的信息(Token)和我们保存的“用户验证信息”进行比较
// 如果不通过,直接抛出定义好的异常。
if (info != null) {
assertCredentialsMatch(token, info);
} else {
return info;
}
权限获取
“权限验证”的处理,是由接口定义的。但“验证是否有访问权限”的逻辑,则是由类定义的。定义的类为:AuthorizingRealm
,在这个类中有个getAuthorizationInfo
方法。这个方法和getAuthenticationInfo
方法的处理流程有点像:
验证是否有指定的权限
返回用户的权限信息
调用时机
下面看一个实际登录的 Controller 的例子:
@Controller
public class LoginController {
//登录跳转
@RequestMapping(value = "/login", method = {RequestMethod.GET})
public String loginUI() throws Exception {
return "../../login";
}
//登录跳转
@RequestMapping(value = "/sxqy", method = {RequestMethod.GET})
public String loginUI2() throws Exception {
return "../../login";
}
//重点!!!!!!
//登录表单处理
@RequestMapping(value = "/login", method = {RequestMethod.POST})
public String login(ViewEmployeeMiPsd viewEmployeeMiPsd) throws Exception {
//Shiro实现登录
UsernamePasswordToken token = new UsernamePasswordToken(viewEmployeeMiPsd.getCode(),
viewEmployeeMiPsd.getPsd());
Subject subject = SecurityUtils.getSubject();
//如果获取不到用户名就是登录失败,但登录失败的话,会直接抛出异常
try{
//重点!!!!!!
//getAuthenticationInfo 执行时机
subject.login(token);
}catch (Exception e){
e.printStackTrace();
}
//重点!!!!!!
//getAuthorizationInfo 执行时机 -- subject.hasRole()
if (subject.hasRole("admin")) {
return "redirect:/admin/showComputerProblems";
} else if (!subject.hasRole("admin")) {
return "redirect:/normal/showComputerProblems";
}
return "/login";
}
}
不过,getAuthorizationInfo
的执行调用方式包括上面的总共有三个:
- subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
- @RequiresRoles(“admin”) :在方法上加注解的时候;
- [@shiro.hasPermission name = “admin”][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。
实现
需要注意的是,在 Shiro 实际使用中,我们是肯定会自定义一个 Realm 类的。
从上面的功能说明可以看出来,在权限控制中比较重要的验证(登录或权限)逻辑,都是在Realm中做的。Realm的类继承如下:
不同的继承,需要实现不同的方法。继承了 AuthorizingRealm 的类,都要实现上面说的 getAuthenticationInfo
和 getAuthorizationInfo
方法,来完成身份验证和权限获取。但如果自定义的 Realm 类只实现 Realm 接口的话,只需要 getAuthenticationInfo
方法就可以。下面看一个只实现 Realm 接口的自定义 Realm:
public class MyRealm1 implements Realm {
@Override
public String getName() {
return "myrealm1";
}
@Override
public boolean supports(AuthenticationToken token) {
//仅支持UsernamePasswordToken类型的Token
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal(); //得到用户名
String password = new String((char[])token.getCredentials()); //得到密码
if(!"zhang".equals(username)) {
throw new UnknownAccountException(); //如果用户名错误
}
if(!"123".equals(password)) {
throw new IncorrectCredentialsException(); //如果密码错误
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}
但是在使用中基本上都会对账户进行权限管理,下面看一个继承 AuthorizingRealm 的自定义 Realm:
@Component
public class LoginRealm extends AuthorizingRealm{
@SuppressWarnings("SpringJavaAutowiringInspection")//忽略警告,下同
@Resource(name = "roleServiceImpl")
private RoleService roleService;
@SuppressWarnings("SpringJavaAutowiringInspection")//忽略警告,下同
@Resource(name = "viewEmployeeMiPsdServiceImpl")
private ViewEmployeeMiPsdService viewEmployeeMiPsdService;
/**
* 获取身份信息,我们可以在这个方法中,从数据库获取该用户的权限和角色信息
* 当调用权限验证时,就会调用此方法
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String code = (String) getAvailablePrincipal(principalCollection);
Role role = null;
ViewEmployeeMiPsd viewEmployeeMiPsd = null;
viewEmployeeMiPsd = viewEmployeeMiPsdService.findByCode(code);
//通过用户名从数据库获取角色权限集
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> r = new HashSet<>();
if (role != null) {
String[] roles = role.getRolename().split("\+");
for(int i = 0;i < roles.length; i++){
r.add(roles[i].toString());
}
//放入该用户权限信息
info.setRoles(r);
}
return info;
}
/**
* 在这个方法中,进行身份验证
* login时调用
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//工号
String code = (String) token.getPrincipal();
//密码
String password = new String((char[])token.getCredentials());
ViewEmployeeMiPsd viewEmployeeMiPsd = null;
viewEmployeeMiPsd = viewEmployeeMiPsdService.findByCode(code);
if (viewEmployeeMiPsd == null) {
//没有该用户
throw new UnknownAccountException();
} else if (!password.equals(viewEmployeeMiPsd.getPsd())) {
//密码错误
throw new IncorrectCredentialsException();
}
//身份验证通过,返回一个身份信息
AuthenticationInfo aInfo = new SimpleAuthenticationInfo(code,password,getName());
return aInfo;
}
}