栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java > SpringBoot

SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录

SpringBoot 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录

小Hub领读:

有源码的教程,不会的同学下载源码,根据教程学一下哈~


作者:Sans_

一. 说明

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
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 context = applicationContext;
    }
    
    public static  T getBean(Class beanClass) {
 return context.getBean(beanClass);
    }
}


创建 Shiro 工具


public class ShiroUtils {

    
    private ShiroUtils(){}

    private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);

    
    public static Session getSession() {
 return SecurityUtils.getSubject().getSession();
    }

    
    public static void logout() {
 SecurityUtils.getSubject().logout();
    }

    
    public static SysUserEntity getUserInfo() {
      return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
    }

    
    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 用于授权和认证


public class ShiroRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    
    @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;
    }

    
    @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 配置类


@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;

    
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
 authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
 return authorizationAttributeSourceAdvisor;
    }

    
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
 shiroFilterFactoryBean.setSecurityManager(securityManager);
 Map filterChainDefinitionMap = new linkedHashMap<>();
 // 注意过滤器配置顺序不能颠倒
 // 配置过滤:不会被拦截的链接
 filterChainDefinitionMap.put("/static
    @Bean
    public SecurityManager securityManager() {
 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
 // 自定义Ssession管理
 securityManager.setSessionManager(sessionManager());
 // 自定义Cache实现
 securityManager.setCacheManager(cacheManager());
 // 自定义Realm验证
 securityManager.setRealm(shiroRealm());
 return securityManager;
    }

    
    @Bean
    public ShiroRealm shiroRealm() {
 ShiroRealm shiroRealm = new ShiroRealm();
 shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
 return shiroRealm;
    }

    
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
 HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
 // 散列算法:这里使用SHA256算法;
 shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
 // 散列的次数,比如散列两次,相当于 md5(md5(""));
 shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
 return shaCredentialsMatcher;
    }

    
    @Bean
    public RedisManager redisManager() {
 RedisManager redisManager = new RedisManager();
 redisManager.setHost(host);
 redisManager.setPort(port);
 redisManager.setTimeout(timeout);
 redisManager.setPassword(password);
 return redisManager;
    }

    
    @Bean
    public RedisCacheManager cacheManager() {
 RedisCacheManager redisCacheManager = new RedisCacheManager();
 redisCacheManager.setRedisManager(redisManager());
 redisCacheManager.setKeyPrefix(CACHE_KEY);
 // 配置缓存的话要求放在session里面的实体类必须有个id标识
 redisCacheManager.setPrincipalIdFieldName("userId");
 return redisCacheManager;
    }

    
    @Bean
    public ShiroSessionIdGenerator sessionIdGenerator(){
 return new ShiroSessionIdGenerator();
    }

    
    @Bean
    public RedisSessionDAO redisSessionDAO() {
 RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
 redisSessionDAO.setRedisManager(redisManager());
 redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
 redisSessionDAO.setKeyPrefix(SESSION_KEY);
 redisSessionDAO.setExpire(expire);
 return redisSessionDAO;
    }

    
    @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 角色拦截测试类


@RestController
@RequestMapping("/role")
public class UserRoleController {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private SysRoleMenuService sysRoleMenuService;

    
    @RequestMapping("/getAdminInfo")
    @RequiresRoles("ADMIN")
    public Map getAdminInfo(){
 Map map = new HashMap<>();
 map.put("code",200);
 map.put("msg","这里是只有管理员角色能访问的接口");
 return map;
    }

    
    @RequestMapping("/getUserInfo")
    @RequiresRoles("USER")
    public Map getUserInfo(){
 Map map = new HashMap<>();
 map.put("code",200);
 map.put("msg","这里是只有用户角色能访问的接口");
 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;
    }

    
    @RequestMapping("/getLogout")
    @RequiresUser
    public Map getLogout(){
 ShiroUtils.logout();
 Map map = new HashMap<>();
 map.put("code",200);
 map.put("msg","登出");
 return map;
    }
}


创建 UserMenuController 权限拦截测试类


@RestController
@RequestMapping("/menu")
public class UserMenuController {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private SysRoleMenuService sysRoleMenuService;

    
    @RequestMapping("/getUserInfoList")
    @RequiresPermissions("sys:user:info")
    public Map getUserInfoList(){
 Map map = new HashMap<>();
 List sysUserEntityList = sysUserService.list();
 map.put("sysUserEntityList",sysUserEntityList);
 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;
    }

    
    @RequestMapping("/getMenuInfoList")
    @RequiresPermissions("sys:menu:info")
    public Map getMenuInfoList(){
 Map map = new HashMap<>();
 List sysMenuEntityList = sysMenuService.list();
 map.put("sysMenuEntityList",sysMenuEntityList);
 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;
    }

    
    @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 登录类


@RestController
@RequestMapping("/userLogin")
public class UserLoginController {

    @Autowired
    private SysUserService sysUserService;

    
    @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;
    }
    
    @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 会重新授权查询权限, 拦截通过

六. 项目源码

gitee.com/liselotte/spring-boot-shiro-demo

github.com/xuyulong2017/my-java-demo


(完)

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号