# shiro-frame **Repository Path**: lexapps/shiro-frame ## Basic Information - **Project Name**: shiro-frame - **Description**: APP-WEB 多模块应用下的shiro登入框架 适用于前后端分离以及session共享,APP与web共享一套shiro-security模块应用.但是可以自定义获取用户信息.APP与web分开管理.但是权限操作可以相同处理 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 3 - **Created**: 2019-02-17 - **Last Updated**: 2021-07-30 ## Categories & Tags **Categories**: authority-management **Tags**: None ## README # 本框架最适用于前后端分离+分布式下的session共享的权限项目框架 tip:这是一个通用模板框架 可以适用于 前后端不分离 或者 前后端不分离 就算不是分布式下依旧可以,但是没必要. ## `2019/2/27 更新:重写shiro未授权或未登入的跳转方式 具体看step5` ## 使用主体框架为: springboot+mybatis-plus+shiro+redis ## 框架简述: ### 0.本项目为多模块项目 有security模块(shiro-core 链接最底下),只有需要权限框架才需要引入(复制)该模块依赖.不然会出现没有实现类错误(已经@Autowired) 需要时候可以直接复制security框架或者引入shiro-core依赖.但是需要注意的是.springbootApplication包扫描需要扫描到.第二就是companyPropertis是从配置文件中读取,不可以没有.如果是复制的可以直接改前缀.引入依赖的就要重新install再引入 ### 也就是说整体框架已经搭完成,各种登入配置接近完成.每个web或者App只需要自定义shiroConfig的登入拦截配置(必须) 和 userDetailServiceImpl(必须)实现里面相应的方法.框架已经加了日志可以直观看出来,具体可以看操作步骤0 #### 每个依赖security模块时候需要自定义实现类.目前有表单登入,短信验证码登入,社交登入 各种配置需要 username login:company:xxx password xxx 下列例子(以下的company是为分隔符.用于分辨登入方式,company由配置文件中读取,可以叫abc那么分隔符就是abc) ##### (1) 表单 username form:company:linenxi password 123456 --表示 表单登入账号为 linenxi ,密码为 123456 ##### (2) 验证码 username mobile:company:13587872968 password 9876 --表示 手机验证码登入手机号为 13587872968 ,验证码为 9876 ##### (3) 社交登入 username social:company:abcdefg password openId:company:wx --表示 社交登入账号为下的 abcdefg,登入方式为openId下的wx登入(也有可能有些需要开放平台下就需要用openId改为unionId,qq登入就将wx改为qq) ### 1.背景 在最基础的shiro框架加前后端不分离技术下,浏览器都会在cookie中携带sessionId,每次访问服务器都会带上sessionId访问服务器.从而达到保持会话目的 ### 2.衍生 在前后端分离技术下http请求都是无状态的 往往不采用cookie保存sessionId的方式而是用请求头中携带token.token中附带sessionId从而来保持会话目的 ### 3.最终 再基于分布式架构下,session共享就是一个最主要的难点.一种办法是tomcat内session共享,但是耗费资源.第二种就是将session储存在redis中.不管多少服务器都是先访问redis.最终达成保持会话 ### 3.1 tip:既然已经不是从服务器内的session来保持会话,那就是要重写获取session的方法.在经过源码断点之后发现最终是SessionDAO类获取session.也就是重写该类方法. ## 操作步骤 ### 0 ShiroConfig实现需要拦截的配置以及登入失败等入口 @Configuration @Slf4j public class APPShiroConfig { @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map filterMap = new LinkedHashMap(); filterMap.put("111111111111111111111/shiro/mp/login", "anon"); filterMap.put("22222222222222222222/**", "authc"); log.info("当前系统拦截模式为:{}", filterMap); shiroFilterFactoryBean.setUnauthorizedUrl("33333333333/admin/page/noAuth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); shiroFilterFactoryBean.setLoginUrl("4444444444444444444/shiro/mp/nologinCode"); return shiroFilterFactoryBean; } } ### 0 框架必须要的寻找用户实现类 表单登入需要用户名查找 验证码登入从redis查找 社交登入从社交表查找,. 只需要实现ShiroUserDetailService接口,整体框架就完成 @Component @Slf4j public class ShiroUserDetailServiceImpl implements ShiroUserDetailService { @Autowired private CompanyProperties companyProperties; @Autowired private MpUserInfoMapper mpUserInfoMapper; @Autowired private MpSocialInfoMapper mpSocialInfoMapper; @Autowired private MpShiroPermsInfoMapper mpShiroPermsInfoMapper; @Override public ShiroUserDetail getByMobile(String mobile) throws AuthenticationException { MpUserInfo mpUserInfo = mpUserInfoMapper.selectOne(new QueryWrapper().eq("mobile", mobile)); if (PublicUtil.isEmpty(mpUserInfo)) { log.error("未找到与该手机号相关联的用户 :mobile=[ {} ]", mobile); throw new AccountException("未找到与该手机号相关联的用户"); } return this.filling(mpUserInfo); } @Override public ShiroUserDetail getByUserName(String username) throws AuthenticationException { MpUserInfo mpUserInfo = mpUserInfoMapper.selectOne(new QueryWrapper().eq("username", username)); if (PublicUtil.isEmpty(mpUserInfo)) { log.error("未找到该用户名对应的用户 :username=[ {} ]", username); throw new AccountException("未找到该用户名对应的用户"); } return this.filling(mpUserInfo); } @Override public ShiroUserDetail getBySocialProvider(String itemId, String socialProviderId) throws AuthenticationException { String[] split = socialProviderId.split(":" + companyProperties.getLoginSplit() + ":"); if (split.length!=2) { throw new PrefixNotFoundException("社交登入,没有分隔符"); } String social = split[0]; // openId/unionId String socialUnderLine = PublicUtil.underline(new StringBuffer(social)).toString(); String providerId = split[1]; //wx/qq log.warn("社交登入中的: social= [ {} 登入,主体为 {} ],providerId=[ {} ]", social,itemId, providerId); List mpSocialInfos = mpSocialInfoMapper.selectList( new QueryWrapper().eq(socialUnderLine, itemId).eq("provider_id", providerId)); if (PublicUtil.isEmpty(mpSocialInfos)) { log.error("未找到社交登入的账号 :social=[ {} ],providerId=[ {} ]", social, providerId); throw new SocialUserNotFoundException("未找到对应的社交登入记录"); } long userId = Long.parseLong(mpSocialInfos.iterator().next().getUserId()); MpUserInfo mpUserInfo = mpUserInfoMapper.selectById(userId); return this.filling(mpUserInfo); } private ShiroUserDetail filling(MpUserInfo mpUserInfo) throws AuthenticationException { List mpShiroPermsInfos = mpShiroPermsInfoMapper.selectList(new QueryWrapper().eq("user_id", mpUserInfo.getId())); List perms = Lists.newArrayList(); if (PublicUtil.isNotEmpty(mpShiroPermsInfos)) { mpShiroPermsInfos.forEach(obj -> perms.add(obj.getPerms())); mpUserInfo.setPerms(perms); mpUserInfo.setMpShiroPermsInfoList(mpShiroPermsInfos); } log.warn("数据库查询成功 登入用户为 userId=[ {} ],用户名为:[ {} ]", mpUserInfo.getId().toString(), mpUserInfo.getUsername()); log.warn("shiro框架识别用户名成功 < ================= < ============== < "); return new ShiroUserDetail(mpUserInfo.getId().toString(), mpUserInfo.getUsername(), mpUserInfo.getPassword(), mpUserInfo.getPerms()); } } ### 1.引入pom依赖 org.crazycake shiro-redis 2.4.2.1-RELEASE ### 2.配置session获得方式 @Bean public SessionManager sessionManager() { //配置sessionId获取方式.这里用ajax请求头 MySessionManager mySessionManager = new MySessionManager(); //配置session获得方式 最终是SessionDAO类获取session会话.所以要重写 mySessionManager.setSessionDAO(sessionDAO()); return mySessionManager; } ### 3.配置sessionDAO @Bean SessionDAO sessionDAO() { log.error("使用redis缓存"); RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); log.info("设置redisSessionDAO"); return redisSessionDAO; } ### 4.配置redisManager @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setPassword(password); redisManager.setExpire(timeout);// 配置过期时间 log.info("配置redis连接设置##########" + host + ":::" + port); return redisManager; } ### 5.重写shiro未授权或未登入的跳转方式 原本shiro未授权或未登入是在ShiroFilterFactoryBean 设置 shiroFilterFactoryBean.setUnauthorizedUrl("33333333333/admin/page/noAuth"); shiroFilterFactoryBean.setLoginUrl("4444444444444444444/shiro/mp/nologinCode"); 但是这个方式是重定向方式,如果是前后端不分离也就是java控制页面控制器,并没有问题. 但是如果前后端分离之后,页面控制器权力在前端手中,我们是需要给予返回json中的code例如2000状态码判断是否登入或授权 而shiro默认跳转方式重定向,而前端get请求无法捕捉重定向方式,所以要重写以上2句代码对应的跳转方法 filter包下 => NoLoginFilter 未登入下的直接返回状态码 saveRequestAndRedirectToLogin => NoPermsFilter 未权限授权下的直接返回状态码 => NoRolesFilter 未权限授权下的直接返回状态码 未授权需要重写2个跳转,因为未授权之前要先判断是否有登入,登入之后再判断是否授权 @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { Subject subject = getSubject(request, response); if (subject.getPrincipal() == null) { log.error("该用户在访问受角色保护资源时候未登入"); //未登入 ShiroURLFunction.saveRequestAndRedirectToLogin(request, response); } else { HttpServletRequest httpServletRequest= (HttpServletRequest) request; log.error("用户:[ {} ] 没有访问该角色资源 [ {} ]",ShiroUtils.getUser().getUsername(),httpServletRequest.getRequestURI()); ShiroThreadLocalMap.put("noAuthError", new StringBuilder().append("用户:[ ") .append(ShiroUtils.getUser().getUsername()).append(" ]") .append("没有访问该角色资源 [ ").append(httpServletRequest.getRequestURI()).append(" ]")); ShiroURLFunction.saveRequestAndRedirectToNoAuth(request, response); } return false; } #### shiro-core https://gitee.com/lexapps/shiro-core.git #### shiro-frame https://gitee.com/lexapps/shiro-frame.git