Spring 整合 Shiro
本文最后更新于:2024年9月8日 晚上
Spring 整合 Shiro
- Apache Shiro 是一个功能强大,灵活的,开源的安全框架,它可以干净利落地处理身份验证,授权,企业会话管理和加密。
- Apache Shiro 的首要目标是易于使用和理解,安全通常很复杂,甚至让人感到很痛苦,但是 Shiro 却不是这样子的,一个好的安全框架应该屏蔽复杂性,向外暴露简单,直观的 API,来简化开发人员实现应用程序安全所花费的时间和精力。
- Shiro 能做什么呢?
- 验证用户身份。
- 用户访问权限控制,比如:判断用户是否分配了一定的安全角色,判断用户是否被授予完成某个操作的权限。
- 在非 Web 或 EJB 容器的环境下可以任意使用 Session API
- 可以响应认证,访问控制,或者 Session 生命周期中发生的事件。
- 可将一个或以上用户安全数据源数据组合成一个复合的用户"view”(视图)
- 支持单点登录(SSO)功能。
- 支持提供"Remember Me”服务,获取用户关联信息而无需登录。
- 等等——都集成到一个有凝聚力的易于使用的 API
- Shiro 致力在所有应用环境下实现上述功能,小到命令行应用程序,大到企业应用中,而且不需要借助第三方框架,容器,应用服务器等,当然 Shiro 的目的是尽量的融入到这样的应用环境中去,但也可以在它们之外的任何环境下开箱即用。
Shiro 特性
- Apache Shiro 是一个全面的,蕴含丰富功能的安全框架,下图为描述 Shiro 功能的框架图:

- Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石,那么就让我们来看看它们吧:
- Authentication(认证):用户身份识别,通常被称为用户"登录”
- Authorization(授权):访问控制,比如某个用户是否具有某个操作的使用权限。
- Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
- Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
- 还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点,特别是对以下的功能支持:
- Web支持:Shiro 提供的 Web 支持 api,可以很轻松的保护 Web 应用程序的安全。
- 缓存:缓存是 Apache Shiro 保证安全操作快速,高效的重要手段。
- 并发:Apache Shiro 支持多线程应用程序的并发特性。
- 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
- Run As:这个功能允许用户假设另一个用户的身份(在许可的前提下)
- Remember Me:跨 session 记录用户的身份,只有在强制需要时才需要登录。
- 注意:Shiro 不会去维护用户,维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给 Shiro
High-Level Overview 高级概述
- 在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm,下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。

-
Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务,守护进程帐户,时钟守护任务或者其它–当前和软件交互的任何事件。
-
SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
-
Realms:用于进行权限信息的验证,由我们自己实现,Realm 本质上是一个特定的安全 DAO,它封装与数据源连接的细节,得到Shiro 所需的相关的数据,在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)
-
我们需要实现Realms的Authentication 和 Authorization,其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
运行原理

