shiro实战:接口权限控制,shiro解决数据平行越权安全漏洞

背景

昨天被网安大队扫描安全漏铜,提到有个致命的安全漏洞:数据接口平行越权,要求半个月内整改完毕。数据平行越权用大白话说就是:我先用超级管理员登录后台系统,然后我记住了菜单A对应的URL。我在用普通管理员登录,但是没有菜单A的权限,但是我可以直接通过菜单A的URL进行访问。
数据平行越权的漏洞最根本问题是,系统没有针对接口级别的权限控制。好多系统都是所有接口做个拦截器,只校验了是否登录,并没有把用户、角色权限和接口给关联起来。
下图为网安大队安全漏洞报告截图:
网安大队安全漏洞报告截图

以下讲下java web框架的解决思路。

解决方案

JAVA后台变更:

  1. pom.xml 引入shiro对应的jar包:
		<!-- 对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>2.4.2.1-RELEASE</version>
        </dependency>
  1. 需要权限控制的接口加入以下注解:
@RequiresPermissions("student:getStudent")
    @RequestMapping(value = "getStudent",method= RequestMethod.POST)
    public ModelAndView getStudent()
    {
    	return new ModelAndView("/student");
    }
  1. application.yml 配置shiro 和redis 信息:
spring:
  redis:
    shiro :
      host: 127.0.0.1
      port: 6379
      timeout : 0
      password:
  1. 统一异常处理类:MyExceptionHandler.java
@Slf4j
public class MyExceptionHandler implements HandlerExceptionResolver {

    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception ex) {
    	ex.printStackTrace();
        ModelAndView mv = new ModelAndView();
        FastJsonJsonView view = new FastJsonJsonView();
        Map<String, Object> attributes = new HashMap<String, Object>();
        if (ex instanceof UnauthenticatedException) {
            attributes.put("sucess", "false");
            attributes.put("info", "token错误");
        } else if (ex instanceof UnauthorizedException) {
            attributes.put("sucess", "false");
            attributes.put("info", "用户无权限");
        } else {
            attributes.put("sucess", "false");
            attributes.put("info", ex.getMessage());
            log.error("发生未处理的异常={}",ex.getMessage(),ex);
        }

        view.setAttributesMap(attributes);
        mv.setView(view);
        return mv;
    }
}

  1. 新建权限控制:MyShiroRealm.java
public class MyShiroRealm extends AuthorizingRealm {
    @Resource
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;

    /**
     * 获取授权信息(把数据库中shiroID加入到shiro中管理,然后如果ctronler层方法加上@RequiresPermissions注解才会调用这个方法和数组进行比对)
	 * 只有当需要检测用户权限的时候才会调用此方法
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
	{
		//获取username
		SysUser sysUser=(SysUser) SecurityUtils.getSubject().getSession().getAttribute("sysUserLogin");
		List<SysPrivilege> sysPrivilegeList=sysPrivilegeList=sysRoleService.getSysPrivilegeByRoid(sysUser.getRoleId());
		// 用户权限列表
		Set<String> permsSet = new HashSet<>();
		for (SysPrivilege perms : sysPrivilegeList)
		{
			if (StringUtils.isBlank(perms.getShiroID()))
			{
				continue;
			}
			permsSet.addAll(Arrays.asList(perms.getShiroID().trim().split(",")));
		}
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		info.addStringPermissions(permsSet);
		return info;
	}

    /**
     * 获取身份验证信息
     * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
//        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
        //获取用户的输入的账号.
        String username = (String) token.getPrincipal();
//        System.out.println(token.getCredentials());
        //通过username从数据库中查找 User对象,如果找到,没找到.
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        SysUser userInfo = sysUserService.getByUserCode(username);
//        System.out.println("----->>userInfo="+userInfo);
        if (userInfo == null) {
            return null;
        }
        if (Integer.parseInt(userInfo.getStates()) == 1) { //账户冻结
            throw new LockedAccountException();
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                token.getCredentials(), //用户名
                userInfo.getPassword(),//salt=username+salt
                getName()  //realm name
        );
        return authenticationInfo;
    }

}
  1. 新建shiro 管理类:MySessionManager.java
public class MySessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "Authorization";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public MySessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //适合于安卓,ios的或者前后端分离情况,直接在head头中传入sessionId的值(sessionId的键名为Authorization)
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //适用于浏览器访问,否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }

}
  1. 新建shiro 配置规则类:ShiroConfig.java
@Configuration
public class ShiroConfig {

    @Value("${spring.redis.shiro.host}")
    private String host;
    @Value("${spring.redis.shiro.port}")
    private int port;
    @Value("${spring.redis.shiro.timeout}")
    private int timeout;
    @Value("${spring.redis.shiro.password}")
    private String password;

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        //注意过滤器配置顺序 不能颠倒
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
        filterChainDefinitionMap.put("/loginout", "logout");
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/jqueryEasyui/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/theme/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/ajaxLogin", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/demoOrder/**r", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl("/index");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授权界面;
//        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     * )
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        return hashedCredentialsMatcher;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }


    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

    //自定义sessionManager
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }

    /**
     * 配置shiro redisManager
     * <p>
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setExpire(1800);// 配置缓存过期时间(登录是失效时间,0代表永不失效)
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * cacheManager 缓存 redis实现
     * <p>
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * <p>
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 注册全局异常处理
     * @return
     */
    @Bean(name = "exceptionHandler")
    public HandlerExceptionResolver handlerExceptionResolver() {
        return new MyExceptionHandler();
    }
}

前端变更:

  1. 前端需要根据自己的框架,作统一的底层判断然后跳到统一的权限异常页面。
  2. 后台界面配置也需要支持对shiroid的管理。如下效果:
    在这里插入图片描述
    在这里插入图片描述

源码地址:

  1. 获取完整源码地址: https://download.csdn.net/download/penggerhe/11670196
  2. 公众号关注,免费领取:
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT管理圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值