- Shiro
- 1、什么是 Shiro
- 2、功能介绍
- 3、Shiro 架构
- 3.1 Shiro 外部架构
- 3.2 Shiro 内部架构
- 4、Hello,Shiro
- 4.1 快速实践
- 5、Shiro 的 Subject 分析
- 6、集成 SpringBoot
- 6.1 搭建环境
- 6.2 登陆拦截
- 6.3 用户认证
- 6.4 整合 MyBatis
- 6.5 请求授权
- 6.6 整合 Thymeleaf
-
Apache Shiro 是一个 java 的安全(权限)框架
-
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境
-
Shiro 可以完成,认证,授权,加密,会话管理,Web 集成,缓存等等
-
Shiro 官方地址:http://shiro.apache.org/
-
10 Minute Tutorial on Apache Shiro:http://shiro.apache.org/10-minute-tutorial.html
- Authentication:身份认证、登录,验证用户是不是拥有相应的身份
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限
- Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都会存放在会话中,会话可以是普通的 JavaSE 环境,也可以是 Web 环境
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储
- Web Support:Web 支持,可以非常容易的集成到 Web 环境
- Caching:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
- Concurrency:Shiro 支持多线程应用的并发验证,即:如在一个线程开启另一个线程,能把权限自动的传播过去
- Testing:提供测试支持
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
- Remember Me:记住我功能,这是一个非常常见的功能,一次登录后,只要不注销,下次打开的话就不用登录
从外部来看 Shiro,即从应用程序角度来观察如何使用 食肉 完成工作:
- subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject,Subject 代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等等,与 Subject 的所有交互都会委托给 SecurityManager;Subject 其实就是一个门面,SecurityManager 才是实际的执行者
- SecurityManager:安全管理器,即所有与安全有关的操作都会与 SecurityManager 加护,并且它管理者所有的 Subject,可以看出它是 Shiro 的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 的 DispatcherServlet 的角色
- Realm:Shiro 从 Realm 获取安全数据(如用户,角色,权限),就是说 SecurityManager 要验证用户身份,那么他需要从 Realm 获取相应的用户进行比较,来确定用户的身份是否合法;也需要从 Realm 得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以吧 Realm 看成 DataSource
- Subject:任何可以与用户交互的 ‘用户‘
- Security Manager:相当于 SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏,所有具体的交互都通过 SecurityManager 进行控制,他管理着所有的 Subject,且负责进行认证,授权,会话,以及缓存的管理
- Authentication:负责 Subject 认证,是一个扩展点,可以自定义实现,可以使用认证策略(AuthenticationStrategy),即什么情况下算用户认证通过了
- Authorizer:授权器,即访问控制器,用来决定主体是否有权进行相应的操作;即控制着用户访问应用中的哪些功能
- Realm:可以有一个或者多个的 Realm,可以认为是安全实体数据源,即用户获取安全实体,可以用 JDBC 实现,也可以提取内存实现等等;由用户提供,所以一般在应用中都需要实现自己的 Realm
- SessionManager:管理 Session 生命周期的组件,而 Shiro 并不仅仅可以用在 Web 环境,也可以用在普通的 JavaSE 环境中
- CacheManager:缓存控制器,来管理如用户,角色,权限等缓存,因为这些数据基本很少改变,放到缓存中后可以提高访问的性能
- Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于密码加密,以及解密等等
官方文档:http://shiro.apache.org/tutorial.html
官方 quickstart:https://github.com/apache/shiro/tree/main/samples/quickstart
快速搭建步骤:
-
创建一个 Maven 父工程,用于学习 Shiro,删掉 src
-
创建一个普通的 Maven 项目
-
根据官方文档,导入 shiro 依赖
- 如果没有导入 log4j,那么就会默认使用 commons-logging
org.apache.shiro shiro-core 1.8.0 org.slf4j jcl-over-slf4j 2.0.0-alpha5 org.slf4j slf4j-log4j12 2.0.0-alpha5 test log4j log4j 1.2.17 -
在 resource 目录下,创建并将官网的内容编写入 log4j 的配置文件
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n # General Apache libraries log4j.logger.org.apache=WARN # Spring log4j.logger.org.springframework=WARN # Default Shiro logging log4j.logger.org.apache.shiro=INFO # Disable verbose logging log4j.logger.org.apache.shiro.util.ThreadContext=WARN log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
-
在 resource 目录下,创建并将官网的内容编写入 shiro.Ini 文件
- 如果使用 IDEA 需要下载并安转 Ini 插件,否则看着不爽
[users] # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc # ----------------------------------------------------------------------------- [roles] # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5 -
在 java 文件夹下,将官网的快速搭建粘入
-
注意:新版中官方已经有更好的方法替代了之前的方法
// 旧 //import org.apache.shiro.ini.IniSecurityManagerFactory; //import org.apache.shiro.lang.util.Factory; // 新 import org.apache.shiro.mgt.DefaultSecurityManager; // new import org.apache.shiro.realm.text.IniRealm; // new
// 旧方法 //Factory
factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //SecurityManager securityManager = factory.getInstance(); // 新方法 DefaultSecurityManager securityManager = new DefaultSecurityManager(); IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); securityManager.setRealm(iniRealm);
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; //import org.apache.shiro.ini.IniSecurityManagerFactory; import org.apache.shiro.mgt.DefaultSecurityManager; // new import org.apache.shiro.realm.text.IniRealm; // new import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; //import org.apache.shiro.lang.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // The easiest way to create a Shiro SecurityManager with configured // realms, users, roles and permissions is to use the simple INI config. // We'll do that by using a factory that can ingest a .ini file and // return a SecurityManager instance: // Use the shiro.ini file at the root of the classpath // (file: and url: prefixes load from files and urls respectively): // 旧方法 //Factoryfactory = new IniSecurityManagerFactory("classpath:shiro.ini"); //SecurityManager securityManager = factory.getInstance(); // 新方法 DefaultSecurityManager securityManager = new DefaultSecurityManager(); IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); securityManager.setRealm(iniRealm); // for this simple example quickstart, make the SecurityManager // accessible as a JVM singleton. Most applications wouldn't do this // and instead rely on their container configuration or web.xml for // webapps. That is outside the scope of this simple quickstart, so // we'll just do the bare minimum so you can continue to get a feel // for things. SecurityUtils.setSecurityManager(securityManager); // Now that a simple Shiro environment is set up, let's see what you can do: // get the currently executing user: Subject currentUser = SecurityUtils.getSubject(); // Do some stuff with a Session (no need for a web or EJB container!!!) Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // let's login the current user so we can check against roles and permissions: if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } //say who they are: //print their identifying principal (in this case, a username): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } //all done - log out! currentUser.logout(); System.exit(0); } } -
-
启动,发现打印了日志信息
-
表明,quickstart 完成
问题:
-
运行时报错
-
问题原因以及解决办法
-
获取当前的用户对象 Subject
Subject currentUser = SecurityUtils.getSubject();
-
通过当前用户拿到 session,并在 session 中设置至,以及获取值
Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } -
判断当前用户是否被认证
if (!currentUser.isAuthenticated()) { // Token:令牌 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); // 设置记住我 token.setRememberMe(true); try { // 执行登录操作 currentUser.login(token); } catch (UnknownAccountException uae) { // 用户名错误 log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { // 密码错误 log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { // 多次用户密码错误,就会被锁定 log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { // 认证是否成功 //unexpected condition? error? } } -
获取当前用户认证
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); -
判断当前是否拥此角色
- 角色都在 shiro.Ini 中配置
if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } -
检测拥有什么权限
-
权限也是在 shiro.Ini 中配置
-
粗粒度
if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } -
细粒度
if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); }
-
-
注销
//all done - log out! currentUser.logout();
-
结束
System.exit(0);
-
创建一个新的 springboot 项目,导入 spring web 依赖
-
导入 thymeleaf 依赖
org.springframework.boot spring-boot-starter-thymeleaf -
创建 config 目录,编写 Shiro 配置文件:ShiroConfig
- 最好从后往前写
- 通常 ShiroFilterFactoryBean 实现请求过滤,而 UserRealm 来进行真正的权限过滤
package com.aze.config; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ShiroConfig { // 3. ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); return bean; } // 2. DefaultWebSecurityManager @Bean(name = "securityManager") public DefaultWebSecurityManager getDDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联 UserRealm securityManager.setRealm(userRealm); return securityManager; } // 1. 创建 realm 对象,需要自定义类 @Bean(name = "userRealm") public UserRealm userRealm(){ return new UserRealm(); } } -
自定义 UserReaml,需要继承 AuthorizingRealm
package com.aze.config; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; // 自定义 UserReaml 需要继承 AuthorizingRealm public class UserRealm extends AuthorizingRealm { // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权 doGetAuthorizationInfo"); return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证 doGetAuthenticationInfo"); return null; } } -
编写前端的测试页面
-
创建 controller 目录,编写 MyController
package com.aze.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyController { @RequestMapping({"/","/index","/index.html"}) public String toIndex(Model model){ model.addAttribute("msg","hello,shiro!"); return "index"; } @RequestMapping("/user/add") public String add(){ return "/user/add"; } @RequestMapping("/user/update") public String update(){ return "/user/update"; } } -
启动项目,进行测试
-
环境搭建成功
-
编写登录页面 login.html
Title 登录
-
在 MyController 中添加登录的跳转
@RequestMapping("/toLogin") public String toLogin(){ return "login"; } -
shiro 内置过滤器
- anon:不需要认证就可以访问
- authc:必须要认证了才可以访问
- user:必须拥有“记住我”功能才可以使用
- perms:拥有对某个资源的权限才可以访问
- role:拥有某个角色的权限才可以访问
-
在 shiro 配置文件中,添加登录拦截的相关过滤器
// 添加 shiro 的内置过滤器 Map
filterMap = new linkedHashMap<>(); // 将 /user 下的所有路径都需要登录认证 filterMap.put("/user Map filterMap = new linkedHashMap<>(); // 授权,正常情况下,没有授权会跳转到未授权页面 filterMap.put("/user/add","perms[user:add]"); filterMap.put("/user/update","perms[user:update]"); //filterMap.put("/user/add","authc"); //filterMap.put("/user/update","authc"); filterMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterMap); // 设置登录请求 bean.setLoginUrl("/toLogin"); // 设置未授权页面 bean.setUnauthorizedUrl("/noauth"); return bean; } // 2. DefaultWebSecurityManager @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联 UserRealm securityManager.setRealm(userRealm); return securityManager; } // 1. 创建 realm 对象,需要自定义类 @Bean(name = "userRealm") public UserRealm userRealm(){ return new UserRealm(); } } -
在 MyController 中添加未授权页面的请求
@RequestMapping("/noauth") @ResponseBody public String unauthorized(){ return "未经授权无法访问此页面!"; } -
编写用户的授权(UserRealm)
package com.aze.config; import com.aze.pojo.User; import com.aze.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; // 自定义 UserReaml 需要继承 AuthorizingRealm public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权 doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //info.addStringPermission("user:add"); // 取得当前登录的对象 Subject subject = SecurityUtils.getSubject(); User currentUser = (User) subject.getPrincipal(); info.addStringPermission(currentUser.getPerms()); return info; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证 doGetAuthenticationInfo"); UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; // 连接数据库 User user = userService.queryUserByName(userToken.getUsername()); if (user == null){ // 如果没有这个用户,则报出异常 UnknownAccountException return null; } // 密码认证,shiro 会自动帮你做了 return new SimpleAuthenticationInfo(user,user.getPwd(),""); } } -
修改数据库,增添权限
-
修改实体类
package com.aze.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private Integer id; private String name; private String pwd; // 增添 private String perms; } -
测试不同用户权限
| 标签 | 说明 |
|---|---|
| shiro:guest | 用户没有身份验证时显示相应信息,即游客访问信息 |
| shiro:user | 用户已经身份验证/记住我登录后显示相应的信息 |
| shiro:authenticated | 用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的 |
| shiro:notAuthenticated | 用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证 |
| shiro: principal | 相当于((User)Subject.getPrincipals()).getUsername() |
| shiro:lacksPermission | 如果当前Subject没有权限将显示body体内容 |
| shiro:hasRole | 如果当前Subject有角色将显示body体内容 |
| shiro:hasAnyRoles | 如果当前Subject有任意一个角色(或的关系)将显示body体内容 |
| shiro:lacksRole | 如果当前Subject没有角色将显示body体内容 |
| shiro:hasPermission | 是否拥有此权限 如果当前Subject有权限将显示body体内容 |