- Subject:主体,可以看到主体可以是任何与应用交互的"用户”
- SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher,它是 Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制,它管理着所有 Subject,且负责进行认证和授权,及会话,缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,我们可以自定义实现,其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了。
- Authrizer:授权器,或者访问控制器,它用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能。
- Realm:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的,它可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等。
- SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager,而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境。
- SessionDAO:数据访问对象,用于会话的 CRUD,我们可以自定义 SessionDAO 的实现,控制 session 存储的位置,如通过 JDBC 写到数据库或通过 jedis 写入 Redis 中,另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能。
- CacheManager:缓存管理器,它来管理如用户,角色,权限等的缓存的,因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
- Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。
pom.xml
1 2 3 4 5
| <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
|
实体类
用户信息
1 2 3 4 5 6 7 8 9 10
| @Data public class UserInfo { private Long id; private String username; private String name; private String password; private String salt; @JsonIgnoreProperties(value = {"userInfos"}) private List<SysRole> roles; }
|
角色信息
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data public class SysRole { private Long id; private String name; private String description; @JsonIgnoreProperties(value = {"roles"}) private List<SysPermission> permissions; @JsonIgnoreProperties(value = {"roles"}) private List<UserInfo> userInfos;
}
|
权限信息
1 2 3 4 5 6 7 8 9 10
| @Data public class SysPermission { private Long id; private String name; private String description; private String url; @JsonIgnoreProperties(value = {"permissions"}) private List<SysRole> roles;
}
|
- 注意:当要使用RESTful风格返回给前台JSON数据的时候,比如当我们想要返回给前台一个用户信息时,由于一个用户拥有多个角色,一个角色又拥有多个权限,而权限跟角色也是多对多的关系,也就是造成了查用户→查角色→查权限→查角色→查用户…这样的无限循环,导致传输错误,所以我们根据这样的逻辑在每一个实体类返回JSON时使用了一个
@JsonIgnoreProperties
注解,来排除自身的无限引用的过程,也就是打断这样的无限循环。
MyShiroRealm
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
| public class MyShiroRealm extends AuthorizingRealm { @Resource private UserInfoService userInfoService;
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal(); for (SysRole role : userInfo.getRoles()) { simpleAuthorizationInfo.addRole(role.getName()); for (SysPermission permission : role.getPermissions()) { simpleAuthorizationInfo.addStringPermission(permission.getName()); } } return simpleAuthorizationInfo; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String username = (String) authenticationToken.getPrincipal(); System.out.println(authenticationToken.getPrincipal()); UserInfo userInfo = userInfoService.findByUsername(username); if (null == userInfo) { return null; }
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( userInfo, userInfo.getPassword(), ByteSource.Util.bytes(userInfo.getSalt()), getName() ); return simpleAuthenticationInfo; } }
|
ShiroConfig
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
| @Configuration public class ShiroConfig {
@Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/index"); shiroFilterFactoryBean.setUnauthorizedUrl("/403"); return shiroFilterFactoryBean; }
@Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5"); hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; }
@Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; }
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
@Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError"); mappings.setProperty("UnauthorizedException", "403"); r.setExceptionMappings(mappings); r.setDefaultErrorView("error"); r.setExceptionAttribute("ex"); return r; } }
|
过滤器
- Filter Chain定义说明。
- 一个URL可以配置多个Filter,使用逗号分隔。
- 当设置多个过滤器时,全部验证通过,才视为通过。
- 部分过滤器可指定参数,如perms,roles
过滤器简称 |
对应的 Java 类 |
anon |
org.apache.shiro.web.filter.authc.AnonymousFilter |
authc |
org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic |
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms |
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port |
org.apache.shiro.web.filter.authz.PortFilter |
rest |
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles |
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl |
org.apache.shiro.web.filter.authz.SslFilter |
user |
org.apache.shiro.web.filter.authc.UserFilter |
logout |
org.apache.shiro.web.filter.authc.LogoutFilter noSessionCreation |
anon
:表示任何人都可以访问。
authc
:表示该 uri 需要认证才能访问。
authcBasic
:表示该 uri 需要 httpBasic 认证。
perms[user:add:*]
:表示该 uri 需要认证用户拥有 user:add:* 权限才能访问。
port[8081]
:表示该 uri 需要使用 8081 端口。
rest[user]
:相当于/admins/=perms[user:method]
,其中,method 表示 get,post,delete 等。
roles[admin]
:表示该 uri 需要认证用户拥有 admin 角色才能访问。
ssl
:表示该 uri 需要使用 https 协议。
user
:表示该 uri 需要认证或通过记住我认证才能访问。
logout
:表示注销,可以当作固定配置。
注意
- anon,authcBasic,auchc,user 是认证过滤器。
- perms,roles,ssl,rest,port 是授权过滤器。
ShiroController
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
| @Controller public class ShiroController {
@GetMapping({"/", "/index"}) public String toIndex(Model model) { model.addAttribute("msg", "Hello Shiro"); return "index"; }
@RequestMapping("/toLogin") public String toLogin() { return "login"; }
@RequiresRoles( "admin" ) @RequiresPermissions("userInfo:add") @GetMapping("/user/add") public String add() { return "user/add"; }
@RequiresRoles( "admin") @RequiresPermissions("userInfo:update") @GetMapping("/user/update") public String update() { return "user/update"; }
@RequestMapping("/login") public String login(String username, String password, Model model) { Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { currentUser.login(token); token.setRememberMe(true); } catch (UnknownAccountException e) { model.addAttribute("msg", "用户名错误"); return "login"; } catch (IncorrectCredentialsException e) { model.addAttribute("msg", "密码错误"); return "login"; } catch (LockedAccountException e) { System.out.println("用户名 " + token.getPrincipal() + " 被锁定 !"); return "login"; } if (currentUser.isAuthenticated()) { System.out.println("用户 " + currentUser.getPrincipal() + " 登陆成功!"); System.out.println("是否拥有 manager 角色:" + currentUser.hasRole("manager")); System.out.println("是否拥有 user:create 权限" + currentUser.isPermitted("user:create")); } return "index"; }
@RequestMapping("/403") @ResponseBody public String unauthorized() { return "未经授权无法访问此页面"; }
@RequestMapping("/logout") @ResponseBody public String logout() { Subject currentUser = SecurityUtils.getSubject(); currentUser.logout(); return "注销成功"; } }
|
注解
- 在使用 Shiro 的注解之前,请确保项目中已经添加支持 AOP 功能的相关jar包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RequiresRoles( "manager" ) public String save() { }
@RequiresPermissions("user:manage") public String delete() { }
@RequiresUser public Map<String,Object> getLogout(){ }
|
注解名称 |
说明 |
@RequiresAuthentication |
使用该注解标注的类,方法等在访问时,当前Subject必须在当前session中已经过认证 |
@RequiresGuest |
使用该注解标注的类,方法等在访问时,当前Subject可以是"Guest"身份,不需要经过认证或者在原先的session中存在记录 |
@RequersUser |
验证用户是否被记忆,有两种含义一种是成功登录的(subject.isAuthenticated()结果为true);另外一种是被记忆的(subject.isRemembere()结果为true). |
@RequiresPermission |
当前Subject需要拥有某些特定的权限时,才能执行被该注解标主的方法如果没有权限,则方法不会执行还会抛出AuthorizationException异常 |
@RequiresRoles |
当前Subject必须拥有所有指定的角色时,才能访问被该注解标主的方法如果没有角色则方法不会执行还会抛出AuthorizationException异常 |
- 允许存在多个角色和权限,默认逻辑是 AND,也就是同时拥有这些才可以访问方法,可以在注解中以参数的形式设置成 OR
1 2 3 4
| @RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
@RequiresPermissions (value={"sys:user:info" ,"sys:role: info", logical = Logical.AND)
|
- Shiro 注解是存在顺序的,当多个注解在一个方法上的时候,会逐个检查,知道全部通过为止。
- 默认拦截顺序是: RequiresRoles->RequiresPermissions->RequiresAuthentication->RequiresUser->RequiresGuest
1 2 3
| @RequiresRoles (value={"ADMIN") @RequiresPermissions("sys:role:info")
|