目录

  1. SpringBoot 和 Shiro 整合
  2. Shiro 中的过滤器
  3. 授权的实现
    1. 基于配置的授权
    2. 基于注解的授权
  4. Shiro 中的会话管理
    1. 应用场景分析
    2. Shiro 会话管理器
    3. 配置 Shiro 基于 redis 的会话管理
    4. 构建步骤
    5. 配置 Shiro 基于 redis 的会话管理
  5. 附录

SpringBoot 和 Shiro 整合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.apache.Shiro</groupId>
<artifactId>Shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.Shiro</groupId>
<artifactId>Shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<!--引入Shiro整合Springboot依赖-->
<dependency>
<groupId>org.apache.Shiro</groupId>
<artifactId>Shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>

创建配置类,用来整合 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
@Configuration
public class ShiroConfiguration {
//配置自定义的Realm
@Bean
public CustomRealm getRealm() {
return new CustomRealm();
}
//配置安全管理器
@Bean
public SecurityManager securityManager(CustomRealm realm) {
//使用默认的安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
//将自定义的realm交给安全管理器统一调度管理
securityManager.setRealm(realm);
return securityManager;
}

//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
//1.创建Shiro过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器
filterFactory.setSecurityManager(securityManager);
//3.通用配置(配置登录页面,登录成功页面,验证未成功页面)
filterFactory.setLoginUrl("/autherror?code=1");
//设置登录页面
filterFactory.setUnauthorizedUrl("/autherror?code=2");
//授权失败跳转页面
//4.配置过滤器集合
Map<String,String> filterMap = new LinkedHashMap<String,String>();
// 配置不会被拦截的链接 顺序判断
filterMap.put("/user/home", "anon");
filterMap.put("/user/**", "authc");
//5.设置过滤器
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}
//配置Shiro注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}

配置自定义 realm

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
//自定义realm
public class CustomRealm extends AuthorizingRealm {
@Override
public void setName(String name) {
super.setName("customRealm");
}
@Autowired
private UserService userService;
/* 构造授权方法 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1.获取认证的用户数据
User user = (User)principalCollection.getPrimaryPrincipal();
//2.构造认证数据
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<Role> roles = user.getRoles();
for (Role role : roles) {
//添加角色信息
info.addRole(role.getName());
for (Permission permission:role.getPermissions()) {
//添加权限信息
info.addStringPermission(permission.getCode());
}
}
return info;
}
/* 认证方法 */
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取登录的upToken
UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
//2.获取输入的用户名密码
String username = upToken.getUsername();
String password = new String(upToken.getPassword());
//3.数据库查询用户
User user = userService.findByName(username);
//4.用户存在并且密码匹配存储用户数据
if(user != null && user.getPassword().equals(password)) {
return new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
}else {
//返回null会抛出异常,表明用户不存在或密码不匹配
return null;
}
}
}

Shiro 中的过滤器

在上一小节 Shiro 配置中有这样一条语句:filterMap.put("/user/home", "anon");其中向 map 中放入的第一个参数表示:需要拦截的路径;第二个参数则是 Shiro 内置的各种过滤器。整条语句的含义就是,使用anon过滤器,对所有请求/user/home的请求进行过滤

下表列出了目前 Shiro 中拥有的常见的过滤器:

Filter含义
anon无参,开放权限,可以理解为匿名用户或游客
authc无参,需要认证
logout无参,注销,执行后会直接跳转到 ShiroFilterFactoryBean.setLoginUrl(); 设置的 url
authcBasic无参,表示 httpBasic 认证
user无参,表示必须存在用户,当登入操作时不做检查
ssl无参,表示安全的 URL 请求,协议为 https
Perm[user]参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算通过
roles[admin]参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles[“admin,user”], 当有多个参数时必须每个参数都通过才算通过
rest[user]根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
port[8081]当请求的 URL 端口不是 8081 时,跳转到当前访问主机 HOST 的 8081 端口

🎶anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤 器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器 (例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 ShiroFilterFactoryBean.setLoginUrl(); 设置的 url )

授权的实现

授权:即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,细节在之前的博客已经进行了阐述,这里主要关注于如何实现 Shiro 中的授权操作。

Shiro 中存在两种方式进行授权:基于过滤器配置的授权方式以及基于注解的授权方式

基于配置的授权

在 Shiro 中可以使用过滤器的方式配置目标地址的请求权限

1
2
3
4
5
6
7
8
9
//配置请求连接过滤器配置
//匿名访问(所有人员可以使用)
filterMap.put("/user/home", "anon");
//具有指定权限访问
filterMap.put("/user/find", "perms[user-find]");
//认证之后访问(登录之后可以访问)
filterMap.put("/user/**", "authc");
//具有指定角色可以访问
filterMap.put("/user/**", "roles[系统管理员]");

基于配置的方式进行授权,一旦操作用户不具备操作权限,目标地址不会被执行。会跳转到指定的 url 连接地址。所以需要在连接地址中更加友好的处理未授权的信息提示

基于注解的授权

基于注解的授权有两种方式:@RequiresPermissions@RequiresRoles

1
2
3
4
5
//配置到方法上,表明执行此方法必须具有指定的权限
@RequiresPermissions(value = "user-find")
public String find() {
return "查询用户成功";
}
1
2
3
4
5
//配置到方法上,表明执行此方法必须具有指定的角色
@RequiresRoles(value = "系统管理员")
public String find() {
return "查询用户成功";
}

基于注解的配置方式进行授权,一旦操作用户不具备操作权限,目标方法不会被执行,而且会抛出 AuthorizationException 异常。所以需要做好统一异常处理完成未授权处理

Shiro 中的会话管理

