【AgileTC】_Shiro权限管理
1.在pom.xml文件中导入shiro相关依赖
1
2
3
4
5<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>2.编写shiro配置文件(xml文件或者@Configuration注解配置类)
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
public class ShiroConfig {
/**
* 配置web容器-请求过滤器
* /api为拦截路径前缀
* 拦截相关的内容请看 {@code AuthFilter.class}
*/
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("header", new AuthFilter());
Map<String, String> ruleMap = new LinkedHashMap<>();
ruleMap.put("/api/**", "header");
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setUnauthorizedUrl("/403");
bean.setFilters(filterMap);
bean.setFilterChainDefinitionMap(ruleMap);
return bean;
}
/**
* {@code RequiresPermissions.class}
* {@code RequiresRoles.class}
* 权限注解的解析+装饰器,装配到容器中
*/
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置安全管理器,同时将Realm塞入进来
*/
(name = "securityManager")
public DefaultWebSecurityManager securityManager(Realm realm, CacheManager cacheManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(realm);
manager.setCacheManager(cacheManager);
return manager;
}
/**
* 用于支持shiro的注解生效
* @see AuthorizationAttributeSourceAdvisor
* 不注册这个会导致request不生效,走不到controller
*/
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}3.创建一个继承AuthorizingRealm的Realm类,重写设置缓存方法、认证方法和授权方法:
- 设置缓存方法:开启缓存并为缓存命名;
- 认证方法:从数据库中根据token查询用户信息,生成用户认证信息;
- 授权方法:从数据库中根据用户信息查询用户关联的角色和权限,生成用户授权信息;
- 查看shiro框架源码,可知上述三个方法之间的工作关系:shiro过滤器拦截到相应请求时,先根据token从认证缓存中查找用户认证信息,没找到则执行此处认证方法,并将用户认证信息放入缓存中;然后根据用户信息从授权缓存中查找用户授权信息,没找到则执行此处授权方法,并将用户授权信息放入缓存中。
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
90
91
92
93
94(value = "realm")
public class AgileRealm extends AuthorizingRealm implements Serializable {
private static final long serialVersionUID = -2741710248922440453L;
private static final Logger LOGGER = LoggerFactory.getLogger(AgileRealm.class);
private static final String REALM_NAME = "AGILE-REALM";
public void setCacheManager(@Autowired CacheManager cacheManager) {
super.setCacheManager(cacheManager);
//实际上此处两个缓存不命名,shiro也会为其自动命名;我们为其命名的原因主要是后续需要获取到这两个缓存,在数据更新是清除对应缓存,之所以要这么做,是因为shiro并没有提供缓存更新接口。
//认证缓存开启
super.setAuthenticationCachingEnabled(true);
//为认证缓存命名
super.setAuthenticationCacheName("AGILE-REALM-AuthenticationCache");
//授权缓存开启
super.setAuthorizationCachingEnabled(true);
//为授权缓存命名
super.setAuthorizationCacheName("AGILE-REALM-AuthorizationCache");
}
private UserMapper userMapper;
public boolean supports(AuthenticationToken token) {
return token instanceof AgileToken;
}
/**
* 认证操作
*
* @param token 用户登录后的身份信息
* @return 认证后的基础信息
*/
(rollbackFor = Exception.class)
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
LOGGER.info("[Login]Username:{}, timestamp:{}", token.toString(), LocalDateTime.now().format(DateTimeFormatter.ISO_DATE));
AgileToken principal = (AgileToken) token.getPrincipal();
// 从数据库查信息,导出用户信息
User user = userMapper.getUser(principal.getUsername(), principal.getChannel(), principal.getLineId());
// 角色相关的校验
if (user == null) {
//如果数据库中没有这个用户,则报错。
throw new CaseServerException("该用户不存在",StatusCode.AUTH_UNKNOWN);
} else if (AuthConstant.BLOCKED.equals(user.getIsBlock())) {
throw new CaseServerException(StatusCode.AUTH_BLOCKED);
}
// 给用户设置默认的信息,由于我们不需要管理用户登录态,所以只需要塞入即可
// 就是表示该用户认证通过,返回认证后取得认证令牌的用户。
return new SimpleAuthenticationInfo(principal, principal, REALM_NAME);
}
/**
* 授权操作
*
* @param principalCollection 认证后的信息集合
* @return 授权信息
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
AgileToken principal = (AgileToken) principalCollection.getPrimaryPrincipal();
// 从数据库查询信息,导出权限相关的信息
// 就是从数据库中通过用户、角色、权限三个表查出该用户对应的权限信息
User user = userMapper.getUserWithPerms(principal.getUsername(), principal.getChannel(), principal.getLineId());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (user == null) {
throw new CaseServerException("您没有权限进行此操作", StatusCode.AUTH_ERROR);
} else if (CollectionUtils.isEmpty(user.getPermissions())) {
throw new CaseServerException("你没有权限进行此操作", StatusCode.AUTH_ERROR);
} else {
List<String> perms = user.getPermissions().stream().map(Permission::getResource).collect(Collectors.toList());
//为该用户进行权限授权,将perms列表的权限授权给该用户;
//因为本项目没有使用角色校验,所以不用为用户进行角色授权。
info.addStringPermissions(perms);
}
return info;
}
}4.创建一个实现AuthenticationToken接口的token类,用于保存用户信息,具体包括哪些信息可以由我们自己设定。
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
public class AgileToken implements AuthenticationToken {
private static final long serialVersionUID = 5288207035894094853L;
private String username;
private Integer channel;
private Long lineId;
public AgileToken(String username, String channel, String lineId) {
this.username = username;
this.channel = Integer.parseInt(channel);
this.lineId = Long.parseLong(lineId);
}
public Object getPrincipal() {
return this;
}
public Object getCredentials() {
return this;
}
}5.创建一个实现Cache<K, V>的缓存类,内部版本是使用Redis客户端Jedis来作为缓存实现结构的,但是开源版本设计成单机项目,此处使用HashMap来作为缓存的实现结构。
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
74public class AgileCache<K, V> implements Cache<K, V> {
private static final Logger LOGGER = LoggerFactory.getLogger(AgileCache.class);
private HashMap cacheMap;
public AgileCache(HashMap cacheMap) {
this.cacheMap = cacheMap;
}
public V get(K k) throws CacheException {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("[Shiro Get]{}", k);
}
if (k == null) {
return null;
}
Object res = cacheMap.get(k);
if (res == null) {
return null;
}
return (V) res;
}
public V put(K k, V v) throws CacheException {
if (k == null || v == null) {
LOGGER.error("[Shiro Put]key={}, value={}", k, v);
throw new CaseServerException("不能塞入空值", StatusCode.SERVER_BUSY_ERROR);
}
if (LOGGER.isInfoEnabled()) {
LOGGER.info("[Shiro Put]key={}, value={}", k, v);
}
cacheMap.put(k, v);
return v;
}
public V remove(K k) throws CacheException {
if (k == null) {
return null;
}
V res = get(k);
cacheMap.remove(k);
return res;
}
public void clear() throws CacheException {
// should do nothing
}
public int size() {
// should do nothing
return 0;
}
public Set<K> keys() {
// should do nothing
return new HashSet<>();
}
public Collection<V> values() {
// should do nothing
return new ArrayList<>();
}
}6.创建一个继承AbstractCacheManager的缓存控制器,shiro配置文件会将其配置到spring容器中:
1
2
3
4
5
6
7
8
9
10@Component
public class AgileCacheManager extends AbstractCacheManager {
private HashMap cacheMap = new HashMap();
@Override
protected Cache createCache(String s) throws CacheException {
return new AgileCache(cacheMap);
}
}7.创建一个继承BasicHttpAuthenticationFilter的shiro过滤器,该过滤器主要进行一些前处理,此处用于将请求头中的用户名取出创建一个token,用户shiro后续认证和授权;查看shiro源码可知该过滤器中isAccessAllowed()方法被一个prehandle方法调用,executeChain()被一个doFilter方法调用,所有前者先执行,后者后执行:
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
62public class AuthFilter extends BasicHttpAuthenticationFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter.class);
private static final String LOGIN_SIGN = "username";
/**
* 判断请求头中有没有用户名请求头,有则说明该请求需要进行登录校验。
*/
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
return StringUtils.hasText(req.getHeader(LOGIN_SIGN));
}
/**
* 登录校验
*/
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String username = req.getHeader(LOGIN_SIGN);
// 直接使用用户名来做token
AgileToken token = new AgileToken(username, AuthConstant.DEFAULTCHANNEL,AuthConstant.DEFAULTLINEID);
// 获取用户实例
Subject subject = getSubject(request, response);
// 登录
subject.login(token);
return true;
}
/**
* 校验用户是否正确登录
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
LOGGER.error("登录出错,错误原因={}", e.getLocalizedMessage());
e.printStackTrace();
throw new CaseServerException(StatusCode.AUTH_UNKNOWN);
}
}
return true;
}
/**
* 先对请求进行数据封装,然后交给后续过滤器。
*/
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain chain) throws Exception {
ShiroHttpServletRequest req = (ShiroHttpServletRequest)request;
HttpServletResponse resp = (HttpServletResponse) response;
chain.doFilter(req, resp);
}
}8.创建更新缓存中数据的ShiroHelper类,在用户角色或权限被修改时,删除缓存中的用户信息键值对:
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
37public class ShiroRedisHelper {
private static AgileCacheManager cacheManager = SpringUtils.getBean(AgileCacheManager.class);
private static Logger LOGGER = LoggerFactory.getLogger(ShiroRedisHelper.class);
/**
* 清除指定用户的认证缓存和授权缓存
*/
public static void delete(AgileToken token) {
PrincipalCollection principals = new SimpleAuthenticationInfo(token, token, "AGILE-REALM").getPrincipals();
deleteAuthorizationInfo(principals);
deleteAuthenticationInfo(token);
}
/**
* 清除指定用户的授权缓存
*/
public static void deleteAuthorizationInfo(PrincipalCollection principals) {
Cache<Object, AuthorizationInfo> cache = cacheManager.getCache("AGILE-REALM-AuthorizationCache");
LOGGER.info("[Authorization_redis_delete]key:{}, timestamp:{}", principals.toString(), LocalDateTime.now().format(DateTimeFormatter.ISO_DATE));
cache.remove(principals);
}
/**
* 清除指定用户的认证缓存
*/
public static void deleteAuthenticationInfo(AgileToken principal) {
Cache<Object, AuthenticationInfo> cache = cacheManager.getCache("AGILE-REALM-AuthenticationCache");
LOGGER.info("[Authentication_redis_delete]key:{}, timestamp:{}", principal.toString(), LocalDateTime.now().format(DateTimeFormatter.ISO_DATE));
cache.remove(principal);
}
}9.在相关接口上添加权限控制注解,如@RequiresPermissions:
1
2
3
4
5(value = "/getCaseInfo")
("case:detail")
public Response<?> getCaseGeneralInfo( (message = "用例id为空") Long id) {
return Response.success(caseService.getCaseGeneralInfo(id));
}