-
Shiro三大核心元素
- Subject:主体(即"the current ‘user’");在Shiro中通过SecurityUtils类来获取;
- SecurityManager:安全管理器;执行所有与安全相关的操作,需从Realm中获取用户的角色或权限进行验证;
- Realm:域;管理用户、角色、权限等安全数据,供SecurityManager使用。
- 注意: 需要手动完成Realm的实现;继承AuthorizingRealm抽象类并实现doGetAuthorizationInfo()和doGetAuthenticationInfo()方法。
-
Shiro功能及特性
- 身份验证(Authentication)
- 授权(Authorization)
- 会话管理(Session Management)
- 密码学(Cryptography)
- Web支持(Web Support)
- 缓存(Caching)
- 并发性(Concurrency)
- 测试(Testing)
- 运行方式(Run As)
- 记住我(Remember Me)
-
Spring Boot Shiro用户认证
-
集成步骤:
- 自定义ShiroConfig类,配置SecurityManager Bean;
- 在ShiroConfig中配置过滤工厂类(ShiroFilterFactoryBean,依赖于SecurityManager);
- 自定义Reaml实现,实现doGetAuthenticationInfo()和doGetAuthorizationInfo()方法。
- 引入shiro-spring依赖
- 自定义Shiro配置类
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置登录URL
shiroFilterFactoryBean.setLoginUrl("/login");
// 配置登录成功跳转URL
shiroFilterFactoryBean.setSuccessUrl("/index");
// 配置未授权URL
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 定义filterChain,静态资源不拦截
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
// Druid数据源监控页面不拦截
filterChainDefinitionMap.put("/druid/**", "anon");
// 配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/", "anon");
// 除上以外所有URL都必须认证通过后才可以访问,未通过认证自动访问LoginUrl
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
// 配置SecurityManager,并注入ShiroRealm
DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}
@Bean
public ShiroRealm shiroRealm() {
// 配置ShiroRealm,需要自己实现
return new ShiroRealm();
}
}
- 注意: filterChain基于短路机制,即最先匹配规则;anon、authc等为Shiro实现的过滤器。
- 实现Realm(继承AuthorizingRealm类)
public class ShiroRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户输入的用户名和密码
String username = (String) authenticationToken.getPrincipal();
String password = new String((char[]) authenticationToken.getCredentials());
// 通过用户名到数据库查询用户信息
UserPO user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
if (!user.getStatus()) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
return new SimpleAuthenticationInfo(user, password, getName());
}
}
- 注意: Shiro具有丰富的运行时AuthenticationException层次结构,可以根据捕获的异常准确指出尝试失败的原因。
- 数据层:与用户、角色、权限相关的表、Mapper映射文件、Dao层接口、Service层接口及实现类
- 登录页面
- 登录接口
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
@PostMapping("/login")
@ResponseBody
public BaseResponse login(String username, String password){
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 获取Subject对象
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return BaseResponse.ok();
} catch (UnknownAccountException | IncorrectCredentialsException | LockedAccountException e) {
return BaseResponse.error(e.getMessage());
} catch (AuthenticationException e) {
return BaseResponse.error("认证失败!");
}
}
@RequestMapping("/")
public String redirectIndex() {
return "redirect:/index";
}
@RequestMapping("/index")
public String index(Model model) {
UserPO user = (UserPO) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user", user);
return "index";
}
}
- 注意: 根据需要在application.yml中加入静态页面的访问路径配置
spring:
thymeleaf:
prefix: classpath:/templates/febs/views/
-
Spring Boot Shiro Remember Me
- 记住我:用户的登录状态不会因为浏览器的关闭而失效,直到Cookie过期。
- 修改Shiro配置类,加入Cookie对象及Cookie管理对象
/**
* Cookie对象:用于实现RememberMe功能
* @return
*/
private SimpleCookie rememberMeCookie() {
// 设置Cookie名称,对应login.html页面的<input type="checkbox" name="rememberMe"/>
SimpleCookie cookie = new SimpleCookie("rememberMe");
// 设置Cookie的过期时间,单位为秒,此处设置30分钟
cookie.setMaxAge(1800);
return cookie;
}
/**
* Cookie管理对象:用于实现RememberMe功能
* @return
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// 设置rememberMeCookie加密的密钥
// cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
- 将Cookie管理对象设置到SecurityManager中
@Bean
public SecurityManager securityManager() {
// 配置SecurityManager,并注入ShiroRealm
DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
- 修改权限配置,将(/**)路径的访问权限由"authc"改为"user";user指的是用户认证通过或者配置了Remember Me记住用户登录状态后可访问
filterChainDefinitionMap.put("/**", "user");
- 修改登录页面:加入Remember Me复选框
- 修改登录接口
@PostMapping("/login")
@ResponseBody
public BaseResponse login(String username, String password, Boolean rememberMe){
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
// 获取Subject对象
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return BaseResponse.ok();
} catch (UnknownAccountException | IncorrectCredentialsException | LockedAccountException e) {
return BaseResponse.error(e.getMessage());
} catch (AuthenticationException e) {
return BaseResponse.error("认证失败!");
}
}
-
Spring Boot Shiro权限控制
- Shiro权限控制的三大核心元素:用户、角色、权限。
- 数据库设计模型:RBAC(Role-Based Access Control,基于角色的访问控制)模型,构成“用户-角色-权限”的授权模型;其中,用户与角色之间,角色与权限之间,是多对多的关系。
- 根据数据库设计模型创建表、Mapper映射文件、Dao层接口、Service层接口及实现类。
- 实现Realm的doGetAuthorizationInfo()方法
/**
* 获取用户角色和权限
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
UserPO user = (UserPO) SecurityUtils.getSubject().getPrincipal();
Long userId = user.getId();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 获取用户角色集
List<RolePO> roleList = userRoleService.findByUserId(userId);
Set<String> roleSet = new HashSet<>();
Set<String> permissionSet = new HashSet<>();
for (RolePO role : roleList) {
roleSet.add(role.getRoleName());
// 获取用户权限集
List<PermissionPO> permissionList = rolePermissionService.findByRoleId(role.getId());
for (PermissionPO permission : permissionList) {
permissionSet.add(permission.getPermissionUrl());
}
}
simpleAuthorizationInfo.setRoles(roleSet);
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/**
* 配置开启权限相关的注解
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
- Shiro提供的和权限相关的注解:
- @RequiresAuthentication:需要当前Subject已通过login进行了登录认证
- @RequiresUser:需要当前Subject已身份认证或已通过记住我进行了登录
- @RequiresGuest:需要当前Subject未身份认证或未通过记住我登录过(游客身份)
- @RequiresRoles(value={“admin”, “user”}, logical = Logical.AND):需要当前Subject拥有角色"admin"和"user"
- @RequiresPermissions (value={“user:a”, “user:b”}, logical = Logical.OR):需要当前Subject拥有权限"user:a"或"user:b"
- 在需要进行权限控制的接口方法上,添加Shiro权限注解控制接口的访问权限
@Controller
@RequestMapping("/user")
public class UserController {
@RequiresPermissions(value = {"user:user"})
@GetMapping("/list")
@ResponseBody
public String userList() {
return "userList";
}
@RequiresPermissions(value = {"user:add"})
@GetMapping("/add")
@ResponseBody
public String add() {
return "add:success";
}
@RequiresPermissions(value = {"user:delete"})
@GetMapping("/delete")
@ResponseBody
public String delete() {
return "delete:success";
}
}
- 在LoginController中添加一个"/403"未授权跳转
@GetMapping("/403")
public String forbid() {
return "403";
}
-
Spring Boot Shiro使用缓存(Redis)
- 引入Redis依赖(shiro-redis)
- 在application.yml中配置Redis
spring:
redis:
# Redis数据库索引(默认为 0)
database: 1
# Redis服务器地址
host: localhost
# Redis服务器连接端口6379
port: 6379
# Redis密码
password:
# Redis连接池
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 0
- 修改Shiro配置类,配置RedisManager并注入到RedisCacheManager
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password:}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.database:0}")
private int database;
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host + ":" + port);
if (StringUtils.isNotBlank(password))
redisManager.setPassword(password);
redisManager.setTimeout(timeout);
redisManager.setDatabase(database);
return redisManager;
}
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
- 将RedisCacheManager设置到SecurityManager中
@Bean
public SecurityManager securityManager() {
// 配置SecurityManager,并注入ShiroRealm
DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
-
Spring Boot Thymeleaf中使用Shiro标签
- 引入thymeleaf-extras-shiro依赖
- 修改Shiro配置类,配置方言标签
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
- 修改页面:在HTML页面中使用Shiro标签时,需要给html标签添加 xmlns:shiro=“http://www.pollix.at/thymeleaf/shiro”
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<p>你好![[${user.username}]]</p>
<p shiro:hasRole="admin">你的角色是超级管理员</p>
<p shiro:hasRole="test">你的角色是测试账户</p>
<div>
<a shiro:hasPermission="user:user" th:href="@{/user/list}">获取用户信息</a>
<a shiro:hasPermission="user:add" th:href="@{/user/add}">添加用户</a>
<a shiro:hasPermission="user:delete" th:href="@{/user/delete}">删除用户</a>
</div>
<a th:href="@{/logout}">注销</a>
</body>
</html>
-
Spring Boot Shiro在线会话管理
- 通过org.apache.shiro.session.mgt.eis.SessionDAO对象的getActiveSessions()方法获取当前所有有效的Session对象。
- 可实现的功能
- 查看当前系统的在线人数
- 查看在线用户的基本信息
- 强制让某个用户下线
- 修改Shiro配置类,配置SessionDAO对象(若使用Redist缓存,则使用RedisSessionDAO
@Bean
public SessionDAO sessionDAO() {
MemorySessionDAO sessionDAO = new MemorySessionDAO();
return sessionDAO;
}
或
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
- 修改Shiro配置类,配置SessionManager对象,用于管理SessionDAO
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<>();
listeners.add(new ShiroSessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
- 上述ShiroSessionListener类为SessionListener接口(Shiro包)的实现类,需手动创建;AtomicInteger类用于统计在线Session的数量
public class ShiroSessionListener implements SessionListener {
private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
}
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
}
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
}
}
- 修改Shiro配置类,将SessionManager对象注入SecurityManager
@Bean
public SecurityManager securityManager() {
// 配置SecurityManager,并注入ShiroRealm
DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(cacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
- 接下来就是具体的功能实现
- 创建UserOnline实体类
- 创建Service接口,定义查看所有在线用户和根据SessionId踢出用户的方法
- 实现Service接口
- 创建SessionController
- 编写页面