自定义的 Realm 使得我们使用安全数据源更加灵活,在自定义的 Realm 中,我们就可以自己控制认证和授权的逻辑了。同时我们还简单介绍了一下在多个 Realm 同时存在的情况下,我们可以配置我们的认证策略来满足我们的需求。
前面两节我们已经介绍过 IniRealm 和 JdbcRealm,这一节我们介绍自定义的 Realm 实现我们自己的安全数据源。
方式一:implements Realm (这种方式不太常用,只是为了说明知识)
这种方式实现的 Realm 仅只能实现认证操作,并不能实现授权操作。
代码:
public class MapRealm implements Realm {
private static Map<String,String> users;
static{
users = new HashMap<>();
users.put("liwei","123456");
users.put("zhouguang","666666");
}
/**
* 返回一个唯一的 Realm 名字
* @return
*/
public String getName() {
System.out.println("Map Realm 中设置 Realm 名字的方法");
return "MyStaticRealm";
}
/**
* 判断此 Realm 是否支持此 Token
* @param authenticationToken
* @return
*/
public boolean supports(AuthenticationToken authenticationToken) {
System.out.println("Map Realm 中给出支持的 Token 的方法");
// 表示仅支持 UsernamePasswordToken 类型的 Token
return authenticationToken instanceof UsernamePasswordToken;
}
/**
* 根据 Token 获取认证信息
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("Map Realm 中返回认证信息的方法");
String userName = (String)authenticationToken.getPrincipal();
String password = new String((char[])authenticationToken.getCredentials());
System.out.println("token 中的用户名:" + userName);
System.out.println("token 中的密码:" + password);
if(!users.containsKey(userName)){
throw new UnknownAccountException("没有这个用户!");
}else if(!password.equals(users.get(userName))){
throw new IncorrectCredentialsException("密码错误!");
}
return new SimpleAuthenticationInfo(userName,password,getName());
}
}
接下来我们要在 shiro.ini 文件中声明我们要是用的这个 Realm。
[main]
#声明了我们自己定义的一个 Realm
myMapRealm=com.liwei.realm.MapRealm
#将我们自己定义的 Realm 注入到 securityManager 的 realms 属性中去
securityManager.realms=$myMapRealm
方式二:extends AuthorizingRealm(比较常用的一种方式,因为这样做既可以实现认证操作,也可以实现授权操作)
示例代码:
package com.shiro.test;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
*
* @ClassName: MyRealm
* @Description: 自定义的realm
* @author cheng
* @date 2017-4-5 上午11:09:31
*/
public class MyRealm extends AuthorizingRealm {
/**
* 用于认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// token是用户输入的
// 1.从token中取出用户信息
String userCode = (String) token.getPrincipal();
// 2.依据用户信息从数据库中查询
// 如果没有查询到,返回null,抛出UnknownAccountException
// if(userCode != null){
// return null;
// }
// ......
// 模拟从数据库中查询得到密码
String password = "111111";
// 如果查询到了,返回AuthenticationInfo
// 如果密码不匹配,抛出IncorrectCredentialsException
// 如果查询到返回认证信息AuthenticationInfo
//activeUser就是用户身份信息(ActiveUser为自定义的pojo)
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid("zhangsan");
activeUser.setUsercode("zhangsan");
activeUser.setUsername("张三");
//..
//根据用户id取出菜单
//通过service取出菜单
List<SysPermission> menus = null;
try {
menus = sysService.findMenuListByUserId("zhangsan");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//将用户菜单 设置到activeUser
activeUser.setMenus(menus);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
activeUser, password, this.getName());
return simpleAuthenticationInfo;
}
/**
* 用于授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principal) {
// 从 principal获取主身份信息
// 将getPrimaryPrincipal方法返回值转为真实身份类型
// 在上边的doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中身份类型
String userCode = (String) principal.getPrimaryPrincipal();
// 根据身份信息获取权限信息
// 连接数据库...
// 模拟从数据库获取到数据
List<String> permissions = new ArrayList<String>();
permissions.add("user:create");// 用户的创建
permissions.add("items:add");// 商品添加权限
// ....
// 查到权限数据,返回授权信息(要包括 上边的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 将上边查询到授权信息填充到simpleAuthorizationInfo对象中
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
}
说明:上面的写作也只是为了测试,真正在生产环境中,应该通过查询数据库去完成认证和授权的相关操作。
获取用户信息
//从shiro的session中取activeUser
Subject subject = SecurityUtils.getSubject();
//取身份信息
ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
//通过model传到页面
model.addAttribute("activeUser", activeUser);
下面这一行说明的事实是很重要的:
在 doGetAuthenticationInfo() 方法中须要返回一个正确的 SimpleAuthenticationInfo 对象,这样 Shiro 就会和 Subject 的 login() 方法中传入的 token 信息进行比对,完成认证的操作。
然后我们在 shiro.ini 中也要配置这个自定义的 Realm:
代码:
[main]
#自定义的realm
myRealm=com.shiro.test.MyRealm
#注入自定义的realm
securityManager.realms=$myRealm
知识点:配置认证策略
这时候,我们会有一个疑问,securityManager 的属性既然是 realms,说明可以设置若干个 Realm,它们认证的顺序是如何的呢。
Shiro 会按照我们声明的顺序,依次验证。在使用了 ini 文件启动 Shiro 的方式中,IniRealm 在 Shiro 中是默认使用的(我个人觉得应该是第一个使用的,即使在我们不声明的情况下)。
那么对于若干个 Realm,Shiro 提供了一种配置方式,让我们来决定在多个 Reaml 同时声明的情况下,采用哪些 Realm 返回的认证信息的方式,这就是我们的认证策略。
认证策略主要有以下三种:
1、FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
2、AtLeastOneSuccessfulStrategy: (这是默认使用的认证策略,即在不配置情况下 Shiro 所采用的认证策略)只要有一个 Realm 验证成功即可, 和 FirstSuccessfulStrategy 不同,返回所有 Realm 身份验证成功的认证信息;
3、AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。
配置示例:
配置认证策略
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrateg
配置好以后,我们可以通过以下的方法来验证我们刚刚配置的 Shiro 的认证策略。
currentSubject.login(token);
PrincipalCollection ps = currentSubject.getPrincipals();
System.out.println(ps.asList());
System.out.println(ps.getRealmNames());
System.out.println(currentSubject.getPrincipals());
自定义 Realm 就介绍到这里了。自定义 Realm 是很重要的,特别是 extends AuthorizingRealm 这种方式。
学习到这里想再强调一下 extends AuthorizingRealm 这种方式覆写的 doGetAuthenticationInfo() 方法,一开始我不是很明白,这个方法到底是做什么的,就上面我们举例,我想再解释一下。
一般地,我们从参数 AuthenticationToken 对象中取出用户填写的用户名和密码信息,这个 token 其实就是 Subject 使用 login() 方法中传入的 UsernamePasswordToken 对象,我们通过这个 UsernamePasswordToken 对象获得用户填写的用户名和密码,然后我们应该通过用户的用户名去数据库查询数据库是否有这个用户名,如果没有,抛出一个用户名不存在异常;如果用户名存在,返回一个用户对象(带密码的),再用数据库返回的密码数据和用户填写的密码数据进行比对,如果错误,就抛出异常,如果正确,就要把正确的用户名和密码信息封装成一个 SimpleAuthenticationInfo 对象返回,这才是一个比较完整并且正确的流程。
另外再补充一下,我在上面的例子中说到“用户填写的用户名和密码”是一种为了方便理解的说法。下面介绍官方正确的说法。
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals, 但只有一个 Primary principals, 一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的 principals 和 credentials 组合就是用户名/密码了。
1、在applicationContext-shiro.xml中配置filter规则
/items/queryItems.action = perms[item:query]
2、用户在认证通过后,请求/items/queryItems.action
3、被PermissionsAuthorizationFilter拦截,发现需要“item:query”权限
4、PermissionsAuthorizationFilter调用realm中的doGetAuthorizationInfo获取数据库中正确的权限
5、PermissionsAuthorizationFilter对item:query 和从realm中获取权限进行对比,如果“item:query”在realm返回的权限列表中,授权通过。