使用shiro来管理权限

前言

最近公司我负责的一个服务(spring-boot spring-could)需要在接口级别做权限控制,本来想着是在controller层加上注解,再通过切面(aop)来实现。后来发现网上有现成的框架spring shiro 以及spring security。某位大佬曾经说过:不要重复造轮子。本着这个原则去了解了这两个框架。

选型

简单看了一下两个框架最后选择了spring shiro。这里说一下原因spring security接口设计有点问题,对用户的入侵有点太强了,扩展性不够。简单举个列子 使用security需要实现下面的接口。

IMG_2811

这个接口只有一个方法,用户用户名获得用户相关的信息,及其权限信息。

我的那个微服务,需要通过用户及用户当前选择的数据源来获得权限信息(每个用户的每个数据源都具有不同的权限)。

这里就明显不符合要求。所以最后选择了spring shiro

整合过程

依赖

     <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.5.0-SNAPSHOT</version>
        </dependency>

配置

@Configuration
public class ShiroConfig {
    private static String[] withOutAuthUrl = new String[]{
            "/auth/**",
            "/error",
            "/actuator/*",
            "/",
            "/**/*swagger*/**"
    };

    @Bean
    public SessionsSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
      	//定义AuthorizingRealm
        securityManager.setRealm(new WebRealm());
      	//设置缓存管理器这里使用内存
        securityManager.setCacheManager(new MemoryConstrainedCacheManager());
				//关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorAuthorizingRealmageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filterMap = Maps.newHashMap();
        shiroFilterFactoryBean.setFilters(filterMap);
      	//添加自定义过滤器
        filterMap.put("authFilter", new ShiroAuthFilter());
		//定义过滤规则 注意使用 有序的map 否则过滤器 规则 是混乱的
        Map<String, String> filterRuleMap = Maps.newLinkedHashMap();
      	//通过过滤器 AnonymousFilter 的url
        for (String url : withOutAuthUrl) {
            filterRuleMap.put(url, "anon");
        }
      	//其他都通过 ShiroAuthFilter
        filterRuleMap.put("/**", "authFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }
    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
}

自定义token 这个token是包含自己需要用来做权限的所有信息的实体,在我这主要包含,当前用户及其选择的数据源的信息

/**
 * @author jianganwei
 * @date 2019/9/29
 */
public class AuthToken implements AuthenticationToken {
    private AuthModel authModel;

    public AuthToken(AuthModel authModel) {
        this.authModel = authModel;
    }
    @Override
    public Object getPrincipal() {
        return authModel;
    }

    @Override
    public Object getCredentials() {
        return authModel;
    }
}

定义AuthorizingRealm

@Component
@Slf4j
public class WebRealm extends AuthorizingRealm {

		//获得权限信息接口,这个接口的结果会缓存,不会每次都调用
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        AuthModel authModel = (AuthModel)principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
      //这里从数据获得权限信息 一下为测试代码
        if(authModel.getDataSourceEntity().getGraphBean().equals("ick_graph")){
            authorizationInfo.addStringPermission("schema:read");
        }else {
            authorizationInfo.addStringPermission("dataSource:get");
        }

        log.debug("授权:{}", authModel);
        return authorizationInfo;
    }

  	/**
  	*添加对自定义的token的支持
  	**/
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof AuthToken;
    }

		//认证接口
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.debug("认证");
        return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), authenticationToken.getCredentials(), WebRealm.class.getTypeName());
    }
}

自定义过滤器

@Component
@Slf4j
public class ShiroAuthFilter extends AccessControlFilter {
    private static String[] withOutDataSourceUrl = new String[]{
            "/user/dataSource",
            "/dataSource/all"
    };
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        log.info("onAccessDenied");
        return false;
    }


    /**
     * 表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
     * (感觉这里应该是对白名单(不需要登录的接口)放行的)
     * 如果isAccessAllowed返回true则onAccessDenied方法不会继续执行
     * 这里可以用来判断一些不被通过的链接(个人备注)
     * * 表示是否允许访问 ,如果允许访问返回true,否则false;
     *
     * @param request
     * @param response
     * @param mappedValue 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
     * @return
     * @throws Exception
     */

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        String url = getPathWithinApplication(request);
        log.debug("当前用户正在访问的 url => {} ", url);
        log.debug("subject.isPermitted(url);{}", subject.isPermitted(url));
        AuthService authService = SpringUtils.getBean(AuthService.class);
       //FeignClient 远程调用 用户中心微服务获得用户信息
        AuthModel authModel = authService.getUserInfoFromAuth3Client((HttpServletRequest) request);
        if (null == authModel) {
          //未登录 
            response401(response);
            return false;
        }
     		//从cookie中获得数据源信息
        DataSourceEntity dataSourceEntity = authService.getDataSourceFromCookie((HttpServletRequest) request);
        authModel.setDataSourceEntity(dataSourceEntity);
        if (Stream.of(withOutDataSourceUrl).anyMatch(x ->
                antPathMatcher.match(x, url))) {
            log.debug("接口:{} 不需要选择数据源", url);
            getSubject(request, response).login(new AuthToken(authModel));
            return true;
        }
        if (null == dataSourceEntity) {
            response402(response);
            return false;
        }
      	//这个方法最终会调用WebRealm#doGetAuthenticationInfo 				//表示通过认证
        getSubject(request, response).login(new AuthToken(authModel));
        return true;
    }


    private void response401(ServletResponse response) {
        try {
            response.getWriter().write(JSON.toJSONString(new ResultData("401", "not choose dataSource", "")));
            response.getWriter().flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void response402(ServletResponse response) {
        try {
            response.getWriter().write(JSON.toJSONString(new ResultData("402", "not choose dataSource", "")));
            response.getWriter().flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

问题

在修改了权限后需要清空缓存,当我是用下面的方法清除缓存的时候发现清除不掉

sessionsSecurityManager.getCacheManager().getCache(WebRealm.class.getName() + ".authorizationCache").remove(authModel);

跟它的源码发现内存的缓存使用Map来做的,我传入的AuthModel 是不同的对象,需要用SimplePrincipalCollection来包装一下。
IMG_2821

sessionsSecurityManager.getCacheManager().getCache(WebRealm.class.getName() + ".authorizationCache").remove(new SimplePrincipalCollection(authModel, WebRealm.class.getTypeName()));

注意复写AuthModel的hashcode及equels方法

懒的话可以将对象转为jsonString

public class AuthToken implements AuthenticationToken {
    private AuthModel authModel;

    public AuthToken(AuthModel authModel) {
        this.authModel = authModel;
    }
    @Override
    public Object getPrincipal() {
        return JSON.toJSONString(authModel);
    }

    @Override
    public Object getCredentials() {
        return JSON.toJSONString(authModel);
    }
}

需要用到的时候转回来就好了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值