❓ 什么是 Shiro 的会话管理

在 Shiro 里所有的用户的会话信息都会由 Shiro 来进行控制,Shiro 提供的会话可以用于 JavaSE/JavaEE 环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过 Shiro 的会话管理器(SessionManager)进行统一的会话管理,对所有 Subject 中的 session 进行创建、维护、删除、失效、验证等工作。SessionManager 是顶层组件,它自身又由 SecurityManager 管理。

简而言之,通过 shiro 的会话管理能够简单的进行用户的认证和授权管理。

Shiro 提供了三个默认实现:

  1. DefaultSessionManager:用于 JavaSE 环境
  2. ServletContainerSessionManager:用于 Web 环境,直接使用 servlet 容器的会话
  3. DefaultWebSessionManager:用于 web 环境,自己维护会话(自己维护着会话,直接废弃了 Servlet 容器的会话管理)

在 web 程序中,通过 Shiro 的 Subject.login() 方法登录成功后,用户的认证信息实际上是保存在 HttpSession 中,可以通过如下代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//登录成功后,打印所有session内容
@RequestMapping(value="/show")
public String show(HttpSession session) {
// 获取session中所有的键值
Enumeration<?> enumeration = session.getAttributeNames();
// 遍历enumeration中的
while (enumeration.hasMoreElements()) {
// 获取session键值
String name = enumeration.nextElement().toString();
// 根据键值取session中的值
Object value = session.getAttribute(name);
// 打印结果
System.out.println("<B>" + name + "</B>=" + value + "<br>/n");
}
return "查看session成功";
}

应用场景分析

在分布式系统或者微服务架构下,都是通过统一的认证中心进行用户认证。如果使用默认会话管理,用户信息只会保存到一台服务器上。那么其他服务就需要进行会话的同步,通过 Shiro 进行统一会话管理能够解决同步问题,具体的流程如下图:

用户登陆之后,通过认证中心进行用户权限角色等一系列的验证,如果通过 Shiro 会自动将内容放入到 session 中,只需要将此时将这个 session 对应的内容放入到 redis 缓存中,就可以保证:下次用户访问其他服务时,不需要再一次在本地执行认证操作,只需要根据是否有 session 去缓存中查询是否有权限即可,将认证的过程从每一个服务中脱离了出来

Shiro 会话管理器

Shiro 的会话管理比较灵活,可以应用到微服务架构或分布式系统中

配置 Shiro 基于 redis 的会话管理

Shiro 内部有自己的缓存,如果想要使用 redis 外部缓存,则需要通过 Shiro-redis 包提供的 RedisManager 统一对 redis 操作,随后再配置 SessionDao,使用 Shiro-redis 包中基于 redis 的 sessionDao 实现

构建步骤

使用开源组件 Shiro-Redis 可以方便的构建 Shiro 与 redis 的整合工程,引入第三方依赖:

1
2
3
4
5
 <dependency>
<groupId>org.crazycake</groupId>
<artifactId>Shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>

在 springboot 配置文件中添加 redis 配置

1
2
3
redis:
host: 127.0.0.1
port: 6379

自定义 Shiro 会话管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CustomSessionManager extends DefaultWebSessionManager {

/**下述方法制定 sessionId 的获取方式**/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader("Authorization");
if (StringUtils.isEmpty(id)) {
return super.getSessionId(request, response);
} else {
id = id.replaceAll("Bearer ", "");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}

头信息中具有 sessionid 格式为:Authorization: sessionid

配置 Shiro 基于 redis 的会话管理

在 Shiro 的配置类中配置 Shiro 的 RedisManager,通过 Shiro-redis 包提供的 RedisManager 统一对 redis 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class ShiroConfiguration {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;

//配置 Shiro redisManager
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(this.host + ":" + port);
return redisManager;
}

}

Shiro 内部有自己的本地缓存机制,为了更加统一方便管理,全部替换为 redis 的实现方式

1
2
3
4
5
6
7
//配置Shiro的缓存管理器
//使用redis实现
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}

配置 SessionDao,使用 Shiro-redis 实现的 sessionDao

1
2
3
4
5
6
7
8
9
/**
* RedisSessionDAO Shiro sessionDao层的实现
* 使用的是Shiro-redis开源插件
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}

配置会话管理器,指定 sessionDao 的依赖关系

1
2
3
4
5
6
7
8
/**
* 3.会话管理器
*/
public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}

统一交给 SecurityManager 管理

1
2
3
4
5
6
7
8
9
10
11
12
13
//配置安全管理器
@Bean
public SecurityManager securityManager(CustomRealm realm) {
//使用默认的安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
//将自定义的realm交给安全管理器统一调度管理
securityManager.setRealm(realm);
return securityManager;
}
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
@Configuration
public class ShiroConfiguration {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;

@Bean
public CustomRealm getRealm() {
return new CustomRealm();
}

@Bean
public SecurityManager getSecurityManager(CustomRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}

public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}

public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}

public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}

public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(this.host + ":" + port);
return redisManager;
}

@Bean
public ShiroFilterFactoryBean ShiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean ShiroFilterFactoryBean = new ShiroFilterFactoryBean();
ShiroFilterFactoryBean.setSecurityManager(securityManager);
ShiroFilterFactoryBean.setLoginUrl("/authError?code=1");
ShiroFilterFactoryBean.setUnauthorizedUrl("/authError?code=2");
Map<String, String> filterMap = new LinkedHashMap<>(9);
filterMap.put("/user/**", "authc");
ShiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return ShiroFilterFactoryBean;
}

/** 配置Shiro注解支持 */
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}

附录

Shiro 学习笔记_03:整合 SpringBoot 项目实战