2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录

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

时间:2024-03-04 06:42:26

相关推荐

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依赖如下:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--AOP依赖,一定要加,否则权限拦截验证不生效--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!--lombok插件--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--Redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId></dependency><!--mybatisPlus核心库--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.1.0</version></dependency><!--引入阿里数据库连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.6</version></dependency><!--Shiro核心依赖--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency><!--Shiro-redis插件--><dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>3.1.0</version></dependency><!--StringUitlS工具--><dependency><groupId>mons</groupId><artifactId>commons-lang3</artifactId><version>3.5</version></dependency></dependencies>

配置如下:

#配置端口server:port:8764spring:#配置数据源datasource:driver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=falseusername:rootpassword:roottype:com.alibaba.druid.pool.DruidDataSource#Redis数据源redis:host:localhostport:6379timeout:6000password:123456jedis: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:"全局唯一IDUUID";id-type:auto#字段策略IGNORED:"忽略判断"NOT_NULL:"非NULL判断")NOT_EMPTY:"非空判断"field-strategy:NOT_EMPTY#数据库类型db-type:MYSQLconfiguration:#是否开启自动驼峰命名规则映射:从数据库列名到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工具

/***@DescriptionSpring上下文工具类*@AuthorSans*@CreateTime/6/1713:40*/@ComponentpublicclassSpringUtilimplementsApplicationContextAware{privatestaticApplicationContextcontext;/***Spring在bean初始化后会判断是不是ApplicationContextAware的子类*如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去*@AuthorSans*@CreateTime/6/1716:58*/@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{context=applicationContext;}/***通过Name返回指定的Bean*@AuthorSans*@CreateTime/6/1716:03*/publicstatic<T>TgetBean(Class<T>beanClass){returncontext.getBean(beanClass);}}

创建Shiro工具

