前言
最近公司我负责的一个服务(spring-boot spring-could)需要在接口级别做权限控制,本来想着是在controller层加上注解,再通过切面(aop)来实现。后来发现网上有现成的框架spring shiro 以及spring security。某位大佬曾经说过:不要重复造轮子。本着这个原则去了解了这两个框架。
选型
简单看了一下两个框架最后选择了spring shiro。这里说一下原因spring security接口设计有点问题,对用户的入侵有点太强了,扩展性不够。简单举个列子 使用security需要实现下面的接口。
这个接口只有一个方法,用户用户名获得用户相关的信息,及其权限信息。
我的那个微服务,需要通过用户及用户当前选择的数据源来获得权限信息(每个用户的每个数据源都具有不同的权限)。
这里就明显不符合要求。所以最后选择了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来包装一下。
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);
}
}
需要用到的时候转回来就好了