目录
- SpringBoot 和 Shiro 整合
- Shiro 中的过滤器
- 授权的实现
- 基于配置的授权
- 基于注解的授权
- Shiro 中的会话管理
- 应用场景分析
- Shiro 会话管理器
- 配置 Shiro 基于 redis 的会话管理
- 构建步骤
- 配置 Shiro 基于 redis 的会话管理
- 附录
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>
<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 {
@Bean public CustomRealm getRealm() { return new CustomRealm(); }
@Bean public SecurityManager securityManager(CustomRealm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm); securityManager.setRealm(realm); return securityManager; }
@Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean(); filterFactory.setSecurityManager(securityManager); filterFactory.setLoginUrl("/autherror?code=1"); filterFactory.setUnauthorizedUrl("/autherror?code=2"); Map<String,String> filterMap = new LinkedHashMap<String,String>(); filterMap.put("/user/home", "anon"); filterMap.put("/user/**", "authc"); filterFactory.setFilterChainDefinitionMap(filterMap); return filterFactory; }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
|
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
| public class CustomRealm extends AuthorizingRealm { @Override public void setName(String name) { super.setName("customRealm"); } @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user = (User)principalCollection.getPrimaryPrincipal(); 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 { UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken; String username = upToken.getUsername(); String password = new String(upToken.getPassword()); User user = userService.findByName(username); if(user != null && user.getPassword().equals(password)) { return new SimpleAuthenticationInfo(user,user.getPassword(),this.getName()); }else { 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 提供了三个默认实现:
- DefaultSessionManager:用于 JavaSE 环境
- ServletContainerSessionManager:用于 Web 环境,直接使用 servlet 容器的会话
- DefaultWebSessionManager:用于 web 环境,自己维护会话(自己维护着会话,直接废弃了 Servlet 容器的会话管理)
在 web 程序中,通过 Shiro 的 Subject.login()
方法登录成功后,用户的认证信息实际上是保存在 HttpSession
中,可以通过如下代码验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @RequestMapping(value="/show") public String show(HttpSession session) { Enumeration<?> enumeration = session.getAttributeNames(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement().toString(); 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 {
@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;
public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(this.host + ":" + port); return redisManager; }
}
|
Shiro 内部有自己的本地缓存机制,为了更加统一方便管理,全部替换为 redis 的实现方式
1 2 3 4 5 6 7
|
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
|
public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; }
|
配置会话管理器,指定 sessionDao 的依赖关系
1 2 3 4 5 6 7 8
|
public DefaultWebSessionManager sessionManager() { CustomSessionManager sessionManager = new CustomSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Bean public SecurityManager securityManager(CustomRealm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm); securityManager.setSessionManager(sessionManager()); securityManager.setCacheManager(cacheManager()); 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; }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
|
附录
Shiro 学习笔记_03:整合 SpringBoot 项目实战