/***@DescriptionShiro工具类*@AuthorSans*@CreateTime/6/1516:11*/publicclassShiroUtils{/**私有构造器**/privateShiroUtils(){}privatestaticRedisSessionDAOredisSessionDAO=SpringUtil.getBean(RedisSessionDAO.class);/***获取当前用户Session*@AuthorSans*@CreateTime/6/1717:03*@ReturnSysUserEntity用户信息*/publicstaticSessiongetSession(){returnSecurityUtils.getSubject().getSession();}/***用户登出*@AuthorSans*@CreateTime/6/1717:23*/publicstaticvoidlogout(){SecurityUtils.getSubject().logout();}/***获取当前用户信息*@AuthorSans*@CreateTime/6/1717:03*@ReturnSysUserEntity用户信息*/publicstaticSysUserEntitygetUserInfo(){return(SysUserEntity)SecurityUtils.getSubject().getPrincipal();}/***删除用户缓存信息*@AuthorSans*@CreateTime/6/1713:57*@Paramusername用户名称*@ParamisRemoveSession是否删除Session*@Returnvoid*/publicstaticvoiddeleteCache(Stringusername,booleanisRemoveSession){//从缓存中获取SessionSessionsession=null;Collection<Session>sessions=redisSessionDAO.getActiveSessions();SysUserEntitysysUserEntity;Objectattribute=null;for(SessionsessionInfo:sessions){//遍历Session,找到该用户名称对应的Sessionattribute=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;}//删除sessionif(isRemoveSession){redisSessionDAO.delete(session);}//删除Cache,在访问受限接口时会重新授权DefaultWebSecurityManagersecurityManager=(DefaultWebSecurityManager)SecurityUtils.getSecurityManager();Authenticatorauthc=securityManager.getAuthenticator();((LogoutAware)authc).onLogout((SimplePrincipalCollection)attribute);}}

创建Shiro的SessionId生成器

三.编写Shiro核心类

创建Realm用于授权和认证

/***@DescriptionShiro权限匹配和账号密码匹配*@AuthorSans*@CreateTime/6/1511:27*/publicclassShiroRealmextendsAuthorizingRealm{@AutowiredprivateSysUserServicesysUserService;@AutowiredprivateSysRoleServicesysRoleService;@AutowiredprivateSysMenuServicesysMenuService;/***授权权限*用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中*@AuthorSans*@CreateTime/6/1211:44*/@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipalCollection){SimpleAuthorizationInfoauthorizationInfo=newSimpleAuthorizationInfo();SysUserEntitysysUserEntity=(SysUserEntity)principalCollection.getPrimaryPrincipal();//获取用户IDLonguserId=sysUserEntity.getUserId();//这里可以进行授权和处理Set<String>rolesSet=newHashSet<>();Set<String>permsSet=newHashSet<>();//查询角色和权限(这里根据业务自行查询)List<SysRoleEntity>sysRoleEntityList=sysRoleService.selectSysRoleByUserId(userId);for(SysRoleEntitysysRoleEntity:sysRoleEntityList){rolesSet.add(sysRoleEntity.getRoleName());List<SysMenuEntity>sysMenuEntityList=sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());for(SysMenuEntitysysMenuEntity:sysMenuEntityList){permsSet.add(sysMenuEntity.getPerms());}}//将查到的权限和角色分别传入authorizationInfo中authorizationInfo.setStringPermissions(permsSet);authorizationInfo.setRoles(rolesSet);returnauthorizationInfo;}/***身份认证*@AuthorSans*@CreateTime/6/1212:36*/@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throwsAuthenticationException{//获取用户的输入的账号.Stringusername=(String)authenticationToken.getPrincipal();//通过username从数据库中查找User对象,如果找到进行验证//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法SysUserEntityuser=sysUserService.selectUserByName(username);//判断账号是否存在if(user==null){thrownewAuthenticationException();}//判断账号是否被冻结if(user.getState()==null||user.getState().equals("PROHIBIT")){thrownewLockedAccountException();}//进行验证SimpleAuthenticationInfoauthenticationInfo=newSimpleAuthenticationInfo(user,//用户名user.getPassword(),//密码ByteSource.Util.bytes(user.getSalt()),//设置盐值getName());//验证成功开始踢人(清除缓存和Session)ShiroUtils.deleteCache(username,true);returnauthenticationInfo;}}

创建SessionManager类

创建ShiroConfig配置类

/***@DescriptionShiro配置类*@AuthorSans*@CreateTime/6/1017:42*/@ConfigurationpublicclassShiroConfig{privatefinalStringCACHE_KEY="shiro:cache:";privatefinalStringSESSION_KEY="shiro:session:";privatefinalintEXPIRE=1800;//Redis配置@Value("${spring.redis.host}")privateStringhost;@Value("${spring.redis.port}")privateintport;@Value("${spring.redis.timeout}")privateinttimeout;@Value("${spring.redis.password}")privateStringpassword;/***开启Shiro-aop注解支持*@Attention使用代理方式所以需要开启代码支持*@AuthorSans*@CreateTime/6/128:38*/@BeanpublicAuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor(SecurityManagersecurityManager){AuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor=newAuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);returnauthorizationAttributeSourceAdvisor;}/***Shiro基础配置*@AuthorSans*@CreateTime/6/128:42*/@BeanpublicShiroFilterFactoryBeanshiroFilterFactory(SecurityManagersecurityManager){ShiroFilterFactoryBeanshiroFilterFactoryBean=newShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);Map<String,String>filterChainDefinitionMap=newLinkedHashMap<>();//注意过滤器配置顺序不能颠倒//配置过滤:不会被拦截的链接filterChainDefinitionMap.put("/static/**","anon");filterChainDefinitionMap.put("/userLogin/**","anon");filterChainDefinitionMap.put("/**","authc");//配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);returnshiroFilterFactoryBean;}/***安全管理器*@AuthorSans*@CreateTime/6/1210:34*/@BeanpublicSecurityManagersecurityManager(){DefaultWebSecurityManagersecurityManager=newDefaultWebSecurityManager();//自定义Ssession管理securityManager.setSessionManager(sessionManager());//自定义Cache实现securityManager.setCacheManager(cacheManager());//自定义Realm验证securityManager.setRealm(shiroRealm());returnsecurityManager;}/***身份验证器*@AuthorSans*@CreateTime/6/1210:37*/@BeanpublicShiroRealmshiroRealm(){ShiroRealmshiroRealm=newShiroRealm();shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());returnshiroRealm;}/***凭证匹配器*将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置*@AuthorSans*@CreateTime/6/1210:48*/@BeanpublicHashedCredentialsMatcherhashedCredentialsMatcher(){HashedCredentialsMatchershaCredentialsMatcher=newHashedCredentialsMatcher();//散列算法:这里使用SHA256算法;shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);//散列的次数,比如散列两次,相当于md5(md5(""));shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);returnshaCredentialsMatcher;}/***配置Redis管理器*@Attention使用的是shiro-redis开源插件*@AuthorSans*@CreateTime/6/1211:06*/@BeanpublicRedisManagerredisManager(){RedisManagerredisManager=newRedisManager();redisManager.setHost(host);redisManager.setPort(port);redisManager.setTimeout(timeout);redisManager.setPassword(password);returnredisManager;}/***配置Cache管理器*用于往Redis存储权限和角色标识*@Attention使用的是shiro-redis开源插件*@AuthorSans*@CreateTime/6/1212:37*/@BeanpublicRedisCacheManagercacheManager(){RedisCacheManagerredisCacheManager=newRedisCacheManager();redisCacheManager.setRedisManager(redisManager());redisCacheManager.setKeyPrefix(CACHE_KEY);//配置缓存的话要求放在session里面的实体类必须有个id标识redisCacheManager.setPrincipalIdFieldName("userId");returnredisCacheManager;}/***SessionID生成器*@AuthorSans*@CreateTime/6/1213:12*/@BeanpublicShiroSessionIdGeneratorsessionIdGenerator(){returnnewShiroSessionIdGenerator();}/***配置RedisSessionDAO*@Attention使用的是shiro-redis开源插件*@AuthorSans*@CreateTime/6/1213:44*/@BeanpublicRedisSessionDAOredisSessionDAO(){RedisSessionDAOredisSessionDAO=newRedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());redisSessionDAO.setKeyPrefix(SESSION_KEY);redisSessionDAO.setExpire(expire);returnredisSessionDAO;}/***配置Session管理器*@AuthorSans*@CreateTime/6/1214:25*/@BeanpublicSessionManagersessionManager(){ShiroSessionManagershiroSessionManager=newShiroSessionManager();shiroSessionManager.setSessionDAO(redisSessionDAO());returnshiroSessionManager;}}

四.实现权限控制

Shiro可以用代码或者注解来控制权限,通常我们使用注解控制,不仅简单方便,而且更加灵活.Shiro注解一共有五个:

一般情况下我们在项目中做权限控制,使用最多的是RequiresPermissions和RequiresRoles,允许存在多个角色和权限,默认逻辑是AND,也就是同时拥有这些才可以访问方法,可以在注解中以参数的形式设置成OR

示例

使用顺序:Shiro注解是存在顺序的,当多个注解在一个方法上的时候,会逐个检查,知道全部通过为止,默认拦截顺序是:RequiresRoles->RequiresPermissions->RequiresAuthentication->RequiresUser->RequiresGuest

示例

创建UserRoleController角色拦截测试类

/***@Description角色测试*@AuthorSans*@CreateTime/6/1911:38*/@RestController@RequestMapping("/role")publicclassUserRoleController{@AutowiredprivateSysUserServicesysUserService;@AutowiredprivateSysRoleServicesysRoleService;@AutowiredprivateSysMenuServicesysMenuService;@AutowiredprivateSysRoleMenuServicesysRoleMenuService;/***管理员角色测试接口*@AuthorSans*@CreateTime/6/1910:38*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getAdminInfo")@RequiresRoles("ADMIN")publicMap<String,Object>getAdminInfo(){Map<String,Object>map=newHashMap<>();map.put("code",200);map.put("msg","这里是只有管理员角色能访问的接口");returnmap;}/***用户角色测试接口*@AuthorSans*@CreateTime/6/1910:38*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getUserInfo")@RequiresRoles("USER")publicMap<String,Object>getUserInfo(){Map<String,Object>map=newHashMap<>();map.put("code",200);map.put("msg","这里是只有用户角色能访问的接口");returnmap;}/***角色测试接口*@AuthorSans*@CreateTime/6/1910:38*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getRoleInfo")@RequiresRoles(value={"ADMIN","USER"},logical=Logical.OR)@RequiresUserpublicMap<String,Object>getRoleInfo(){Map<String,Object>map=newHashMap<>();map.put("code",200);map.put("msg","这里是只要有ADMIN或者USER角色能访问的接口");returnmap;}/***登出(测试登出)*@AuthorSans*@CreateTime/6/1910:38*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getLogout")@RequiresUserpublicMap<String,Object>getLogout(){ShiroUtils.logout();Map<String,Object>map=newHashMap<>();map.put("code",200);map.put("msg","登出");returnmap;}}

创建UserMenuController权限拦截测试类

/***@Description权限测试*@AuthorSans*@CreateTime/6/1911:38*/@RestController@RequestMapping("/menu")publicclassUserMenuController{@AutowiredprivateSysUserServicesysUserService;@AutowiredprivateSysRoleServicesysRoleService;@AutowiredprivateSysMenuServicesysMenuService;@AutowiredprivateSysRoleMenuServicesysRoleMenuService;/***获取用户信息集合*@AuthorSans*@CreateTime/6/1910:36*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getUserInfoList")@RequiresPermissions("sys:user:info")publicMap<String,Object>getUserInfoList(){Map<String,Object>map=newHashMap<>();List<SysUserEntity>sysUserEntityList=sysUserService.list();map.put("sysUserEntityList",sysUserEntityList);returnmap;}/***获取角色信息集合*@AuthorSans*@CreateTime/6/1910:37*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getRoleInfoList")@RequiresPermissions("sys:role:info")publicMap<String,Object>getRoleInfoList(){Map<String,Object>map=newHashMap<>();List<SysRoleEntity>sysRoleEntityList=sysRoleService.list();map.put("sysRoleEntityList",sysRoleEntityList);returnmap;}/***获取权限信息集合*@AuthorSans*@CreateTime/6/1910:38*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getMenuInfoList")@RequiresPermissions("sys:menu:info")publicMap<String,Object>getMenuInfoList(){Map<String,Object>map=newHashMap<>();List<SysMenuEntity>sysMenuEntityList=sysMenuService.list();map.put("sysMenuEntityList",sysMenuEntityList);returnmap;}/***获取所有数据*@AuthorSans*@CreateTime/6/1910:38*@ReturnMap<String,Object>返回结果*/@RequestMapping("/getInfoAll")@RequiresPermissions("sys:info:all")publicMap<String,Object>getInfoAll(){Map<String,Object>map=newHashMap<>();List<SysUserEntity>sysUserEntityList=sysUserService.list();map.put("sysUserEntityList",sysUserEntityList);List<SysRoleEntity>sysRoleEntityList=sysRoleService.list();map.put("sysRoleEntityList",sysRoleEntityList);List<SysMenuEntity>sysMenuEntityList=sysMenuService.list();map.put("sysMenuEntityList",sysMenuEntityList);returnmap;}/***添加管理员角色权限(测试动态权限更新)*@AuthorSans*@CreateTime/6/1910:39*@Paramusername用户ID*@ReturnMap<String,Object>返回结果*/@RequestMapping("/addMenu")publicMap<String,Object>addMenu(){//添加管理员角色权限SysRoleMenuEntitysysRoleMenuEntity=newSysRoleMenuEntity();sysRoleMenuEntity.setMenuId(4L);sysRoleMenuEntity.setRoleId(1L);sysRoleMenuService.save(sysRoleMenuEntity);//清除缓存Stringusername="admin";ShiroUtils.deleteCache(username,false);Map<String,Object>map=newHashMap<>();map.put("code",200);map.put("msg","权限添加成功");returnmap;}}

创建UserLoginController登录类

/***@Description用户登录*@AuthorSans*@CreateTime/6/1715:21*/@RestController@RequestMapping("/userLogin")publicclassUserLoginController{@AutowiredprivateSysUserServicesysUserService;/***登录*@AuthorSans*@CreateTime/6/209:21*/@RequestMapping("/login")publicMap<String,Object>login(@RequestBodySysUserEntitysysUserEntity){Map<String,Object>map=newHashMap<>();//进行身份验证try{//验证身份和登陆Subjectsubject=SecurityUtils.getSubject();UsernamePasswordTokentoken=newUsernamePasswordToken(sysUserEntity.getUsername(),sysUserEntity.getPassword());//验证成功进行登录操作subject.login(token);}catch(IncorrectCredentialsExceptione){map.put("code",500);map.put("msg","用户不存在或者密码错误");returnmap;}catch(LockedAccountExceptione){map.put("code",500);map.put("msg","登录失败,该用户已被冻结");returnmap;}catch(AuthenticationExceptione){map.put("code",500);map.put("msg","该用户不存在");returnmap;}catch(Exceptione){map.put("code",500);map.put("msg","未知异常");returnmap;}map.put("code",0);map.put("msg","登录成功");map.put("token",ShiroUtils.getSession().getId().toString());returnmap;}/***未登录*@AuthorSans*@CreateTime/6/209:22*/@RequestMapping("/unauth")publicMap<String,Object>unauth(){Map<String,Object>map=newHashMap<>();map.put("code",500);map.put("msg","未登录");returnmap;}}

五.POSTMAN测试

登录成功后会返回TOKEN,因为是单点登录,再次登陆的话会返回新的TOKEN,之前Redis的TOKEN就会失效了

当第一次访问接口后我们可以看到缓存中已经有权限数据了,在次访问接口的时候,Shiro会直接去缓存中拿取权限,注意访问接口时候要设置请求头.

ADMIN这个号现在没有sys:info:all这个权限的,所以无法访问getInfoAll接口,我们要动态分配权限后,要清掉缓存,在访问接口时候,Shiro会去重新执行授权方法,之后再次把权限和角色数据放入缓存中

访问添加权限测试接口,因为是测试,我把增加权限的用户ADMIN写死在里面了,权限添加后,调用工具类清掉缓存,我们可以发现,Redis中已经没有缓存了

再次访问getInfoAll接口,因为缓存中没有数据,Shiro会重新授权查询权限,拦截通过

六.项目源码

/liselotte/spring-boot-shiro-demo

/xuyulong/my-java-demo

推荐阅读(点击即可跳转阅读)

1.SpringBoot内容聚合

2.面试题内容聚合

3.设计模式内容聚合

4.Mybatis内容聚合

5.多线程内容聚合

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。