shiro之 分布式Cache共享之 Realm共享
在我们之前的学习中我们知道了 Shiro 的核心功能之Cache 主要两个应用场景就是 Session 和 Realm。即Session和认证授权信息会被写入 Cache。
在分布式的系统中我们如果使用了 Shiro。 那么我们就不得不考虑一件事:那就是Cache 共享(特指 Session 和 Realm 共享)。这一节我们讲学习一下 Shiro 的分布式应用中如何共享 Realm 数据。
实例
以我们之前学习 Shiro 的框架例子,整合Redis。如果还有同学不知道如何整合Redis,请移步至 redis系列文章(共两种整个方式,喜欢哪个就整合哪个)。
整合 Redis(参阅 整合redis(一) 和 整合redis(二))。
在Realm 中把认证和授权信息写入 Redis 中。
package com.shiro.example.interceptor.realm; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; 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 org.apache.shiro.util.ByteSource; import com.shiro.example.entity.SubjectEntity; import com.shiro.example.redis.RedisClientTemplate; public class ShiroRealm extends AuthorizingRealm { protected Logger logger = Logger.getLogger(this.getClass()); @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("[FirstRealm] doGetAuthenticationInfo"); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(); try { // 1. 把 AuthenticationToken 转换为 UsernamePasswordToken UsernamePasswordToken upToken = (UsernamePasswordToken) token; // 2. 从 UsernamePasswordToken 中来获取 username String username = upToken.getUsername(); // 3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录 SubjectEntity principals = new SubjectEntity("580655386dc0bef1105a44f9dcbe4a1d3a7b0781", "Dustyone"); // 4. 若用户不存在, 则可以抛出 UnknownAccountException 异常 if ("unknown".equals(username)) { throw new UnknownAccountException("用户不存在!"); } // 5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常. if ("monster".equals(username)) { throw new LockedAccountException("用户被锁定"); } // 6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: // SimpleAuthenticationInfo // 以下信息是从数据库中获取的. // 1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. Object principal = principals.getUsername(); // 2). credentials: 密码. Object credentials = principals.getPassword(); // "fc1709d0a95a6be30bc5926fdb7f22f4"; // 3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String realmName = getName(); // 4). 盐值. ByteSource credentialsSalt = ByteSource.Util.bytes(username); /* * SimpleAuthenticationInfo info = new * SimpleAuthenticationInfo(principal, credentials, realmName); */ info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt,realmName); //共享Cache RedisClientTemplate redisCache = RedisClientTemplate.getRedisClient();; redisCache.set("Authentication-"+username, username); } catch (Exception e) { logger.error(e.getMessage()); } return info; } // 授权会被 shiro 回调的方法 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("ShiroRealm Authorization"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); ; try { // 1. 从 PrincipalCollection 中来获取登录用户的信息 Object principal = principals.getPrimaryPrincipal(); RedisClientTemplate redisCache = RedisClientTemplate.getRedisClient();; // 2. 利用登录的用户的信息来用户当前用户的角色或权限(可能需要查询数据库) Set<String> roles = new HashSet<>(); roles.add("admin"); redisCache.set("Roles-"+principal.toString(), "admin"); List<String> permissions=new ArrayList<>(); permissions.add("admin:view:*"); redisCache.set("Permissions-"+principal.toString(), "admin:view:*"); /* if ("admin".equals(principal)) { roles.add("admin"); }*/ // 3. 创建 SimpleAuthorizationInfo, 并设置其 roles 属性. //添加角色 info.addRoles(roles); //添加权限 info.addStringPermissions(permissions); // 4. 返回 SimpleAuthorizationInfo 对象. return info; } catch (Exception e) { logger.error(e.getMessage()); } return info; } }
运行结果
小结
所谓的分布式数据共享实现方式一般为:把各个地方的数据放在同一个DB或者缓存中,然后我们在这个DB或者缓存中统一调度即可。
分布式Shiro 共享 之间共享Realm 数据的设计思路:将在各个分布式的系统中做过Realm 认证和授权的用户的信息统一保存在同一个 Cache中(本例子我们使用Redis 作为缓存来保存信息)。
在共享Realm数据的实例中 各个分布式系统配置的Redis 应为同一个IP和port的Redis 服务。此外我们还可以设置各个缓存数据的Timeout 等操作。
优化建议。在做分布式 Shiro 共享Realm 数据时,把认证和授权的共享数据放在同一个 HashMap中,Key值建议为 Subject 的 principal。这样便于对共享数据的管理(如权限更新、统一删除等)。