0x01 shiro环境搭建 以下是在Spring Boot中整合Shiro的基本应用示例,帮助我们掌握Shiro的基本使用
springboot:2.2.1.RELEASE
java:1.8.0_144
mysql:5.7.21
01、创建springboot项目 1)创建旧版本的 Spring Boot 项目。
关于创建任意 Spring Boot 和 Java 版本的 Spring Boot 项目,以 Maven 项目为例,只需要在创建好的springboot项目修改 pom.xml
中的 2 个配置元素即可:
<version>2.2.1.RELEASE</version>
指定 Spring Boot 的版本号
<java.version>1.8</java.version>
指定 Java 的版本号
2)创建springboot项目这个比较简单、就不多说了。然后导入thymeleaf
模版依赖。
1 2 3 4 5 <!--thymeleaf--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
2)新建一个首页, 在src\main\resources\templates
路径下
我们要使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。
xmlns:th="http://www.thymeleaf.org"
3)为首页编写一个Controller
4)启动,测试。访问到如下页面就是成功了!
02、导入基本的依赖 1 2 3 4 5 6 <!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency>
03、编写shiro配置文件骨架 先创建好基本骨架、再添加内容!
shiro三大核心对象,咱们从后往前依次创建
ShiroFilterFactoryBean
DefaultWebSecurityManager
自定义Realm对象
1)Realm 可以理解为 Shiro 与后端数据源进行交互的桥梁
负责从数据源(例如数据库、LDAP、文件系统等)中获取用户身份信息和权限信息,然后将这些信息提供给 Shiro 进行认证和授权操作。
自定义 Realm 类需要继承 AuthorizingRealm
类,并实现其中的 doGetAuthenticationInfo
和 doGetAuthorizationInfo
方法来完成认证和授权逻辑。
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 package com.example.shiro.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;import org.springframework.stereotype.Component;@Component 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 ; } }
2)DefaultWebSecurityManager是 Apache Shiro 框架中的一个关键组件,它实现了 SecurityManager 接口,并且特别适用于 Web 应用程序的安全管理。
DefaultWebSecurityManager 负责协调 Shiro 的各个组件,包括 Realm、SessionManager、CacheManager 等,以提供全面的安全功能。
1 2 3 4 5 6 7 8 9 10 11 12 @Bean("securityManager") public DefaultWebSecurityManager defaultWebSecurityManager () { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(userRealm); return securityManager; }
3)ShiroFilterFactoryBean
整合 Shiro 到 Spring 应用程序中时,通常会使用 ShiroFilterFactoryBean
来配置 Shiro 的过滤器链,以定义 URL 拦截的规则和权限控制策略。
1 2 3 4 5 6 7 8 9 10 11 12 @Bean("shirFilter") public ShiroFilterFactoryBean shirFilter (SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean (); shiroFilterFactoryBean.setSecurityManager(securityManager); return shiroFilterFactoryBean; }
完整配置文件:
04、实现登录拦截 1)需求:login页都可访问、index也需登录才能访问
首先新建登录页,并编写接口
controller
1 2 3 4 @GetMapping("/login") public String toLogin () { return "login" ; }
login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!DOCTYPE html > <html xmlns:th ="http://www.thymeleaf.org" lang ="en" > <head > <meta charset ="UTF-8" > <title > Login Page</title > </head > <body class ="text-center" > <form th:action ="@{/login}" method ="post" > Username: <input type ="text" placeholder ="Username" th:name ="username" required ="required" > <br > Password: <input type ="password" placeholder ="Password" th:name ="password" required ="required" > <br > <button type ="submit" name ="submit" value ="Login" > Sign in</button > </form > </body > </html >
2)实现
通过在shiro配置文件中配置ShiroFilterFactoryBean实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Bean("shirFilter") public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { //创建了一个 ShiroFilterFactoryBean 对象 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); //未经身份验证时,将重定向到 /login 这个URL,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); //添加shiro内置过滤器 /* anon 无需认证可访问 authc 必须认证才能访问 user 需有rememberMe功能才可使用 permes 有对应权限可访问 role 有对应角色才可访问 */ LinkedHashMap<String,String> fc = new LinkedHashMap<>(); fc.put("/**", "authc"); fc.put("/login", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(fc); return shiroFilterFactoryBean; }
3)测试:访问首页,直接跳转到登录页!
05、实现用户认证 用户认证是通过实现 Shiro 的 Realm
接口来完成的。在 Realm
实现类中,需要重写 doGetAuthenticationInfo
方法,该方法用于验证用户的身份凭证(例如用户名和密码)是否正确。在验证成功后,可以返回一个 AuthenticationInfo
对象,表示用户经过身份验证。
1)首先第一步还是先写一个处理登录的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @PostMapping("/login") public String login(String username,String password,Model model){ //获取当前用户的 Subject 对象,该对象用于执行身份验证和授权操作。 Subject subject = SecurityUtils.getSubject(); //封装用户登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); //执行登录,如果没有异常就说明ok try { subject.login(token); return "index"; } catch (UnknownAccountException e) { model.addAttribute("msg","用户名错误"); return "login"; } catch (IncorrectCredentialsException e){ model.addAttribute("msg","密码错误"); return "login"; } }
2)登录页添加返回信息
1 <p th:text="${msg}" style="color:red"></p>
3)处理登录信息调用的是controller,但实际的核心是在自定义的Realm
中
给证明一下,这里先把之前咱们自定义的Realm拿来回忆一下,貌似也啥都没写!
4)随便登录一个用户,看看控制台
由此证具体的用户认证是通过实现 Shiro 的 Realm
接口来完成的
5)具体如何实现入户认证
一般都是从数据库查询然后与在controller接口处封装的数据对比完成!
现在为了方便演示,我们先用写死的用户信息!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { System.out.println("执行了 -> 认证 doGetAuthenticationInfo" ); String username="admin" ; String password="123456" ; UsernamePasswordToken userToken= (UsernamePasswordToken) token; if (!userToken.getUsername().equals(username)){ return null ; } return new SimpleAuthenticationInfo ("" ,password,"" ); }
6)测试,密码正确就可以成功完成认证
06、整合mybaits 1)导入依赖
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 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.16.20</version > </dependency > <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <version > 1.2.17</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.8</version > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.1.0</version > </dependency >
2)mybatis配置文件
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 spring: datasource: url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false username: shiro password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 mybatis: type-aliases-package: com.example.shiro.pojo mapper-locations: classpath:mapper/*.xml
3)idea连接数据库
导入sql文件
1 2 3 4 5 6 7 8 9 10 11 DROP TABLE IF EXISTS `users`;CREATE TABLE `users` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `password` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `rid` int (11 ) NULL DEFAULT 0 , PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ; INSERT INTO `users` VALUES (1 , 'admin' , '123456' , 1 );INSERT INTO `users` VALUES (2 , 'test' , '123456' , 0 );
4)实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.shiro.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data @NoArgsConstructor @AllArgsConstructor public class User { private Integer id; private String username; private String password; private Integer rid; }
5)编写Mapper接口
6)测试一下
7)规范一下,我们把Service层也写上
8)用户认证功能,完善从数据库获取用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { System.out.println("执行了 -> 认证 doGetAuthenticationInfo" ); UsernamePasswordToken userToken = (UsernamePasswordToken) token; User user = userService.getByUserName(userToken.getUsername()); if (null == user) { return null ; } else { String password = user.getPassword(); return new SimpleAuthenticationInfo (token.getPrincipal(), password, getName()); } }
现在就实现了用户信息从数据库获取
07、实现rememberMe功能 Apache Shiro 提供了 “Remember Me”(记住我)功能,可以使用户在关闭浏览器后再次访问应用程序时无需重新登录。
当用户选择 “Remember Me” 选项并成功登录时,Shiro 将生成一个记住登录状态的令牌,并将其存储在用户浏览器的 Cookie 中。下次用户再次访问应用程序时,Shiro 将从 Cookie 中读取令牌,并使用该令牌自动进行身份验证,而无需用户再次提供用户名和密码。
1)修改配置类,添加一下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public SimpleCookie rememberMeCookie () { SimpleCookie cookie = new SimpleCookie ("ShiroTest_RememberMe" ); cookie.setPath("/" ); cookie.setHttpOnly(true ); cookie.setMaxAge(30 * 24 * 60 * 60 ); return cookie; } public CookieRememberMeManager rememberMeManager () { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager (); cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey("1234567890987654" .getBytes()); return cookieRememberMeManager; }
2)在安全管理器
中设置rememberMe
1 securityManager.setRememberMeManager(rememberMeManager());
3)配置shiro内置过滤器
4)修改controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @PostMapping("/login") public String login (String username,String password,Model model,@RequestParam(defaultValue = "false") boolean rememberMe, HttpSession session) { Subject subject = SecurityUtils.getSubject(); AuthenticationToken token = new UsernamePasswordToken (username, password, rememberMe); try { subject.login(token); return "redirect:index" ; } catch (UnknownAccountException e) { model.addAttribute("msg" ,"用户名错误" ); return "login" ; } catch (IncorrectCredentialsException e){ model.addAttribute("msg" ,"密码错误" ); return "login" ; } }
5)修改登录页面
1 2 3 4 5 6 <form th:action ="@{/login}" method ="post" > Username: <input type ="text" placeholder ="Username" th:name ="username" required ="required" > <br > Password: <input type ="password" placeholder ="Password" th:name ="password" required ="required" > <br > 记住我:<input type ="checkbox" name ="rememberMe" value ="true" > </div > <br > <button type ="submit" name ="submit" value ="Login" > Sign in</button > </form >
6)退出浏览器再次访问,状态还在!
08、退出功能 1)添加退出按钮
1 2 3 4 5 6 7 8 9 <body > <h1 > 首页</h1 > <p th:text ="${msg}" > </p > <a href ="/logout" > 登出</a > </body >
2)配置类中添加logout过滤器
1 fc.put("/logout","logout");
09、授权之角色认证 在 Apache Shiro 中,授权是在 Realm 中完成的。通过在自定义的 Realm 类中实现 doGetAuthorizationInfo
方法来完成授权操作。该方法会在授权过程中被 Shiro 调用,并返回一个 AuthorizationInfo
对象,其中包含了用户的角色、权限等相关信息。
1)编写一个接口用于测试是否拥有角色
1 2 3 4 5 6 @GetMapping("/testAdmin") @ResponseBody public String testAdmin () { return "您是管理员" ; }
2)在shiro过滤器中设置,这个接口下的页面之后admin角色才可以访问
1 fc.put("/testAdmin", "roles[admin]");
3)现在登录后我们访问这个页面,可以看到报401未授权、并且控制台可以看到条用了我们自定义的Realm中的doGetAuthorizationInfo
方法
4)我们在自定义的Realm中的doGetAuthorizationInfo
方法中给当前用户添加角色
1 2 3 4 5 6 7 8 9 10 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("执行了 -> 授权 doGetAuthorizationInfo"); //1 创建对象,存储当前登录的用户的权限和角色 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //2 存储角色 info.addRole("admin"); return info; }
5)再次访问就可以访问到了
6)自定义401错误页面
1 shiroFilterFactoryBean.setUnauthorizedUrl("/401");
1 2 3 4 5 @GetMapping("/401") @ResponseBody public String Unauthorized () { return "你没有权限访问!" ; }
10、授权之权限认证 权限认证和角色认证实现方法类似,就不过多赘述!接下来就直接整合规范一下授权这块
1)创建角色表、权限表、以及角色-权限表
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 SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0 ;DROP TABLE IF EXISTS `perm`;CREATE TABLE `perm` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `permission` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `ps- name` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `pid` int (11 ) NULL DEFAULT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ; INSERT INTO `perm` VALUES (1 , 'user:add' , '增' , 1 );INSERT INTO `perm` VALUES (2 , 'user:update' , '改' , 2 );INSERT INTO `perm` VALUES (3 , 'user:select' , '查' , 3 );INSERT INTO `perm` VALUES (4 , 'user:delete' , '删' , 4 );DROP TABLE IF EXISTS `role`;CREATE TABLE `role` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `role` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `roleName` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `rid` int (11 ) NULL DEFAULT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ; INSERT INTO `role` VALUES (1 , 'admin' , '管理员' , 1 );INSERT INTO `role` VALUES (2 , 'user' , '普通用户' , 0 );DROP TABLE IF EXISTS `role_perm`;CREATE TABLE `role_perm` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `rid` int (11 ) NULL DEFAULT NULL , `pid` int (11 ) NULL DEFAULT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Fixed; INSERT INTO `role_perm` VALUES (1 , 0 , 1 );INSERT INTO `role_perm` VALUES (2 , 0 , 2 );INSERT INTO `role_perm` VALUES (3 , 0 , 3 );INSERT INTO `role_perm` VALUES (4 , 1 , 1 );INSERT INTO `role_perm` VALUES (5 , 1 , 2 );INSERT INTO `role_perm` VALUES (6 , 1 , 3 );INSERT INTO `role_perm` VALUES (7 , 1 , 4 );DROP TABLE IF EXISTS `role_user`;CREATE TABLE `role_user` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `uid` int (11 ) NULL DEFAULT NULL , `rid` int (11 ) NULL DEFAULT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Fixed; INSERT INTO `role_user` VALUES (1 , 1 , 1 );INSERT INTO `role_user` VALUES (2 , 2 , 0 );DROP TABLE IF EXISTS `users`;CREATE TABLE `users` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `password` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `rid` int (11 ) NULL DEFAULT 0 , PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ; INSERT INTO `users` VALUES (1 , 'admin' , '123456' , 1 );INSERT INTO `users` VALUES (2 , 'test' , '123456' , 0 );SET FOREIGN_KEY_CHECKS = 1 ;
2)编写查询角色和权限的mapper
mapper接口
1 2 3 public String getUserRole(int id); public List<String> getRolePerms(int id);
实现
1 2 3 4 5 6 7 8 9 10 11 <select id="getUserRole" resultType="java.lang.String"> select role from role where rid = (select rid from role_user where uid = #{id}) </select> <select id="getRolePerms" resultType="java.lang.String"> SELECT DISTINCT p.permission FROM users u JOIN role_user ru ON u.id = ru.uid JOIN role_perm rp ON ru.rid = rp.rid JOIN perm p ON rp.pid = p.pid WHERE u.id = #{id}; </select>
3)编写其对应的service
接口
1 2 3 public String getUserRole(int id); public List<String> getRolePerms(int id);
实现
1 2 3 4 5 6 7 8 9 10 11 @Override public String getUserRole(int id) { String role=userMapper.getUserRole(id); return role; } @Override public List<String> getRolePerms(int id) { List<String> perms = userMapper.getRolePerms(id); return perms; }
4)修改自定义的Realm中doGetAuthorizationInfo
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("执行了 -> 授权 doGetAuthorizationInfo"); //1 创建对象,存储当前登录的用户的权限和角色 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //2 获取用户id User p = (User) principals.getPrimaryPrincipal(); Integer uid = p.getId(); //3 查询角色 String userRole = userService.getUserRole(uid); //4 查询权限 List<String> rolePerms = userService.getRolePerms(uid); //5 存储角色 info.addRole(userRole); //6 存储权限 info.addStringPermissions(rolePerms); return info; }
5)添加几个页面用来测试权限认证
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 @GetMapping("/admin") @ResponseBody public String testAdmin () { return "您是管理员" ; } @GetMapping("/add") @ResponseBody public String add () { return "add" ; } @GetMapping("/update") @ResponseBody public String update () { return "update" ; } @GetMapping("/select") @ResponseBody public String select () { return "select" ; } @GetMapping("/delete") @ResponseBody public String delete () { return "delete" ; }
6)shiro过滤器对这几个页面添加拦截
7)测试
0x02 参考 start.spring.io 创建旧版本的 Spring Boot 项目 - spring 中文网 (springdoc.cn)
https://www.w3cschool.cn/shiro/co4m1if2.html
SpringBoot整合Shiro环境搭建_哔哩哔哩_bilibili
https://blog.csdn.net/weixin_60223449/article/details/127143064
https://github.com/phith0n/JavaThings/tree/master/shirodemo