注:
本篇为 楠哥教你学Java 的 【硬核干货】2小时学会Spring Boot整合Shiro 的学习笔记。
本人水平有限,仅供参考,有所意见和建议欢迎指出。
Shiro
1、什么是Shiro
是一款主流的Java安全框架,不依赖任何容器,可以在Java SE和Java EE项目中,它的主要作用是对访问系统的用户进行身份认证、授权、会话管理、加密等操作。
Shiro就是用来解决安全管理的系统化框架。
2、Shiro核心组件
用户、角色、权限关系
会给角色赋予权限,给用户赋予角色
(1)Shiro核心组件
- UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息来创建令牌Token。
- SecurityManager,Shiro 的核心部分,负责安全认证和授权。
- Suject,Shiro 的一个抽象概念,包含了用户信息。
- Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
- AuthenticationInfo,用户的角色信息集合,认证时使用。
- AuthorzationInfo,角色的权限信息集合,授权时使用。
- DefaultWebSecurityManager,安全管理器,开发者自定义的Realm需要注入到 DefaultWebSecurityManager 进行管理才能生效。
- ShiroFilterFactoryBean,过滤器工厂,Shiro的基本运行机制时开发者定制规则,Shiro去执行,具体的执行操作就是由ShiroFilterFactoryBean创建的一个个Filter对象来完成。
(2)Shiro的运行机制
3、SpingBoot整合Shiro
(1)新建SpringBoot项目
(2)添加Shiro依赖
1.查找相应依赖版本
可以百度搜索 shiro-spring maven 查找相应依赖版本
2.添加Shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
3.验证工程
可以在终端输入命令mvn clean install
,构建工程验证;
也可以通过Maven中的插件进行验证。
注:遇到mvn不是内部或外部命令,也不是可运行的程序或批处理文件
的解决方法
首先配置环境变量:在系统变量中添加了maven的环境变量后,cmd可运行 mvn -v
查看版本,但IDEA仍提示mvn不是内部或外部命令,也不是可运行的程序或批处理文件
,
则设置IDEA管理员运行:在IDEA快捷方式的属性中设置为以管理员身份运行。
4.Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
(3)新建mysql数据库
步骤略
(4)配置application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/shiro
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
(5)新建前置条件
1.结构
2.entity
package com.example.entity;
import lombok.Data;
@Data
public class Account {
private Integer id;
private String username;
private String password;
private String perms;
private String role;
}
3.mapper
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.Account;
import org.springframework.stereotype.Repository;
/*
可加可不加
不加时,
@Autowired
private AccountMapper accountMapper;
中的accountMapper会标红,但不影响正常使用
@Repository用于将类识别为Bean,
同时它还能将所标注的类中抛出的数据访问异常封装为 Spring 的数据访问异常类型
*/
@Repository
public interface AccountMapper extends BaseMapper<Account> {
}
测试:
package com.example.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class AccountMapperTest {
@Autowired
private AccountMapper accountMapper;
@Test
void test() {
accountMapper.selectList(null).forEach(System.out::println);
}
}
4.service
package com.example.service;
import com.example.entity.Account;
import org.springframework.stereotype.Repository;
//可加可不加,这里理论上应该不加,但若要去红,加上也不影响使用
@Repository
public interface AccountService {
public Account findByUsername(String username);
}
5.serviceImpl
package com.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.entity.Account;
import com.example.mapper.AccountMapper;
import com.example.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public Account findByUsername(String username) {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
return accountMapper.selectOne(wrapper);
}
}
测试:
package com.example.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
void findByUsername() {
System.out.println(accountService.findByUsername("ls"));
}
}
(6)自定义Shiro过滤器
1.realm
package com.example.realm;
import com.example.entity.Account;
import com.example.service.AccountService;
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 AccountRealm extends AuthorizingRealm {
@Autowired
private AccountService accountService;
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 封装客户输入的用户名和密码
UsernamePasswordToken token =(UsernamePasswordToken)authenticationToken;
// 根据用户名进行查询
Account account = accountService.findByUsername(token.getUsername());
if (account != null) {
// 验证密码
return new SimpleAuthenticationInfo(account, account.getPassword(), getName());
}
return null;
}
}
2.config 配置类
package com.example.config;
import com.example.realm.AccountRealm;
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 {
/**
* 注入ShiroFilterFactoryBean
* @param defaultWebSecurityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(defaultWebSecurityManager);
return factoryBean;
}
/**
* 注入DefaultWebSecurityManager
* @param accountRealm
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("accountRealm") AccountRealm accountRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(accountRealm);
return manager;
}
/**
* 注入Realm,把创建的Realm装到ioc里面
* @return
*/
@Bean
public AccountRealm accountRealm() {
return new AccountRealm();
}
}
@Qualifier("") //表明了哪个实现类才是我们所需要的
3.编写认证和授权规则
认证过滤器:
anon:无需认证。
authc:必须认证。
authcBasic:需要通过HTTPBasic认证。
user:不一定通过认证,只要曾经被Shiro记录即可,比如:记住我。
授权过滤器:
perms:必须拥有某个权限才能访问。
role:必须拥有某个角色才能访问。
port:请求的端口必须时指定值才可以。
rest:请求必须基于RESTful,POST、PUT、GET、DELETE。
ssl:必须是安全的URL请求,协议HTTPS。
示例
创建3个页面:
1、main.html:必须登录才能访问
2、manage.html:当前用户必须用于manage授权才能访问
3、administrator.html:当前用户必须用于administrator角色才能访问
实现:
前端实现:
<link rel="shortcut icon" href="#">
用于防止读取icon文件,加上后不报错,但不加上也不影响
main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>main</h1>
</body>
</html>
manage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>manage</h1>
</body>
</html>
administrator.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>administrator</h1>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>index</h1>
<a href="/main">main</a> |
<a href="/manage">manage</a> |
<a href="/administrator">administrator</a>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<form action="/login" method="post">
<table>
<tr>
<td>用户名:</td>
<td>
<input type="text" name="username" />
</td>
</tr>
<tr>
<td>密码:</td>
<td>
<input type="password" name="password" />
</td>
</tr>
<tr>
<td>
<input type="submit" value="登录" />
</td>
</tr>
</table>
</form>
</body>
</html>
后端实现:
controller类:
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class AccountController {
@GetMapping("/{url}")
public String redirect(@PathVariable("url") String url) {
return url;
}
}
配置视图解析器:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/shiro
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
ShiroConfig.java
package com.example.config;
import com.example.realm.AccountRealm;
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;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(defaultWebSecurityManager);
//权限设置
Map<String,String> map = new HashMap<>();
map.put("/main","authc");
map.put("/manage","perms[manage]");
map.put("/administrator","roles[administrator]");
factoryBean.setFilterChainDefinitionMap(map);
//设置登录页面
factoryBean.setLoginUrl("/login");
return factoryBean;
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("accountRealm") AccountRealm accountRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(accountRealm);
return manager;
}
@Bean
public AccountRealm accountRealm() {
return new AccountRealm();
}
}
取消Shiro:
实现效果:
不符合访问条件,跳转至login.html
取消Shiro
4.登录
前端代码:
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymelear.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<form action="/login" method="post">
<table>
<span th:text="${msg}" style="color: red"></span>
<tr>
<td>用户名:</td>
<td>
<input type="text" name="username" />
</td>
</tr>
<tr>
<td>密码:</td>
<td>
<input type="password" name="password" />
</td>
</tr>
<tr>
<td>
<input type="submit" value="登录" />
</td>
</tr>
</table>
</form>
</body>
</html>
代码实现:
AccountController.java
package com.example.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class AccountController {
@GetMapping("/{url}")
public String redirect(@PathVariable("url") String url) {
return url;
}
@PostMapping("/login")
public String login(String username, String password, Model model) {
//Suject,Shiro 的一个抽象概念,包含了用户信息
//取得Subject,注意不要引错包
Subject subject = SecurityUtils.getSubject();
//封装用户登录信息
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//调用,进入到之前自定义的realm内的认证方法
try {
//登录成功
subject.login(token);
return "index";
} catch (UnknownAccountException e) { //用户名不存在
e.printStackTrace();
model.addAttribute("msg","用户名错误");
return "login";
} catch (IncorrectCredentialsException e) { //密码不存在
e.printStackTrace();
model.addAttribute("msg","密码错误");
return "login";
}
}
}
运行逻辑(可以自行打断点查看):
实现结果(登录成功后):
无权限,无角色:
5.授权
主要代码:
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取当前登录的用户信息
Subject subject = SecurityUtils.getSubject();
Account account = (Account) subject.getPrincipal();
//设置角色
Set<String> roles = new HashSet<>();
roles.add(account.getRole());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//设置权限
info.addStringPermission(account.getPerms());
return info;
}
数据库:
实现结果:
实现结果(登录成功后):
有权限,无角色:
有权限,有角色:
补充:
前端界面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymelear.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>index</h1>
<div th:if="${session.account != null}">
<span th:text="${session.account.username}+'欢迎回来'"></span>
<a href="/logout">退出</a>
</div>
<a href="/main">main</a> |
<a href="/manage">manage</a> |
<a href="/administrator">administrator</a>
</body>
</html>
修改无权限界面:
效果:
ShiroConfig:
AccountController:
欢迎回来和退出
效果:
AccountController:
XXX欢迎回来:
退出:
完整代码:
ShiroConfig:
package com.example.config;
import com.example.realm.AccountRealm;
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;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(defaultWebSecurityManager);
//权限设置
Map<String,String> map = new HashMap<>();
map.put("/main","authc");
map.put("/manage","perms[manage]");
map.put("/administrator","roles[administrator]");
factoryBean.setFilterChainDefinitionMap(map);
//设置登录页面
factoryBean.setLoginUrl("/login");
//设置未授权页面
factoryBean.setUnauthorizedUrl("/unauth");
return factoryBean;
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("accountRealm") AccountRealm accountRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(accountRealm);
return manager;
}
@Bean
public AccountRealm accountRealm() {
return new AccountRealm();
}
}
AccountController:
package com.example.controller;
import com.example.entity.Account;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class AccountController {
@GetMapping("/{url}")
public String redirect(@PathVariable("url") String url) {
return url;
}
@PostMapping("/login")
public String login(String username, String password, Model model) {
//Suject,Shiro 的一个抽象概念,包含了用户信息
//取得Subject,注意不要引错包
Subject subject = SecurityUtils.getSubject();
//封装用户登录信息
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//调用,进入到之前自定义的realm内的认证方法
try {
//登录成功
subject.login(token);
Account account = (Account) subject.getPrincipal();
subject.getSession().setAttribute("account",account);
return "index";
} catch (UnknownAccountException e) { //用户名不存在
e.printStackTrace();
model.addAttribute("msg","用户名错误");
return "login";
} catch (IncorrectCredentialsException e) { //密码不存在
e.printStackTrace();
model.addAttribute("msg","密码错误");
return "login";
}
}
@GetMapping("/unauth")
@ResponseBody
public String unauth() {
return "未授权,无法访问!";
}
@GetMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
}
(7)Shiro整合Thymeleaf
1、pom.xml引入依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2、配置类添加ShiroDialect
ShiroConfig.java:
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
3、index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymelear.org"
xmlns:shiro="http://www.thymelear.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>index</h1>
<div th:if="${session.account != null}">
<span th:text="${session.account.username}+'欢迎回来'"></span>
<a href="/logout">退出</a>
</div>
<a href="/main">main</a>
<div shiro:hasPermission="manage">
<a href="/manage">manage</a>
</div>
<div shiro:hasRole="administrator">
<a href="/administrator">administrator</a>
</div>
</body>
</html>
实现结果: