一、会话相关API及会话使用
Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Web容器Tomcat),可以在JavaSE和JavaEE环境中使用。会话相关API主要包括:
Subject.getSession()
: 获取当前用户的会话,如果当前没有创建会话对象,则会创建一个新的会话。这等价于Subject.getSession(true)
。Subject.getSession(boolean create)
: 根据参数决定是否创建一个新的会话。如果create
为true
且当前没有会话,则创建一个新的会话;如果为false
且当前没有会话,则返回null
。session.setAttribute(key, value)
: 设置会话属性。session.getAttribute(key)
: 获取会话属性。session.removeAttribute(key)
: 删除会话属性。
会话使用时,建议在Controller层使用原生的HttpSession
对象,在Service层使用Shiro提供的Session
对象。
二、缓存
问题分析
在每次访问设置了权限的页面时,Shiro都会执行doGetAuthorizationInfo()
方法来获取权限信息。这可能导致性能问题,因为每次请求都需要重新计算权限。
解决办法
对权限授权数据进行缓存处理。可以使用第三方的Shiro-Redis集成Redis来实现缓存。
具体实现
- 在Shiro配置类中配置Redis缓存管理器。
- 在自定义Realm中使用缓存管理器来缓存权限信息。
- 在用户登录或权限变更时,手动清除缓存中的旧权限信息。
示例代码(Spring Boot环境):
// Shiro配置类
@Configuration
public class ShiroConfig {
// ...
@Bean
public RedisCacheManager cacheManager(RedisManager redisManager) {
RedisCacheManager cacheManager = new RedisCacheManager();
cacheManager.setRedisManager(redisManager);
return cacheManager;
}
// ...
}
// 自定义Realm
public class CustomRealm extends AuthorizingRealm {
// ...
@Autowired
private RedisCacheManager cacheManager;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 从缓存中获取权限信息
String cacheKey = getAuthorizationCacheKey(principals);
AuthorizationInfo cachedAuthInfo = (AuthorizationInfo) cacheManager.getCache("authorizationCache").get(cacheKey);
if (cachedAuthInfo != null) {
return cachedAuthInfo;
}
// 如果缓存中没有,则查询数据库并缓存结果
AuthorizationInfo authInfo = // 查询数据库获取权限信息
cacheManager.getCache("authorizationCache").put(cacheKey, authInfo);
return authInfo;
}
// ...
private String getAuthorizationCacheKey(PrincipalCollection principals) {
// 生成缓存键,例如使用用户名
return principals.getPrimaryPrincipal().toString();
}
// 清除缓存的方法
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
String cacheKey = getAuthorizationCacheKey(principals);
cacheManager.getCache("authorizationCache").remove(cacheKey);
}
}
演示测试
在测试环境中,可以通过模拟用户登录和访问受保护资源来验证缓存是否生效。观察日志或调试信息,确认Shiro是否从缓存中获取了权限信息而不是每次都查询数据库。
三、加密
哈希与盐
为了增强密码的安全性,Shiro支持使用哈希算法对密码进行加密,并可以添加盐值以防止彩虹表攻击。
加密与验证
在Shiro中,可以通过配置HashedCredentialsMatcher
来实现密码的哈希加密和验证。HashedCredentialsMatcher
可以设置哈希算法(如MD5、SHA-256等)和哈希迭代次数。
具体实现
- 在Shiro配置类中配置
HashedCredentialsMatcher
。 - 在自定义Realm中使用
HashedCredentialsMatcher
进行密码验证。
示例代码:
// Shiro配置类
@Configuration
public class ShiroConfig {
// ...
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5"); // 设置哈希算法
hashedCredentialsMatcher.setHashIterations(1024); // 设置哈希迭代次数
return hashedCredentialsMatcher;
}
@Bean
public SecurityManager securityManager(CustomRealm customRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm);
securityManager.setCredentialsMatcher(hashedCredentialsMatcher()); // 设置密码加密验证器
return securityManager;
}
// ...
}
// 自定义Realm
public class CustomRealm extends AuthorizingRealm {
// ...
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// 查询数据库获取用户信息
User user = userService.findUserByName(username);
if (user == null) {
throw new UnknownAccountException("用户不存在");
}
// 返回认证信息,包含用户名、密码(已加密)、盐值和Realm名称
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
}
// ...
}
在上面的代码中,User
类应该包含用户名、密码(已加密)、盐值等属性。在注册用户时,应该使用与HashedCredentialsMatcher
相同的哈希算法和迭代次数对密码进行加密,并将加密后的密码和盐值存储在数据库中。
四、登录次数限制
Shiro本身没有直接提供登录次数限制的功能,但可以通过自定义Realm或拦截器来实现。例如,可以在自定义Realm中维护一个登录失败次数的计数器,当登录失败次数超过一定限制时,可以锁定用户账户或增加额外的验证步骤。
实现登录次数限制的示例代码(简化版):
// 自定义Realm
public class CustomRealm extends AuthorizingRealm {
// ...
private Map<String, Integer> loginFailureCounts = new ConcurrentHashMap<>(); // 登录失败次数计数器
private static final int MAX_FAILURE_COUNT = 5; // 最大失败次数限制
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// 检查登录失败次数是否超过限制
if (loginFailureCounts.getOrDefault(username, 0) >= MAX_FAILURE_COUNT) {
throw new LockedAccountException("账户已被锁定,请稍后再试");
}
// 查询数据库获取用户信息
User user = userService.findUserByName(username);
if (user == null) {
// 登录失败,增加失败次数计数器
loginFailureCounts.put(username, loginFailureCounts.getOrDefault(username, 0) + 1);
throw new UnknownAccountException("用户不存在");
}
// 验证密码(省略具体实现)
// ...
// 登录成功,清除失败次数计数器
loginFailureCounts.remove(username);
// 返回认证信息
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
}
// ...
}