0%

一、前言

本文小编将基于 SpringBoot 集成 Shiro 实现动态uri权限,由前端vue在页面配置uri,Java后端动态刷新权限,不用重启项目,以及在页面分配给用户 角色按钮uri 权限后,后端动态分配权限,用户无需在页面重新登录才能获取最新权限,一切权限动态加载,灵活配置

基本环境
  1. spring-boot 2.1.7
  2. mybatis-plus 2.1.0
  3. mysql 5.7.24
  4. redis 5.0.5

温馨小提示:案例demo源码附文章末尾,有需要的小伙伴们可参考哦 ~

二、SpringBoot集成Shiro

1、引入相关maven依赖

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
<properties>
<shiro-spring.version>1.4.0</shiro-spring.version>
<shiro-redis.version>3.1.0</shiro-redis.version>
</properties>
<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>

<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
</dependency>
</dependencies>

2、自定义Realm

  1. doGetAuthenticationInfo:身份认证 (主要是在登录时的逻辑处理)
  2. doGetAuthorizationInfo:登陆认证成功后的处理 ex: 赋予角色和权限
    【 注:用户进行权限验证时 Shiro会去缓存中找,如果查不到数据,会执行doGetAuthorizationInfo这个方法去查权限,并放入缓存中 】 -> 因此我们在前端页面分配用户权限时 执行清除shiro缓存的方法即可实现动态分配用户权限
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
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private RoleMapper roleMapper;

@Override
public String getName() {
return "shiroRealm";
}


@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

User user = (User) principalCollection.getPrimaryPrincipal();
Integer userId =user.getId();

Set<String> rolesSet = new HashSet<>();
Set<String> permsSet = new HashSet<>();

List<Role> roleList = roleMapper.selectRoleByUserId( userId );
for (Role role:roleList) {
rolesSet.add( role.getCode() );
List<Menu> menuList = menuMapper.selectMenuByRoleId( role.getId() );
for (Menu menu :menuList) {
permsSet.add( menu.getResources() );
}
}

authorizationInfo.setStringPermissions(permsSet);
authorizationInfo.setRoles(rolesSet);
log.info("--------------- 赋予角色和权限成功! ---------------");
return authorizationInfo;
}


@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken tokenInfo = (UsernamePasswordToken)authenticationToken;

String username = tokenInfo.getUsername();

String password = String.valueOf( tokenInfo.getPassword() );



User user = userMapper.selectUserByUsername(username);

if (user == null) {

return null;
}

if ( !password.equals( user.getPwd() ) ){
throw new IncorrectCredentialsException("用户名或者密码错误");
}


if (user.getFlag()==null|| "0".equals(user.getFlag())){
throw new LockedAccountException();
}

SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());

ShiroUtils.deleteCache(username,true);


String token = ShiroUtils.getSession().getId().toString();
user.setToken( token );
userMapper.updateById(user);
return authenticationInfo;
}

}

3、Shiro配置类

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@Configuration
public class ShiroConfig {

private final String CACHE_KEY = "shiro:cache:";
private final String SESSION_KEY = "shiro:session:";

private final int EXPIRE = 1800;


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




@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}


@Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager, ShiroServiceImpl shiroConfig){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);


Map<String, Filter> filtersMap = new LinkedHashMap<>();

filtersMap.put( "zqPerms", new MyPermissionsAuthorizationFilter() );
filtersMap.put( "zqRoles", new MyRolesAuthorizationFilter() );
filtersMap.put( "token", new TokenCheckFilter() );
shiroFilterFactoryBean.setFilters(filtersMap);


shiroFilterFactoryBean.setLoginUrl("/api/auth/unLogin");



shiroFilterFactoryBean.setUnauthorizedUrl("/api/auth/unauth");

shiroFilterFactoryBean.setFilterChainDefinitionMap( shiroConfig.loadFilterChainDefinitionMap() );
return shiroFilterFactoryBean;
}


@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

securityManager.setSessionManager(sessionManager());

securityManager.setCacheManager(cacheManager());

securityManager.setRealm(shiroRealm());
return securityManager;
}


@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}


@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();

shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);

shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
return shaCredentialsMatcher;
}


@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout(timeout);

return redisManager;
}


@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix(CACHE_KEY);

