SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录
武哥领读:
有源码的教程,不会的同学下载源码,根据教程学一下哈~
作者:Sans_
juejin.im/post/5d087d605188256de9779e64
一. 说明
Shiro 是一个安全框架, 项目中主要用它做认证, 授权, 加密, 以及用户的会话管理, 虽然 Shiro 没有 SpringSecurity 功能更丰富, 但是它轻量, 简单, 在项目中通常业务需求 Shiro 也都能胜任.
二. 项目环境
MyBatis-Plus 版本: 3.1.0
SpringBoot 版本: 2.1.5
JDK 版本: 1.8
Shiro 版本: 1.4
Shiro-redis 插件版本: 3.1.0
数据表 (SQL 文件在项目中): 数据库中测试号的密码进行了加密, 密码皆为 123456
Maven 依赖如下:
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-aop
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-data-redis-reactive
com.baomidou
mybatis-plus-boot-starter
3.1.0
com.alibaba
druid
1.1.6
org.apache.shiro
shiro-spring
1.4.0
org.crazycake
shiro-redis
3.1.0
org.apache.commons
commons-lang3
3.5
配置如下:
# 配置端口
server:
port: 8764
spring:
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
# Redis数据源
redis:
host: localhost
port: 6379
timeout: 6000
password: 123456
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
# mybatis-plus相关配置
mybatis-plus:
# xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
mapper-locations: classpath:mapper/*.xml
# 以下配置均有默认值,可以不设置
global-config:
db-config:
#主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: auto
#字段策略 IGNORED:"忽略判断" NOT_NULL:"非 NULL 判断") NOT_EMPTY:"非空判断"
field-strategy: NOT_EMPTY
#数据库类型
db-type: MYSQL
configuration:
# 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
map-underscore-to-camel-case: true
# 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
call-setters-on-nulls: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
二. 编写项目基础类
用户实体, Dao,Service 等在这里省略, 请参考源码
编写 Exception 类来处理 Shiro 权限拦截异常
创建 SHA256Util 加密工具
创建 Spring 工具
/**
* @Description Spring上下文工具类
* @Author Sans
* @CreateTime 2019/6/17 13:40
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext context;
/**
* Spring在bean初始化后会判断是不是ApplicationContextAware的子类
* 如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去
* @Author Sans
* @CreateTime 2019/6/17 16:58
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 通过Name返回指定的Bean
* @Author Sans
* @CreateTime 2019/6/17 16:03
*/
public static T getBean(Class beanClass) {
return context.getBean(beanClass);
}
}
创建 Shiro 工具
/**
* @Description Shiro工具类
* @Author Sans
* @CreateTime 2019/6/15 16:11
*/
public class ShiroUtils {
/** 私有构造器 **/
private ShiroUtils(){}
private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
/**
* 获取当前用户Session
* @Author Sans
* @CreateTime 2019/6/17 17:03
* @Return SysUserEntity 用户信息
*/
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
/**
* 用户登出
* @Author Sans
* @CreateTime 2019/6/17 17:23
*/
public static void logout() {
SecurityUtils.getSubject().logout();
}
/**
* 获取当前用户信息
* @Author Sans
* @CreateTime 2019/6/17 17:03
* @Return SysUserEntity 用户信息
*/
public static SysUserEntity getUserInfo() {
return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
}
/**
* 删除用户缓存信息
* @Author Sans
* @CreateTime 2019/6/17 13:57
* @Param username 用户名称
* @Param isRemoveSession 是否删除Session
* @Return void
*/
public static void deleteCache(String username, boolean isRemoveSession){
//从缓存中获取Session
Session session = null;
Collection sessions = redisSessionDAO.getActiveSessions();
SysUserEntity sysUserEntity;
Object attribute = null;
for(Session sessionInfo : sessions){
//遍历Session,找到该用户名称对应的Session
attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
sysUserEntity = (SysUserEntity) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (sysUserEntity == null) {
continue;
}
if (Objects.equals(sysUserEntity.getUsername(), username)) {
session=sessionInfo;
}
}
if (session == null||attribute == null) {
return;
}
//删除session
if (isRemoveSession) {
redisSessionDAO.delete(session);
}
//删除Cache,在访问受限接口时会重新授权
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
Authenticator authc = securityManager.getAuthenticator();
((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
}
}
创建 Shiro 的 SessionId 生成器
三. 编写 Shiro 核心类
创建 Realm 用于授权和认证
/**
* @Description Shiro权限匹配和账号密码匹配
* @Author Sans
* @CreateTime 2019/6/15 11:27
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
/**
* 授权权限
* 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
* @Author Sans
* @CreateTime 2019/6/12 11:44
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
//获取用户ID
Long userId =sysUserEntity.getUserId();
//这里可以进行授权和处理
Set rolesSet = new HashSet<>();
Set permsSet = new HashSet<>();
//查询角色和权限(这里根据业务自行查询)
List sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
for (SysRoleEntity sysRoleEntity:sysRoleEntityList) {
rolesSet.add(sysRoleEntity.getRoleName());
List sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
for (SysMenuEntity sysMenuEntity :sysMenuEntityList) {
permsSet.add(sysMenuEntity.getPerms());
}
}
//将查到的权限和角色分别传入authorizationInfo中
authorizationInfo.setStringPermissions(permsSet);
authorizationInfo.setRoles(rolesSet);
return authorizationInfo;
}
/**
* 身份认证
* @Author Sans
* @CreateTime 2019/6/12 12:36
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String) authenticationToken.getPrincipal();
//通过username从数据库中查找 User对象,如果找到进行验证
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUserEntity user = sysUserService.selectUserByName(username);
//判断账号是否存在
if (user == null) {
throw new AuthenticationException();
}
//判断账号是否被冻结
if (user.getState()==null||user.getState().equals("PROHIBIT")){
throw new LockedAccountException();
}
//进行验证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户名
user.getPassword(), //密码
ByteSource.Util.bytes(user.getSalt()), //设置盐值
getName()
);
//验证成功开始踢人(清除缓存和Session)
ShiroUtils.deleteCache(username,true);
return authenticationInfo;
}
}
创建 SessionManager 类
创建 ShiroConfig 配置类
/**
* @Description Shiro配置类
* @Author Sans
* @CreateTime 2019/6/10 17:42
*/
@Configuration
public class ShiroConfig {
private final String CACHE_KEY = "shiro:cache:";
private final String SESSION_KEY = "shiro:session:";
private final int EXPIRE = 1800;
//Redis配置
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.password}")
private String password;
/**
* 开启Shiro-aop注解支持
* @Attention 使用代理方式所以需要开启代码支持
* @Author Sans
* @CreateTime 2019/6/12 8:38
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro基础配置
* @Author Sans
* @CreateTime 2019/6/12 8:42
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterChainDefinitionMap = new LinkedHashMap<>();
// 注意过滤器配置顺序不能颠倒
// 配置过滤:不会被拦截的链接
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/userLogin/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 安全管理器
* @Author Sans
* @CreateTime 2019/6/12 10:34
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义Ssession管理
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(cacheManager());
// 自定义Realm验证
securityManager.setRealm(shiroRealm());
return securityManager;
}
/**
* 身份验证器
* @Author Sans
* @CreateTime 2019/6/12 10:37
*/
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* 凭证匹配器
* 将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置
* @Author Sans
* @CreateTime 2019/6/12 10:48
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用SHA256算法;
shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
// 散列的次数,比如散列两次,相当于 md5(md5(""));
shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
return shaCredentialsMatcher;
}
/**
* 配置Redis管理器
* @Attention 使用的是shiro-redis开源插件
* @Author Sans
* @CreateTime 2019/6/12 11:06
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
/**
* 配置Cache管理器
* 用于往Redis存储权限和角色标识
* @Attention 使用的是shiro-redis开源插件
* @Author Sans
* @CreateTime 2019/6/12 12:37
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix(CACHE_KEY);
// 配置缓存的话要求放在session里面的实体类必须有个id标识
redisCacheManager.setPrincipalIdFieldName("userId");
return redisCacheManager;
}
/**
* SessionID生成器
* @Author Sans
* @CreateTime 2019/6/12 13:12
*/
@Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
return new ShiroSessionIdGenerator();
}
/**
* 配置RedisSessionDAO
* @Attention 使用的是shiro-redis开源插件
* @Author Sans
* @CreateTime 2019/6/12 13:44
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setExpire(expire);
return redisSessionDAO;
}
/**
* 配置Session管理器
* @Author Sans
* @CreateTime 2019/6/12 14:25
*/
@Bean
public SessionManager sessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
shiroSessionManager.setSessionDAO(redisSessionDAO());
return shiroSessionManager;
}
}
四. 实现权限控制
Shiro 可以用代码或者注解来控制权限, 通常我们使用注解控制, 不仅简单方便, 而且更加灵活. Shiro 注解一共有五个:
一般情况下我们在项目中做权限控制, 使用最多的是 RequiresPermissions 和 RequiresRoles, 允许存在多个角色和权限, 默认逻辑是 AND, 也就是同时拥有这些才可以访问方法, 可以在注解中以参数的形式设置成 OR
示例
使用顺序: Shiro 注解是存在顺序的, 当多个注解在一个方法上的时候, 会逐个检查, 知道全部通过为止, 默认拦截顺序是: RequiresRoles->RequiresPermissions->RequiresAuthentication->
RequiresUser->RequiresGuest
示例
创建 UserRoleController 角色拦截测试类
/**
* @Description 角色测试
* @Author Sans
* @CreateTime 2019/6/19 11:38
*/
@RestController
@RequestMapping("/role")
public class UserRoleController {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private SysRoleMenuService sysRoleMenuService;
/**
* 管理员角色测试接口
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map 返回结果
*/
@RequestMapping("/getAdminInfo")
@RequiresRoles("ADMIN")
public Map getAdminInfo(){
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只有管理员角色能访问的接口");
return map;
}
/**
* 用户角色测试接口
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map 返回结果
*/
@RequestMapping("/getUserInfo")
@RequiresRoles("USER")
public Map getUserInfo(){
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只有用户角色能访问的接口");
return map;
}
/**
* 角色测试接口
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map 返回结果
*/
@RequestMapping("/getRoleInfo")
@RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
@RequiresUser
public Map getRoleInfo(){
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只要有ADMIN或者USER角色能访问的接口");
return map;
}
/**
* 登出(测试登出)
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map 返回结果
*/
@RequestMapping("/getLogout")
@RequiresUser
public Map getLogout(){
ShiroUtils.logout();
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","登出");
return map;
}
}
创建 UserMenuController 权限拦截测试类
/**
* @Description 权限测试
* @Author Sans
* @CreateTime 2019/6/19 11:38
*/
@RestController
@RequestMapping("/menu")
public class UserMenuController {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private SysRoleMenuService sysRoleMenuService;
/**
* 获取用户信息集合
* @Author Sans
* @CreateTime 2019/6/19 10:36
* @Return Map 返回结果
*/
@RequestMapping("/getUserInfoList")
@RequiresPermissions("sys:user:info")
public Map getUserInfoList(){
Map map = new HashMap<>();
List sysUserEntityList = sysUserService.list();
map.put("sysUserEntityList",sysUserEntityList);
return map;
}
/**
* 获取角色信息集合
* @Author Sans
* @CreateTime 2019/6/19 10:37
* @Return Map 返回结果
*/
@RequestMapping("/getRoleInfoList")
@RequiresPermissions("sys:role:info")
public Map getRoleInfoList(){
Map map = new HashMap<>();
List sysRoleEntityList = sysRoleService.list();
map.put("sysRoleEntityList",sysRoleEntityList);
return map;
}
/**
* 获取权限信息集合
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map 返回结果
*/
@RequestMapping("/getMenuInfoList")
@RequiresPermissions("sys:menu:info")
public Map getMenuInfoList(){
Map map = new HashMap<>();
List sysMenuEntityList = sysMenuService.list();
map.put("sysMenuEntityList",sysMenuEntityList);
return map;
}
/**
* 获取所有数据
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map 返回结果
*/
@RequestMapping("/getInfoAll")
@RequiresPermissions("sys:info:all")
public Map getInfoAll(){
Map map = new HashMap<>();
List sysUserEntityList = sysUserService.list();
map.put("sysUserEntityList",sysUserEntityList);
List sysRoleEntityList = sysRoleService.list();
map.put("sysRoleEntityList",sysRoleEntityList);
List sysMenuEntityList = sysMenuService.list();
map.put("sysMenuEntityList",sysMenuEntityList);
return map;
}
/**
* 添加管理员角色权限(测试动态权限更新)
* @Author Sans
* @CreateTime 2019/6/19 10:39
* @Param username 用户ID
* @Return Map 返回结果
*/
@RequestMapping("/addMenu")
public Map addMenu(){
//添加管理员角色权限
SysRoleMenuEntity sysRoleMenuEntity = new SysRoleMenuEntity();
sysRoleMenuEntity.setMenuId(4L);
sysRoleMenuEntity.setRoleId(1L);
sysRoleMenuService.save(sysRoleMenuEntity);
//清除缓存
String username = "admin";
ShiroUtils.deleteCache(username,false);
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","权限添加成功");
return map;
}
}
创建 UserLoginController 登录类
/**
* @Description 用户登录
* @Author Sans
* @CreateTime 2019/6/17 15:21
*/
@RestController
@RequestMapping("/userLogin")
public class UserLoginController {
@Autowired
private SysUserService sysUserService;
/**
* 登录
* @Author Sans
* @CreateTime 2019/6/20 9:21
*/
@RequestMapping("/login")
public Map login(@RequestBody SysUserEntity sysUserEntity){
Map map = new HashMap<>();
//进行身份验证
try{
//验证身份和登陆
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
//验证成功进行登录操作
subject.login(token);
}catch (IncorrectCredentialsException e) {
map.put("code",500);
map.put("msg","用户不存在或者密码错误");
return map;
} catch (LockedAccountException e) {
map.put("code",500);
map.put("msg","登录失败,该用户已被冻结");
return map;
} catch (AuthenticationException e) {
map.put("code",500);
map.put("msg","该用户不存在");
return map;
} catch (Exception e) {
map.put("code",500);
map.put("msg","未知异常");
return map;
}
map.put("code",0);
map.put("msg","登录成功");
map.put("token",ShiroUtils.getSession().getId().toString());
return map;
}
/**
* 未登录
* @Author Sans
* @CreateTime 2019/6/20 9:22
*/
@RequestMapping("/unauth")
public Map unauth(){
Map map = new HashMap<>();
map.put("code",500);
map.put("msg","未登录");
return map;
}
}
五. POSTMAN 测试
登录成功后会返回 TOKEN, 因为是单点登录, 再次登陆的话会返回新的 TOKEN, 之前 Redis 的 TOKEN 就会失效了
当第一次访问接口后我们可以看到缓存中已经有权限数据了, 在次访问接口的时候, Shiro 会直接去缓存中拿取权限, 注意访问接口时候要设置请求头.
ADMIN 这个号现在没有 sys:info:all 这个权限的, 所以无法访问 getInfoAll 接口, 我们要动态分配权限后, 要清掉缓存, 在访问接口时候, Shiro 会去重新执行授权方法, 之后再次把权限和角色数据放入缓存中
访问添加权限测试接口, 因为是测试, 我把增加权限的用户 ADMIN 写死在里面了, 权限添加后, 调用工具类清掉缓存, 我们可以发现, Redis 中已经没有缓存了
再次访问 getInfoAll 接口, 因为缓存中没有数据, Shiro 会重新授权查询权限, 拦截通过
六. 项目源码
https://gitee.com/liselotte/spring-boot-shiro-demo
https://github.com/xuyulong2017/my-java-demo
-END- 我是武哥,最后给大家免费分享我写的 10 万字 Spring Boot 学习笔记(带完整目录)以及对应的源码。这是我之前在 CSDN 开的一门课,所以笔记非常详细完整,我准备将资料分享出来给大家免费学习,相信大家看完一定会有所收获(下面有下载方式)。
可以看出,我当时备课非常详细,目录非常完整,读者可以手把手跟着笔记,结合源代码来学习。现在免费分享出来,有需要的读者可以下载学习,就在我下面的公众号Java秃头哥里回复:笔记,就行。
如有文章对你有帮助,
在看和转发是对我最大的支持!
关注Java秃头哥
只有秃头才能更强
点赞是最大的支持