使用shiro来管理权限

前言

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

选型

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

IMG_2811

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

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

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

整合过程

依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.0-SNAPSHOT</version>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@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<String, String> filterRuleMap = Maps.newHashMap();
//不需要通过过滤器的url
for (String url : withOutAuthUrl) {
filterRuleMap.put(url, "anon");
}
//定义过滤规则
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是包含自己需要用来做权限的所有信息的实体,在我这主要包含,当前用户及其选择的数据源的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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());
}
}

自定义过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@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();
}
}
}

问题

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

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

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

IMG_2821

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

注意复写AuthModel的hashcode及equels方法

懒的话可以将对象转为jsonString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}

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