SpringBoot 整合 Shiro
可对比 SpringSecurity 学习使用。
1、环境搭建
-
创建 springboot 的 web 项目,创建时勾选 thymeleaf 选项
也可以导入直接创建 web 项目,然后导入 thymeleaf 依赖
<!--引入thymeleaf依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
-
编写首页
- 导入命名空间
xmlns:th="http://www.thymeleaf.org"
- 获取测试数据
<p th:text="${msg}"></p>
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> </body> </html>
- 导入命名空间
-
编写对应的 controller
package com.cheng.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 springboot-shiro!"); return "index"; } }
-
测试
测试成功!
-
导入 shiro-spring 依赖
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency>
-
自定义 UserRealm 类
package com.cheng.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; 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; } }
-
配置 ShiroConfig
package com.cheng.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; @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 getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联 UserRealm securityManager.setRealm(userRealm); return securityManager; } // 1.创建 realm 对象,需要自定义类 @Bean public UserRealm userRealm() { return new UserRealm(); } }
-
在 templates 文件夹下创建 user 文件夹,在 user 文件夹下创建 添加用户页 和 修改用户页
- templates
- user
- addUser
- updateUser
- user
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>addUser</title> </head> <body> <h2>添加用户页</h2> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>updateUser</title> </head> <body> <h2>修改用户页</h2> </body> </html>
- templates
-
编写 添加用户 和 修改用户 的 controller
@RequestMapping("/user/add") public String toAddUserPage() { return "user/addUser"; } @RequestMapping("/user/update") public String toUpdateUserPage() { return "user/updateUser"; }
-
在首页添加跳转到 添加用户页 和 修改用户页 的链接
<hr> <a th:href="@{/user/add}">添加用户</a> | <a th:href="@{/user/update}">修改用户</a>
-
测试
测试成功!
2、Shiro 实现登录拦截
-
添加 shiro 的内置过滤器
// 3.ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(defaultWebSecurityManager); // 添加 shiro 的内置过滤器 /* * anon : 无需认证就可以访问 * authc : 必须认证了才能访问 * user : 必须拥有 记住我 功能才能访问 * perms : 拥有对某个资源的权限才能访问 * roles : 拥有某个角色权限才能访问 * */ Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/user/add", "authc"); filterMap.put("/user/update", "authc"); bean.setFilterChainDefinitionMap(filterMap); return bean; }
-
编写登录页
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录页</title> </head> <body> <h2>登录页</h2> <hr> <form action=""> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="password" name="password"></p> <p><input type="submit" value="提交"></p> </form> </body> </html>
-
在 controller 中配置登录页
@RequestMapping("/toLogin") public String toLogin() { return "login"; }
-
在 config 的 ShiroFilterFactoryBean 中设置登录的请求
// 3.ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(defaultWebSecurityManager); // 添加 shiro 的内置过滤器 /* * anon : 无需认证就可以访问 * authc : 必须认证了才能访问 * user : 必须拥有 记住我 功能才能访问 * perms : 拥有对某个资源的权限才能访问 * roles : 拥有某个角色权限才能访问 * */ Map<String, String> filterMap = new LinkedHashMap<>(); //filterMap.put("/user/add", "authc"); //filterMap.put("/user/update", "authc"); filterMap.put("/user/*", "authc"); bean.setFilterChainDefinitionMap(filterMap); // 设置登录的请求 bean.setLoginUrl("/toLogin"); return bean; }
-
测试
测试成功!
3、Shiro 实现用户认证
-
在 controller 中配置 登录认证
@RequestMapping("/login") public String login(String username, String password, Model model){ // 获取当前的用户 Subject subject = SecurityUtils.getSubject(); // 封装用户的登陆数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try {// 没有异常,登录成功,返回首页 subject.login(token);// 执行登录的方法 return "index"; }catch (UnknownAccountException e) {// 用户名不存在,返回登陆页 model.addAttribute("msg", "用户名错误"); return "login"; }catch (IncorrectCredentialsException e) {// 密码不存在,返回登陆页 model.addAttribute("msg", "密码错误"); return "login"; } }
-
在 login 页面设置提交,并设置错误信息展示
<p th:text="${msg}" style="color: red"></p> <form th:action="@{/login}"> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="password" name="password"></p> <p><input type="submit" value="提交"></p> </form>
-
在 UserRealm 中的认证方法中,进行用户名和密码校验
注:这里先伪造数据进行测试
// 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了认证 ==> doGetAuthenticationInfo 方法"); // 用户名,密码 从数据库中取 String name = "root"; String password = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) token; if (!userToken.getUsername().equals(name)) {// 用户名不存在 return null; } // 密码认证,shiro 去做 return new SimpleAuthenticationInfo("",password, ""); }
-
重启测试
测试成功:
- 用户名输入 root 之外的字符时,提交显示用户名错误信息
- 密码输入 123456 之外的字符时,提交显示密码错误信息
- 用户名和密码输入正确时,登陆成功
4、Shiro 整合 Mybatis
-
导入数据库连接所需要的依赖
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.22</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency> <!--偷懒神器,非必要--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
-
配置 ymal
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #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:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
连接数据库,搭建框架(pojo, mapper),配置 properties
mybatis.type-aliases-package=com.cheng.pojo mybatis.mapper-locations=classpath:mapper/*.xml
-
编写 User 实体类
package com.cheng.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; }
-
编写 UserMapper 接口
package com.cheng.mapper; import com.cheng.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Repository @Mapper public interface UserMapper { public User queryUserByName(String name); }
-
编写 UserMapper.xml 配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cheng.mapper.UserMapper"> <select id="queryUserByName" parameterType="String" resultType="User"> select * from mybatis.user where name=#{name}; </select> </mapper>
-
编写 UserService 接口
package com.cheng.service; import com.cheng.pojo.User; public interface UserService { public User queryUserByName(String name); }
-
编写 UserServiceImpl 实现类
package com.cheng.service; import com.cheng.mapper.UserMapper; import com.cheng.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User queryUserByName(String name) { return userMapper.queryUserByName(name); } }
-
测试
测试成功!
-
将 UserRealm 认证中的模拟数据修改为真实数据
package com.cheng.config; import com.cheng.pojo.User; import com.cheng.service.UserService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了授权 ==> doGetAuthorizationInfo 方法"); return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了认证 ==> doGetAuthenticationInfo 方法"); UsernamePasswordToken userToken = (UsernamePasswordToken) token; // 连接真实数据库,获取用户 User user = userService.queryUserByName(userToken.getUsername()); if (user == null) {// 没有查到这个人 return null; } // 密码认证,shiro 去做 return new SimpleAuthenticationInfo("",user.getPwd(), ""); } }
-
测试,使用数据库中的用户名及密码登录
测试成功!
5、Shiro 请求授权实现
-
设置 /user/add 需要有权限才能进入
// 3.ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(defaultWebSecurityManager); // 添加 shiro 的内置过滤器 /* * anon : 无需认证就可以访问 * authc : 必须认证了才能访问 * user : 必须拥有 记住我 功能才能访问 * perms : 拥有对某个资源的权限才能访问 * roles : 拥有某个角色权限才能访问 * */ // 拦截 Map<String, String> filterMap = new LinkedHashMap<>(); // 授权,正常情况下,未授权会跳转到未授权页面 // 注:如果拦截使用了 /user/* , 那么首选需要写在拦截之前,否则不生效 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"); return bean; }
注:经测试发现👇
- 如果拦截使用了 /user/* , 那么授权需要写在拦截之前,否则不生效
- 如果拦截使用了具体的 /user/add 和 /user/update(注释部分),则 授权 与 拦截 顺序无影响
-
重启测试
- 当登录的用户没有权限时,无法进入 /user/add 页面,显示 401 错误(未授权错误)。
- /user/update 页面则不需要权限,即可以进入
测试成功!
-
设置未授权页面
-
在 controller 中配置
@RequestMapping("/noauth") @ResponseBody public String unauthorized() { // 注:正常情况下,这里会跳转到一个未授权页面,这里仅做测试 return "用户未经授权,无法访问此页面!"; }
-
在 config 中配置
// 设置未授权页面 bean.setUnauthorizedUrl("/noauth");
-
-
测试
登陆后进入 add 页面,显示设置的 ”用户未经授权,无法访问此页面!” 信息,测试成功!
-
设置权限
-
在数据库中添加权限列(perms),并设置测试值
- 张三: 有 user/add 权限
- 李四:有 user/update 权限
-
修改 pojo 文件夹下的 User 实体类
@Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; private String perms; }
-
修改认证中的密码认证时,传递 user,即 SimpleAuthenticationInfo 传递的第一个参数
// 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了认证 ==> doGetAuthenticationInfo 方法"); UsernamePasswordToken userToken = (UsernamePasswordToken) token; // 连接真实数据库,获取用户 User user = userService.queryUserByName(userToken.getUsername()); if (user == null) {// 没有查到这个人 return null; } // 密码认证,shiro 去做 return new SimpleAuthenticationInfo(user,user.getPwd(), ""); }
-
在授权中拿到当前的 user ,并设置当前用户的权限。
// 授权 @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();// 拿到User对象 // 设置当前用户的权限 info.addStringPermission(currentUser.getPerms()); return info; }
-
-
重启测试
- 当用户 张三 登录时,能进入 add 页面(有权限),无法进入 update 页面(无权限)
- 当用户 李四 登录时,不能进入 add 页面(无权限),能进入 update 页面(有权限)
测试成功!
6、Shiro 整合 Thymeleaf
-
导入整合所需要的
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
-
在 Config 中配置
// 整合 ShiroDialect : 用来整合 Shiro Thymeleaf @Bean public ShiroDialect getShiroDialect() { return new ShiroDialect(); }
-
修改 index.html 页面,根据不同权限显示不同内容
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> <hr> <!--游客访问 未登陆时:有登录链接 登陆后(非游客):不显示登录链接--> <div shiro:guest> <a th:href="@{/toLogin}">登录</a> </div> <!--有 user:add 权限时,显示 添加用户 链接--> <div shiro:hasPermission="user:add"> <a th:href="@{/user/add}">添加用户</a> </div> <!--有 user:update 权限时,显示 修改用户 链接--> <div shiro:hasPermission="user:update"> <a th:href="@{/user/update}">修改用户</a> </div> </body> </html>
-
测试
测试成功!
补充:
权限标签
<!--游客访问
未登陆时:有登录链接
登陆后(非游客):不显示登录链接-->
<div shiro:guest>
<a th:href="@{/toLogin}">登录</a>
</div>
<!--
user 标签:用户已通过认证\记住我\登录后显示响应的内容
-->
<div shiro:user>
<p th:text="${msg}"></p>
</div>
<!--
authenticated 标签:用户身份验证通过,即 Subjec.login 登录成功 不是记住我登录的
-->
<div shiro:authenticated>
<p th:text="${msg}"></p>
</div>
<!--
notAuthenticated 标签:用户未进行身份验证,即没有调用Subject.login进行登录,包括"记住我"也属于未进行身份验证
-->
<div shiro:notAuthenticated>
<p th:text="${msg}"></p>
</div>
<!--
notAuthenticated 标签:用户未进行身份验证,即没有调用Subject.login进行登录,包括"记住我"也属于未进行身份验证
-->
<div shiro:notAuthenticated>
<p th:text="${msg}"></p>
</div>