redisCacheManager.setPrincipalIdFieldName("id");
return redisCacheManager;
}


@Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
return new ShiroSessionIdGenerator();
}


@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setExpire(EXPIRE);
return redisSessionDAO;
}


@Bean
public SessionManager sessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
shiroSessionManager.setSessionDAO(redisSessionDAO());
return shiroSessionManager;
}

}

三、shiro动态加载权限处理方法

  1. loadFilterChainDefinitionMap:初始化权限
    ex: 在上面Shiro配置类ShiroConfig中的Shiro基础配置shiroFilterFactory方法中我们就需要调用此方法将数据库中配置的所有uri权限全部加载进去,以及放行接口和配置权限过滤器等
    【注:过滤器配置顺序不能颠倒,多个过滤器用 , 分割】
    ex: filterChainDefinitionMap.put("/api/system/user/list", "authc,token,zqPerms[user1]")
  2. updatePermission:动态刷新加载数据库中的uri权限 -> 页面在新增uri路径到数据库中,也就是配置新的权限时就可以调用此方法实现动态加载uri权限
  3. updatePermissionByRoleId:shiro动态权限加载 -> 即分配指定用户权限时可调用此方法删除shiro缓存,重新执行doGetAuthorizationInfo方法授权角色和权限
1
2
3
4
5
6
7
8
9
10
11
12
public interface ShiroService {


Map<String, String> loadFilterChainDefinitionMap();


void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, Integer roleId, Boolean isRemoveSession);


void updatePermissionByRoleId(Integer roleId, Boolean isRemoveSession);

}
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@Slf4j
@Service
public class ShiroServiceImpl implements ShiroService {

@Autowired
private MenuMapper menuMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;

@Override
public Map<String, String> loadFilterChainDefinitionMap() {

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();


filterChainDefinitionMap.put("/swagger-ui.html","anon");
filterChainDefinitionMap.put("/swagger/**","anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**","anon");
filterChainDefinitionMap.put("/v2/**","anon");
filterChainDefinitionMap.put("/static/**", "anon");


filterChainDefinitionMap.put("/api/auth/login/**", "anon");

filterChainDefinitionMap.put("/api/auth/loginByQQ", "anon");
filterChainDefinitionMap.put("/api/auth/afterlogin.do", "anon");

filterChainDefinitionMap.put("/api/auth/logout", "anon");

filterChainDefinitionMap.put("/api/auth/unauth", "anon");

filterChainDefinitionMap.put("/api/auth/tokenExpired", "anon");

filterChainDefinitionMap.put("/api/auth/downline", "anon");



List<Menu> permissionList = menuMapper.selectList( null );
if ( !CollectionUtils.isEmpty( permissionList ) ) {
permissionList.forEach( e -> {
if ( StringUtils.isNotBlank( e.getUrl() ) ) {

List<Role> roleList = roleMapper.selectRoleByMenuId( e.getId() );
StringJoiner zqRoles = new StringJoiner(",", "zqRoles[", "]");
if ( !CollectionUtils.isEmpty( roleList ) ){
roleList.forEach( f -> {
zqRoles.add( f.getCode() );
});
}







filterChainDefinitionMap.put( "/api" + e.getUrl(),"authc,token,"+ zqRoles.toString() +",zqPerms[" + e.getResources() + "]" );

}
});
}

filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}

@Override
public void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, Integer roleId, Boolean isRemoveSession) {
synchronized (this) {
AbstractShiroFilter shiroFilter;
try {
shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
} catch (Exception e) {
throw new MyException("get ShiroFilter from shiroFilterFactoryBean error!");
}
PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();


manager.getFilterChains().clear();


shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

shiroFilterFactoryBean.setFilterChainDefinitionMap(loadFilterChainDefinitionMap());

Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
for (Map.Entry<String, String> entry : chains.entrySet()) {
manager.createChain(entry.getKey(), entry.getValue());
}
log.info("--------------- 动态生成url权限成功! ---------------");


if(roleId != null){
updatePermissionByRoleId(roleId,isRemoveSession);
}
}
}

@Override
public void updatePermissionByRoleId(Integer roleId, Boolean isRemoveSession) {

List<User> userList = userMapper.selectUserByRoleId(roleId);

if ( !CollectionUtils.isEmpty( userList ) ) {
for (User user : userList) {
ShiroUtils.deleteCache(user.getUsername(), isRemoveSession);
}
}
log.info("--------------- 动态修改用户权限成功! ---------------");
}

}

四、shiro中自定义角色、权限过滤器

1、自定义uri权限过滤器 zqPerms

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
@Slf4j
public class MyPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestUrl = httpRequest.getServletPath();
log.info("请求的url: " + requestUrl);


Subject subject = this.getSubject(request, response);
if (subject.getPrincipal() == null) {
this.saveRequestAndRedirectToLogin(request, response);
} else {

HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;


String header = req.getHeader("X-Requested-With");

if (header!=null && "XMLHttpRequest".equals(header)){
resp.setContentType("text/json,charset=UTF-8");
resp.getWriter().print("{\"success\":false,\"msg\":\"没有权限操作!\"}");
}else {
String unauthorizedUrl = this.getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(401);
}
}

}
return false;
}

}

2、自定义角色权限过滤器 zqRoles

shiro原生的角色过滤器RolesAuthorizationFilter 默认是必须同时满足roles[admin,guest]才有权限,而自定义的zqRoles 只满足其中一个即可访问
ex: zqRoles[admin,guest]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyRolesAuthorizationFilter extends AuthorizationFilter {

@Override
protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) throws Exception {
Subject subject = getSubject(req, resp);
String[] rolesArray = (String[]) mappedValue;

if (rolesArray == null || rolesArray.length == 0) {
return true;
}
for (int i = 0; i < rolesArray.length; i++) {

if (subject.hasRole(rolesArray[i])) {
return true;
}
}
return false;
}

}

3、自定义token过滤器 token -> 判断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
@Slf4j
public class TokenCheckFilter extends UserFilter {


private static final String TOKEN_EXPIRED_URL = "/api/auth/tokenExpired";


@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

String token = WebUtils.toHttp(request).getHeader(Constants.REQUEST_HEADER);
log.info("浏览器token:" + token );
User userInfo = ShiroUtils.getUserInfo();
String userToken = userInfo.getToken();

if ( !token.equals(userToken) ){
return false;
}
return true;
}


@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
User userInfo = ShiroUtils.getUserInfo();

WebUtils.issueRedirect(request, response, TOKEN_EXPIRED_URL);
return false;
}

}

五、项目中会用到的一些工具类、常量等

温馨小提示:这里只是部分,详情可参考文章末尾给出的案例demo源码

1、Shiro工具类

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
public class ShiroUtils {


private ShiroUtils(){ }

private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);


public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}


public static void logout() {
SecurityUtils.getSubject().logout();
}


public static User getUserInfo() {
return (User) SecurityUtils.getSubject().getPrincipal();
}


public static void deleteCache(String username, boolean isRemoveSession){

Session session = null;

Collection<Session> sessions = redisSessionDAO.getActiveSessions();
User sysUserEntity;
Object attribute = null;

for(Session sessionInfo : sessions){
attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
sysUserEntity = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (sysUserEntity == null) {
continue;
}
if (Objects.equals(sysUserEntity.getUsername(), username)) {
session=sessionInfo;

if (isRemoveSession) {
redisSessionDAO.delete(session);
}
}
}

if (session == null||attribute == null) {
return;
}

if (isRemoveSession) {
redisSessionDAO.delete(session);
}

DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
Authenticator authc = securityManager.getAuthenticator();
((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
}


private static Session getSessionByUsername(String username){

Collection<Session> sessions = redisSessionDAO.getActiveSessions();
User user;
Object attribute;

for(Session session : sessions){
attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
user = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (user == null) {
continue;
}
if (Objects.equals(user.getUsername(), username)) {
return session;
}
}
return null;
}

}

2、Redis常量类

1
2
3
4
public interface RedisConstant {

String REDIS_PREFIX_LOGIN = "code-generator_token_%s";
}

3、Spring上下文工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext context;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}

public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
}

六、案例demo源码

GitHub地址

https://github.com/zhengqingya/code-generator/tree/master/code-generator-api/src/main/java/com/zhengqing/modules/shiro

码云地址

https://gitee.com/zhengqingya/code-generator/blob/master/code-generator-api/src/main/java/com/zhengqing/modules/shiro