1. 自定义Realm基础
- 步骤:
- 创建一个类 ,继承AuthorizingRealm->AuthenticatingRealm->CachingRealm->Realm
- 重写授权方法 doGetAuthorizationInfo
- 重写认证方法 doGetAuthenticationInfo
- 方法:
- 当用户登陆的时候会调用 doGetAuthenticationInfo
- 进行权限校验的时候会调用: doGetAuthorizationInfo
- 对象介绍
- UsernamePasswordToken : 对应就是 shiro的token中有Principal和Credential
-
UsernamePasswordToken-》HostAuthenticationToken-》AuthenticationToken
-
- SimpleAuthorizationInfo:代表用户角色权限信息
- SimpleAuthenticationInfo :代表该用户的认证信息
- UsernamePasswordToken : 对应就是 shiro的token中有Principal和Credential
2. 自定义realm代码:
package net.xdclass.xdclassshiro; import org.apache.commons.lang3.StringUtils; 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 java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * 自定义realm:继承AuthorizingRealm,重写抽象方法 */ public class CustomRealm extends AuthorizingRealm { private final Map<String, String> userInfoMap = new HashMap<>(); { userInfoMap.put("jack", "123"); userInfoMap.put("xdclass", "456"); } //role->permission 模拟数据库角色与权限的关系 private final Map<String, Set<String>> permissonMap = new HashMap<>(); { Set<String> set1 = new HashSet<>(); Set<String> set2 = new HashSet<>(); set1.add("video:find"); set1.add("video:buy"); set2.add("video:add"); set2.add("video:delete"); permissonMap.put("jack", set1); permissonMap.put("xdclass", set2); } //user->role 模拟数据库角色与权限的关系 private final Map<String, Set<String>> roleMap = new HashMap<>(); { Set<String> set1 = new HashSet<>(); Set<String> set2 = new HashSet<>(); set1.add("role1"); set1.add("role2"); set2.add("root"); roleMap.put("jack", set1); roleMap.put("xdclass", set2); } /** * 校验权限时会调用这个方法,原理就是把角色和权限封装到SimpleAuthorizationInfo的实体中,shiro框架会帮我们进行权限的校验 * 最终org.apache.shiro.realm.AuthorizingRealm#hasRole(org.apache.shiro.subject.PrincipalCollection, java.lang.String) * 方法会调用这个返回的simpleAuthorizationInfo * @param principalCollection * @return simpleAuthorizationInfo */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("权限:doGetAuthorizationInfo"); //获取用户主账户名 String name = (String) principalCollection.getPrimaryPrincipal(); //通过名称查找权限,简化操作(正常是通过名称找角色,通过角色查询对应的权限) Set<String> permissions = getPermissonsByNameFromDB(name); Set<String> roles = getRolesByNameFromDB(name); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } private Set<String> getRolesByNameFromDB(String name) { return roleMap.get(name); } private Set<String> getPermissonsByNameFromDB(String name) { return permissonMap.get(name); } //用户登录的时候会调用该方法 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("认证: doGetAuthenticationInfo"); //从token中获取身份信息,token代表用户输入的信息 String name = (String) authenticationToken.getPrincipal(); //模拟从数据库读密码 String pwd = getPwdByUserNameFromDB(name); if (StringUtils.isBlank(pwd)) { //这里判断是必须要加的,返回null,即为认证不通过!!!详见源码 return null; //匹配失败 } /*useNmaePasswordToken中有用户的Principal和Credential SimpleAuthorizationInfo代表用户的角色权限信息 SimpleAuthenticationInfo 代表该用户的认证信息*/ // this.getName()是获取CachingRealm的名字 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, pwd, this.getName()); return simpleAuthenticationInfo; } private String getPwdByUserNameFromDB(String name) { return userInfoMap.get(name); } }
代码测试:
package net.xdclass.xdclassshiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.Before; import org.junit.Test; /** * 自定义Realm操作 */ public class QuicksStratTest5_4 { private CustomRealm customRealm = new CustomRealm(); private DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); @Before public void inint(){ //构建环境 defaultSecurityManager.setRealm(customRealm); SecurityUtils.setSecurityManager(defaultSecurityManager); } @Test public void testAuthentication() { //获取当前操作的主体 Subject subject = SecurityUtils.getSubject(); //模拟用户输入账号密码 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("jack", "123"); subject.login(usernamePasswordToken); System.out.println("认证 结果:" + subject.isAuthenticated()); System.out.println("getPrincipal()=" + subject.getPrincipal()); System.out.println("调用权限校验:subject.hasRole方法会调用自定义realm重写的doGetAuthorizationInfo方法"); System.out.println("用户jack是否有root角色:" + subject.hasRole("root")); //false // 权限校验 subject.checkRole("role1"); System.out.println("用户jack是否有role1角色:" + subject.hasRole("role1")); //true System.out.println("用户jack是否有video:find权限:" + subject.isPermitted("video:find")); //true } }
上面代码中 ,subject.login方法很关键,是所有认证授权逻辑的入口,跟踪一下代码,可以发现subject.login方法实际调用过程如下:
总结一下,shiro认证的主要流程就是
subject.login(usernamePasswordToken);
DelegatingSubject->login()
DefaultSecurityManager->login()
AuthenticatingSecurityManager->authenticate()
AbstractAuthenticator->authenticate()
ModularRealmAuthenticator->doAuthenticate()
ModularRealmAuthenticator->doSingleRealmAuthentication()
AuthenticatingRealm->getAuthenticationInfo()
需要注意的是:org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate 这个方法中,一般只配置一个realm,因此走的是doSingleRealmAuthentication 这个分支
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken); }
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); } else {
// 这里实际上调用的就是自定义realmz中重写了的getAuthenticationInfo方法,如果返回null,抛出UnknownAccountException,认证不通过 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); } else { return info; } } }
在源码中,获取 AuthenticationInfo 认证信息,如果为空的话,抛出异常,这也是为什么自定义realm中 doGetAuthenticationInfo 方法中判断密码不正确时要返回null的原因;
doSingleRealmAuthentication 方法中的 AuthenticationInfo info = realm.getAuthenticationInfo(token) 调用了org.apache.shiro.realm.AuthenticatingRealm 类的getAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)方法,这个方法里面,调用了一行: info = this.doGetAuthenticationInfo(token),查找其实现类,就是我们自定义realm重写的的doGetAuthenticationInfo方法。
同理,shiro授权的主要流程如下:subject.checkRole方法是授权流程的入口
subject.checkRole("admin")
DelegatingSubject->checkRole()
AuthorizingSecurityManager->checkRole()
ModularRealmAuthorizer->checkRole()
AuthorizingRealm->hasRole()
AuthorizingRealm->doGetAuthorizationInfo()