1. 什么是 Apache Shiro?它的核心组件有哪些?
答:Apache Shiro 是一个强大且灵活的 Java 安全框架,用于处理认证、授权、会话管理和加密。
核心组件:
- Subject:当前操作的用户或系统进程。
- SecurityManager:Shiro 的核心,用于协调内部组件。
- Realm:用于连接应用程序的数据源(如数据库、LDAP),执行认证和授权逻辑。
2. Shiro 的认证和授权流程是什么?
认证流程:
- 用户提交认证信息(如用户名和密码)。
- SecurityManager 调用 Realm 验证用户身份。
- 验证通过后,创建用户的 Subject。
授权流程:
- SecurityManager 调用 Realm 查询用户角色和权限。
- 根据配置的权限策略判断用户是否有权限访问资源。
3. 什么是 Realm?如何自定义 Realm?
答:Realm 是 Shiro 用于连接数据源(如数据库)的组件,主要负责认证和授权。
自定义 Realm 的步骤:
- 继承
AuthorizingRealm
类。 - 重写
doGetAuthenticationInfo
方法实现认证逻辑。 - 重写
doGetAuthorizationInfo
方法实现授权逻辑。
public class CustomRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
String password = "从数据库中查询密码";
if (password == null) {
throw new UnknownAccountException("用户不存在");
}
return new SimpleAuthenticationInfo(username, password, getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("admin");
info.addStringPermission("user:read");
return info;
}
}
4. 如何在 Shiro 中实现 Remember Me 功能?
答:Remember Me 功能允许用户在关闭浏览器后仍保持登录状态。
实现步骤:
- 配置
CookieRememberMeManager
。 - 在登录时设置
RememberMe
属性:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
subject.login(token);
5. Shiro 中如何配置加密密码?
答:使用 HashedCredentialsMatcher
进行密码加密,数据库中的密码需使用相同的算法加密存储:
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("SHA-256");
matcher.setHashIterations(1024);
customRealm.setCredentialsMatcher(matcher);
6. 如何使用注解实现权限控制?
开启注解支持:
@Configuration
public class ShiroConfig {
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
使用注解进行权限控制:
@RequiresRoles("admin")
@RequiresPermissions("user:read")
@RequiresRoles("admin")
@RequiresPermissions("user:read")
public void getUserDetails() {
// 仅 admin 角色且拥有 user:read 权限的用户可调用
}
7. Shiro 如何实现会话管理?
答:Shiro 提供了默认的会话管理机制,支持分布式会话管理。
配置会话管理:
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(1800000); // 30 分钟
自定义会话 DAO:
public class CustomSessionDAO extends EnterpriseCacheSessionDAO {
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
// 自定义存储逻辑
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
// 自定义读取逻辑
return super.doReadSession(sessionId);
}
}
8. Shiro 中的过滤器有哪些?
答:Shiro 提供了多个内置过滤器:
- anon:匿名访问。
- authc:需要认证。
- user:登录过即可访问。
- perms:需要指定权限。
- roles:需要指定角色。
配置示例:
shiroFilterFactoryBean.setFilterChainDefinitionMap(Map.of(
"/login", "anon",
"/admin/**", "roles[admin]",
"/user/**", "perms[user:read]",
"/**", "authc"
));
9. Shiro 的线程绑定是如何实现的?
答:Shiro 使用 ThreadContext
绑定 Subject,使得在同一线程内可以随时获取当前用户。
Subject currentUser = SecurityUtils.getSubject();
ThreadContext.bind(currentUser);
多线程场景下需手动绑定 Subject:
Subject subject = SecurityUtils.getSubject();
Runnable task = subject.associateWith(() -> {
// 新线程内的操作
});
new Thread(task).start();
10. 如何在 Shiro 中实现分布式会话?
答:分布式会话需要将会话存储到共享介质(如 Redis)中。自定义会话 DAO,使用 Redis 存储会话,配置 DefaultWebSessionManager
使用自定义 DAO:
public class RedisSessionDAO extends AbstractSessionDAO {
@Override
protected Serializable doCreate(Session session) {
// Redis 存储逻辑
}
@Override
protected Session doReadSession(Serializable sessionId) {
// Redis 读取逻辑
}
}
11. Shiro 如何动态加载权限?
答:动态加载权限指从数据库动态查询用户权限。
实现方式:
- 在
doGetAuthorizationInfo
方法中,从数据库查询权限:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roleService.getRolesByUsername(username));
info.addStringPermissions(permissionService.getPermissionsByUsername(username));
return info;
}
- 配置缓存,减少频繁查询。
12. Shiro 和 Spring Security 的区别是什么?
特性 | Shiro | Spring Security |
---|---|---|
学习曲线 | 简单,轻量级 | 较复杂,功能全面 |
功能 | 专注于认证、授权、加密等核心功能 | 支持全面的安全功能(如 OAuth2、SSO 等) |
扩展性 | 高,开发者需实现自定义组件 | 功能齐全,但扩展性稍差 |
集成性 | 无内置与 Spring 深度集成的功能 | 与 Spring 全面集成 |
13. Shiro 如何实现分布式会话共享?
答:分布式会话共享需要将会话信息存储在共享存储介质中,例如 Redis、数据库等。
实现步骤:
- 自定义会话 DAO,使用 Redis 或其他分布式存储介质存储会话信息。
- 配置 Shiro 使用自定义的 SessionDAO 和 DefaultWebSessionManager。
- 确保多个节点访问同一个存储介质,从而实现会话共享。
14. Shiro 中如何实现动态权限?
答:动态权限指的是从数据库动态加载或更新用户权限,而非硬编码在配置文件中。
实现方式:
- 在 doGetAuthorizationInfo 方法中,从数据库动态查询用户的角色和权限信息。
- 配置缓存(如 Redis、Ehcache),以减少频繁查询数据库的开销。
- 实现权限动态更新:
监听权限表或角色表的变化。
使用 Shiro 的缓存 API(clearCachedAuthorizationInfo)清除缓存。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 从数据库加载角色和权限
List<String> roles = roleService.getRolesByUsername(username);
List<String> permissions = permissionService.getPermissionsByUsername(username);
info.addRoles(roles);
info.addStringPermissions(permissions);
return info;
}
15. Shiro 如何处理多线程中的 Subject?
答:Shiro 的 Subject 是线程绑定的,但在多线程场景下需要显式地将 Subject 绑定到新线程。
解决方式:使用 Subject.associateWith
Subject currentSubject = SecurityUtils.getSubject();
Runnable task = currentSubject.associateWith(() -> {
// 在新线程中使用 Subject
System.out.println("当前用户:" + currentSubject.getPrincipal());
});
new Thread(task).start();
16. 如何在 Shiro 中集成 OAuth 认证?
答:Shiro 本身不直接支持 OAuth,可以通过自定义 Realm 或结合第三方库(如 spring-security-oauth)实现。
实现方式:
- 自定义 OAuthRealm,实现 OAuth 认证逻辑,例如使用 OAuth 提供商(如 Google、GitHub)的 Access Token 验证用户身份。
- 在 doGetAuthenticationInfo 方法中校验 Access Token 并加载用户信息。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
OAuthToken oAuthToken = (OAuthToken) token;
String accessToken = oAuthToken.getToken();
// 调用 OAuth 提供商的 API 验证 Token
UserInfo userInfo = oAuthService.getUserInfo(accessToken);
if (userInfo == null) {
throw new AuthenticationException(\"Invalid OAuth Token\");
}
return new SimpleAuthenticationInfo(userInfo.getUsername(), accessToken, getName());
}
17. Shiro 如何实现无状态的 REST API 权限校验?
答:在无状态 REST API 场景下,通常不会使用会话管理,而是通过 Token 进行权限校验。
实现步骤:
- 自定义过滤器,提取请求中的 Token:
public class StatelessAuthFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String token = getToken(request);
if (token != null && verifyToken(token)) {
return true;
}
return false;
}
private String getToken(ServletRequest request) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
return httpRequest.getHeader("Authorization");
}
private boolean verifyToken(String token) {
// 验证 Token 的逻辑
return true;
}
}
- 配置过滤器到 Shiro 的 FilterChain。
18. Shiro 的并发会话限制如何实现?
答:可以通过会话管理器和自定义监听器实现。
实现步骤:
- 自定义会话监听器,跟踪用户会话:
public class SessionLimitListener implements SessionListener {
private final Map<String, Session> activeSessions = new ConcurrentHashMap<>();
@Override
public void onStart(Session session) {
String username = (String) session.getAttribute("username");
if (activeSessions.containsKey(username)) {
activeSessions.get(username).stop();
}
activeSessions.put(username, session);
}
@Override
public void onStop(Session session) {
activeSessions.remove(session.getAttribute("username"));
}
@Override
public void onExpiration(Session session) {
onStop(session);
}
}
- 配置自定义监听器到
DefaultWebSessionManager
。
19. 如何解决 Shiro 在分布式系统中的会话共享问题?
答:通过 Redis 或其他共享存储实现会话共享。
实现步骤:
- 使用 Redis 实现自定义会话 DAO:
public class RedisSessionDAO extends AbstractSessionDAO {
private RedisTemplate<String, Object> redisTemplate;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(sessionId.toString(), session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return (Session) redisTemplate.opsForValue().get(sessionId.toString());
}
@Override
public void update(Session session) {
redisTemplate.opsForValue().set(session.getId().toString(), session);
}
@Override
public void delete(Session session) {
redisTemplate.delete(session.getId().toString());
}
}
- 配置
DefaultWebSessionManager
使用 RedisSessionDAO。
20. 如何动态修改用户权限?
答:动态修改权限可以通过缓存实现。
实现方式:
- 在 Realm 的授权方法中启用缓存:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roleService.getRoles(username));
info.addStringPermissions(permissionService.getPermissions(username));
return info;
}
- 动态修改权限时清除缓存:
public void clearAuthorizationCache(String username) {
Subject subject = SecurityUtils.getSubject();
if (subject.getPrincipal().equals(username)) {
((AuthorizingRealm) securityManager.getRealm()).clearCachedAuthorizationInfo(subject.getPrincipals());
}
}
21. 如何防止 Shiro 的权限配置中 URL 被绕过?
答:配置默认的过滤器链规则,确保所有未匹配的 URL 都需要认证。
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/api/**", "authc");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
22. 如何扩展 Shiro 的权限模型?
答:可以通过自定义 Permission
接口实现复杂的权限控制。
示例代码:
- 自定义权限类:
public class CustomPermission implements Permission {
private final String resource;
private final String action;
public CustomPermission(String permissionString) {
String[] parts = permissionString.split(":");
this.resource = parts[0];
this.action = parts[1];
}
@Override
public boolean implies(Permission p) {
if (!(p instanceof CustomPermission)) {
return false;
}
CustomPermission cp = (CustomPermission) p;
return this.resource.equals(cp.resource) && this.action.equals(cp.action);
}
}
- 在 Realm 中使用自定义权限:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addObjectPermission(new CustomPermission("file:read"));
return info;
}
23. 如何使用 Shiro 实现多租户架构?
答:多租户架构需要在认证和授权时区分不同的租户。
实现方式:
- 扩展 Token,添加租户信息:
public class MultiTenantToken extends UsernamePasswordToken {
private String tenantId;
public MultiTenantToken(String username, char[] password, String tenantId) {
super(username, password);
this.tenantId = tenantId;
}
public String getTenantId() {
return tenantId;
}
}
- 在 Realm 中处理租户信息:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
MultiTenantToken tenantToken = (MultiTenantToken) token;
String tenantId = tenantToken.getTenantId();
String username = tenantToken.getUsername();
// 根据租户 ID 和用户名查询用户信息
User user = userService.getUserByTenantAndUsername(tenantId, username);
if (user == null) {
throw new UnknownAccountException("用户不存在");
}
return new SimpleAuthenticationInfo(username, user.getPassword(), getName());
}
24. Shiro 中的权限缓存如何优化?
答:通过引入分布式缓存(如 Redis)优化权限缓存。
实现方式:
- 使用 Redis 作为缓存实现:
@Bean
public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
return new RedisCacheManager(redisTemplate);
}
- 配置到 SecurityManager:
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setCacheManager(cacheManager);
25. Shiro常见的权限控制方式有哪几种?
- 方法注解权限控制
- 页面标签权限控制
- 代码级别权限控制
- URL级别权限控制
26. Shiro的优点有哪些?
- 简单的身份认证,支持多种数据源
- 非常简单的加密API
- 对角色的简单的授权,支持细粒度的授权(方法级)
- 支持一级缓存,以提升应用程序的性能
- 内置的基于POJO企业会话管理,适用于Web以及非Web的环境
- 不跟任何的框架或者容器捆绑,可以独立运行
25. Shiro 如何集成 Spring Boot 并实现自动化配置?
答:通过自定义配置类实现 Shiro 的自动化集成。
@Configuration
public class ShiroConfig {
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
return shiroFilterFactoryBean;
}
@Bean
public Realm customRealm() {
return new CustomRealm();
}
}
评论区