Spring Boot 整合Shiro
在Shiro入门我们已经基本了解了Shiro,下面我们就进行Spring Boot和Shiro 的整合。
一、准备工作
环境配置
springboot 2.4.4
maven 3.6.1
创建工程
我们在之前学习shiro的项目里创建一个子项目shiro-springboot
创建springboot项目
我们只需要导入web模块
二、导入相关依赖、测试运行环境
<!--thymeleaf xmlns:th="http://www.thymeleaf.org-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
测试springboot运行环境这里还没有开始整合
在编写resources下的templates编写index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
</body>
</html>
创建Controller层编写MyController类进行测试
package com.zhao.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello Shiro");
return "index";
}
}
测试成功了,我们就可以开始整合了
三、整合Shiro
shiro三个核心的对象
1.Subject :用户
2.SecurityManager : 管理所有用户
3.Realm :连接数据
1、导入shiro相关依赖
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
2、编写配置类
1)在config层创建一个ShiroConfig类(配置类)
package com.zhao.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
}
下面我们我就要创建shiro的三个核心对象
ShiroFilterFactoryBean(Subject)、DafaultWebSecurityManager(SecurityManager)、realm对象 。他们三个的配置顺序是倒着来的。realm->DafaultWebSecurityManager->ShiroFilterFactoryBean
其中realm对象需要我们执行自定义
2)在config层创建一个UserRealm类(自定义Realm类)
创建好UserRealm类我们只需要继承AuthorizingRealm
类并重写其方法。
package com.zhao.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;
}
}
3)在ShiroConfig创建UserRealm对象注入到Spring中
package com.zhao.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
//3.ShiroFilterFactoryBean
//2.DafaultWebSecurityManager
//1.创建realm对象 需要自定义
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
4)创建管理SecurityManager关联Realm
使用@Qualifier("userRealm")
进行与下面的userRealm绑定,默认就是与类名相同
package com.zhao.config;
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
//2.DafaultWebSecurityManager
@Bean
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();
}
}
5)创建ShiroFilterFactoryBean 进行绑定DefaultWebSecurityManager
上面说了@Bean
注入到spring默认的是方法名, 但是我这名字太长了需要起一个别名 @Bean(name = "securityManager")
,然后在用@Qualifier("securityManager")
进行绑定。
package com.zhao.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.DafaultWebSecurityManager
@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();
}
}
3、编写测试页面及跳转路径
在templates 下创建user及add.html和update.html
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<FORM action="">
<p>用户名:<input type="text" name="username"/></p>
<p>密 码:<input type="text" name="passwprd"/></p>
<p><input type="submit"/></p>
</FORM>
</body>
</html>
add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>
update.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>update</h1>
</body>
</html>
在index.html中加入一个跳转
<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
在之前的MyController添加如下代码
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
@RequestMapping("toLogin")
public String toLogin(){
return "login";
}
四、shiro实现登录拦截
在ShiroConfig中的第三步,getShiroFilterFactoryBean
方法加入下列代码
//3.ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
anon:无需认证就可以访问
authc: 必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
//拦截
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;
}
然后进行测试
我们进行跳转的时候会自己动调到登录页面,因为我们没有进行登录认证
五、shiro实现用户认证
1.在MyController添加登录方法
1.创建shiro中的Subject对象获取当前用户
2.将用户名、密码封装成令牌(token)
3.执行登录方法
4.捕获其异常情况返回给页面
@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";
}
}
2.在登录页面login.html中加入登录地址以及接收反馈信息
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${msg}" style="color:red"></p>
<FORM th:action="@{/login}">
<p>用户名:<input type="text" name="username"/></p>
<p>密 码:<input type="text" name="password"/></p>
<p><input type="submit"/></p>
</FORM>
</body>
</html>
到这里我们可以运行一下程序他是可以进入后台方法去认证的,只是认证环节我们还没有写
3.编写UserRealm中的认证
1.我们先伪造一个用户名和密码,用于测试
2.获取token并强转换成我们上面使用过的UsernamePasswordToken
类型
3.进行验证用户名,用户名不同返回null,shiro就会自动抛出UnknownAccountException
异常
4.shiro为了防止密码泄露,它帮助我们进行密码验证,我们只需要传入密码SimpleAuthenticationInfo("",password,"")
(别两个参数为获取当前用户的认证和认证名)
//认证
@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;//抛出异常 UnknownAccountException
}
//密码认证:shiro做
return new SimpleAuthenticationInfo("",password,"");
}
下面我们就可以测试登录了
六、shiro整合Mybatis
这里我们整合Mybatis使用Shiro
注意:我们没有换项目还是上面的项目
1.导入依赖
这里我们使用了Druid数据源(复习之前学习的知识,springboot整合Druid)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
<!--MySQL驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--数据源druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<!---->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
2.编写application.yml
配置了Druid数据源和mybatis(注意yml文件的格式要求注意空格)
spring:
datasource:
username: root
password: x5
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.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
#整合Mybatis
mybatis:
type-aliases-package: com.zhao.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
3.编写User类(pojo)
package com.zhao.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
4.编写UserMapper类(dao/mapper)
package com.zhao.mapper;
import com.zhao.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
5.编写UserMapper.xml(mapper.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.zhao.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="User">
select * from user where name =#{name}
</select>
</mapper>
6.编写UserService接口及其实现类UserServiceImpl(service 和 serviceImpl)
UserService代码
package com.zhao.service;
import com.zhao.pojo.User;
public interface UserService {
public User queryUserByName(String name);
}
UserServiceImpl代码
package com.zhao.service;
import com.zhao.mapper.UserMapper;
import com.zhao.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);
}
}
7.测试搭建环境
package com.zhao;
import com.zhao.service.UserService;
import com.zhao.service.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ShiroSpringbootApplicationTests {
@Autowired
UserServiceImpl userService;
@Test
void contextLoads() {
System.out.println(userService.queryUserByName("张三"));
}
}
看到我们查询的结果,就说名我们搭建成功了
对于Druid和mybatis整合springboot不太了解的小伙伴可以看springboot整合Druid和springboot整合Mybatis
在运行的时候可能会出现一些错误
mysql驱动版本和自己使用的数据库不匹配(太高或太低)
数据库驱动名不同版本不一样
mapper 文件中的路径 不能有多余的空格
配置文件中的各个路径
8.修改UserRealm
添加UserService属性
查询用户信息进行密码验证
package com.zhao.config;
import com.zhao.pojo.User;
import com.zhao.service.UserService;
import org.apache.shiro.SecurityUtils;
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");
//用户名,密码~
/*String name ="root";
String password="123456";*/
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//数据库查询真实数据
User user = userService.queryUserByName(userToken.getUsername());
if (user==null){//没有这个人
return null;
}
//密码认证:shiro做
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
}
到这里我们就可以进行登录认证了
七、shiro请求授权实现
下面我们进行授权操作
1.给页面设置权限
在ShiroConfig
的getShiroFilterFactoryBean
方法中加入一行代码
有授予
perms[user:add]
权限的用户才能访问/user/add
页面
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/add","perms[user:update]");
现在我们运行一下看看没有权限的话会出现什么错误?
正常情况下,没有授权会跳转到未授权页面(这种报错页面不能给用户看)
2.设置未授权页面
在MyController中添加未授权路径方法
@RequestMapping("/noAuth")
@ResponseBody
public String noAuth(){
return "未经授权,无法访问!";
}
然后ShiroConfig
的getShiroFilterFactoryBean
方法中加入一行代码```java
//设置未授权的请求
bean.setUnauthorizedUrl("/noAuth");
效果
3.对用户进行授权
在UserRealm中doGetAuthorizationInfo方法中添加如下代码进行对用户授权
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权+doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");
return info;
}
进行测试 现在所有用户都可以访问add页面了
但是这不是我们想要的
我们想要的是不同用户有不同的访问权限
4.修改数据库字段和实体类信息
数据库添加字段
给用户设置不同权限
实体类
package com.zhao.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
private String perms;
}
5.获取用户的权限信息
package com.zhao.config;
import com.zhao.pojo.User;
import com.zhao.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;
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对象
User currentUser = (User) subject.getPrincipal();
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证+doGetAuthenticationInfo");
//用户名,密码~
/*String name ="root";
String password="123456";*/
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//数据库查询真实数据
User user = userService.queryUserByName(userToken.getUsername());
if (user==null){//没有这个人
return null;
}
//密码认证:shiro做
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
到这里就可以进行测试了
这里李四就只可以访问update页面,不可以访问add页面
效果不好展示 大家就自行测试 (获取脑补画面(*_^))
八、shiro整合thymeleaf
这里的整合是使我们的一面动态显示,只显示有用的信息
例如:李四只有添加的权限,我们就只显示添加功能即可
1.导入整合依赖
<!--整合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>
2.在config下的ShiroConfig中添加如下代码
//整合ShiroDialect:用来整合 shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
3.编写页面代码(设置功能显示)
添加标签xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
shiro:hasPermission="user:add"
判断用户是否有此权限有显示没有隐藏
<!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</title>
</head>
<body>
<h1>首页</h1>
<p>
<a th:href="@{/toLogin}">登录</a>
</p>
<p th:text="${msg}"></p>
<hr />
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
4.设置登录显示
登录后不显示登录按钮
首先在config下UserRealm中的doGetAuthenticationInfo方法中获取用户信息保存到session中
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginUser",user);
然后在index页面中修改登录按钮
th:if="${session.loginUser==null}"
根据session的值判断是否登录,登录了隐藏否则显示
<div th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登录</a>
</div>
效果如下图