# shiro
# 1. 入门概述
# 1.1. 是什么
Apache Shiro 是 Java 安全(权限)框架。
功能:
- 认证
- 授权
- 加密
- 会话管理
- 与 Web 集成
- 缓存
# 1.2. 为什么要用 Shiro
- 易于使用
- 全面
- 灵活
- 强力支持 Web
- 兼容性强
# 1.3. 与 Spring Security 的对比
- Spring Security 基于 Spring,更方便、强大,社区资源也更好
- Shiro 不依赖 Spring,在集群中 会话 独立于容器
# 1.4. 基本功能



# 2. 基本使用
# 2.1. 环境准备
创建普通 Java 工程(选择 Maven 构建)
添加依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.9.0</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
# 2.2. ini 文件
Shiro 获取权限相关信息可以通过数据库,也可以通过 ini 配置文件
位置:
${root}/
src/main/resources
shiro.ini
shiro.ini:
[users]
zhangsan=123
lisi=456
# 2.3. 登录认证
# 2.3.1. 登录认证概念
身份验证
- 一般需要提供标志信息来表明登录者的身份
- 比如 email 、 用户名/密码
principals / credentials
- principals:
/ˈprɪnsəpəlz/, 身份 - credentials:
/krəˈdenʃlz/, 证明 - 在 Shiro 中,用户需要提供 身份/证明 来验证用户身份
- principals:
principals
- 身份,即主体的标识属性
- 比如: 用户名、邮箱、手机号(唯一即可)
- 但只有一个 primary principals
credentials
- 证明 / 凭证
- 只有主体知道的安全值
- 比如: 密码 、数字证书
最常见的 principals/credentials 组合就是 用户名/密码
# 2.3.2. 登录认证基本流程
- 收集用户 身份/凭证,比如 用户名/密码
- 调用 Subject.login() 登录
- 登录失败则抛异常 AuthenticationException
- 创建自定义 Realm 类
- 继承 AuthenticatingRealm 类
- 实现 doGetAuthenticationInfo() 方法

# 2.3.3. 登录认证实例
// 1. 初始化: 创建 securityManager
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
// securityManager 放入 SecurityUtils
SecurityUtils.setSecurityManager(securityManager);
// 2. 获取 Subject 对象
Subject subject = SecurityUtils.getSubject();
// 3. 创建 token 对象
AuthenticationToken token = new UsernamePasswordToken("zhangsan", "123");
try {
// 4. 登录
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
System.out.println("登录失败: 用户名不存在");
} catch (IncorrectCredentialsException e) {
System.out.println("登录失败: 密码错误");
} catch (AuthenticationException e) {
System.out.println("登录失败: 未知错误");
}
# 2.4. 角色 和 授权
# 2.4.1. 授权概念
授权
- 访问控制,即在应用中控制谁能访问的哪些资源
- 关键对象:主体(subject)、资源(resource)、权限(Permission)、角色(role)
主体(subject)
- 访问应用的用户
- 在 shiro 中,Subject 代表该用户
- 用户只有授权后才允许访问相应资源
资源(resource)
- 用户可以访问的 URL
权限(permission)
- 在应用中用户能不能访问某个资源
角色(role)
- 权限的集合
- 一般会赋予用户角色,不同的角色拥有一组不同的权限
# 2.4.2. 授权方式
编程:
if (subject.hasRole("admin")) {
// 有权限
} else {
// 无权限
}
注解:
// 在方法上放置相应注解,没有权限则抛出相应异常
@RequiresRoles("admin")
@PostMapping("/user/getById")
public JsonResult getById(String id) {
// 有权限
}
# 2.4.3. 授权流程
先调用
subject.hasRole()/subject.isPermitted()- 委托给 SecurityManager
- SecurityManager 委托给 Authorizer
Authorizer 是真正的授权者
- 比如调用
subject.isPermitted("user:insert") - PermissionResolver 把
"user:insert"转为相应 Permission 实例
- 比如调用
在授权前,会调用相应的 Realm 获取 subject 的 角色和权限
Authorizer 会判断 Realm 的 角色/权限 是否与传入的匹配
# 2.4.4. 授权实例
shiro.ini :
[users]
zhangsan=123,role1,role2
lisi=456
[roles]
role1=user:insert,user:select
代码:
try {
// 4. 登录
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
System.out.println("登录失败: 用户名不存在");
} catch (IncorrectCredentialsException e) {
System.out.println("登录失败: 密码错误");
} catch (AuthenticationException e) {
System.out.println("登录失败: 未知错误");
}
// 5. 判断角色: 用户 是否 有指定的角色
boolean hasRole1 = subject.hasRole("role1");
System.out.println("hasRole1 = " + hasRole1);
// 6. 判断权限: 用户 是否 有指定的权限
boolean isPermittedOfUserInsert = subject.isPermitted("user:insert");
System.out.println("isPermittedOfUserInsert = " + isPermittedOfUserInsert);
// 检查权限,没有则抛异常(UnauthorizedException)
subject.checkPermission("user:insert2");
# 2.5. MD5 加密
使用 Shiro 的加密工具类,实现方便的加密
String password = "123456";
// md5 加密
Md5Hash md5Hash1 = new Md5Hash(password);
System.out.println("md5Hash1 = \t" + md5Hash1.toHex());
// md5 带盐加密
Md5Hash md5Hash2 = new Md5Hash(password, "salt-test");
System.out.println("md5Hash2 = \t" + md5Hash2.toHex());
// md5 带盐 迭代 加密
Md5Hash md5Hash3 = new Md5Hash(password, "salt-test", 3);
System.out.println("md5Hash3 = \t" + md5Hash3.toHex());
// (使用 Md5Hash 的父类) md5 带盐 迭代 加密
SimpleHash md5Hash4 = new SimpleHash("MD5", password, "salt-test", 3);
System.out.println("md5Hash4 = \t" + md5Hash4.toHex());
# 2.6. 自定义 Ream
略
# 3. 与 Spring Boot 整合
# 3.1. 框架整合
目录:
shiro_02_spring/src/
main/java
org.example
mapper/
ShiroApplication.java
resources/
application.yml
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>shiro_02_spring</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>
<!--JDK 的版本-->
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</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>
</plugin>
</plugins>
</build>
</project>
application.yml:
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro_db?serverTimezone=UTC
username: root
password: 123456
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
shiro:
loginUrl: /login
ShiroApplication.java:
package org.example;
@SpringBootApplication
@MapperScan("org.example.mapper")
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
}
# 3.2. 登录认证实现
# 3.2.1. 数据库
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint NOT NULL,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`role_id` bigint NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO `sys_user` VALUES (1, 'ZhangSan', '3dda874af7bb9cb770722e15ea03c1d4', NULL);
INSERT INTO `sys_user` VALUES (2, 'LiSi', '83b5be425a1adcfba658d228716593e3', NULL);
# 3.2.2. 实体
package org.example.entity;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_user")
public class User {
private Long id;
private String username;
private String password;
private Long roleId;
}
# 3.2.3. Mapper
package org.example.mapper;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
# 3.2.4. Service
package org.example.service;
public interface UserService {
User getUserInfoByUsername(String username);
}
package org.example.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserInfoByUsername(String username) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(username != null, User::getUsername, username);
User user = userMapper.selectOne(queryWrapper);
return user;
}
}
# 3.2.5. 自定义 Realm
package org.example.realm;
@Component
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 登录
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 身份(用户名)
Object principal = authenticationToken.getPrincipal();
String username = principal.toString();
// 获取数据库中的密码
User user = userService.getUserInfoByUsername(username);
if (user == null) {
return null;
}
String password = user.getPassword();
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
principal,
password,
ByteSource.Util.bytes("salt-test"), // 盐值
username
);
return authenticationInfo;
}
}
# 3.2.6. 配置 Shiro
package org.example.config;
@Configuration
public class ShiroConfig {
@Autowired
private MyShiroRealm myShiroRealm;
// 配置 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 加密对象,设置密码加密方式
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// md5 算法
matcher.setHashAlgorithmName("md5");
// 迭代 3 次
matcher.setHashIterations(3);
myShiroRealm.setCredentialsMatcher(matcher);
defaultWebSecurityManager.setRealm(myShiroRealm);
return defaultWebSecurityManager;
}
// 配置 Shiro 内置过滤器 拦截的范围
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
// 匿名
definition.addPathDefinition("/sysUser/login", "anon");
// 认证
definition.addPathDefinition("/**", "authc");
return definition;
}
}
# 3.2.7. controller
package org.example.controller;
@RestController
public class UserController {
@GetMapping("/sysUser/login")
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return "Login success";
} catch (AuthenticationException e) {
e.printStackTrace();
return "Login fail";
}
}
}
# 3.2.8. 测试
GET http://localhost:8080/sysUser/login?username=ZhangSan&password=123
GET http://localhost:8080/sysUser/login?username=LiSi&password=456
# 3.3. 多个 realm
略
# 3.4. remember me
略
# 3.5. 登出
// 配置 Shiro 内置过滤器 拦截的范围
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
// 匿名
definition.addPathDefinition("/sysUser/login", "anon");
// 登出
definition.addPathDefinition("/logout", "logout");
// 认证
definition.addPathDefinition("/**", "authc");
return definition;
}
# 3.6. 授权、角色认证
# 3.6.1. 授权
用户登录后,需要验证其 角色、权限
涉及方法: AuthorizingRealm.doGetAuthorizationInfo()
触发判断的方式: 调用的方法上 使用了 @RequiresXxx 注解
# 3.6.2. 注解
一般在 控制器方法 上使用注解
@RequiresAuthentication- 是否登录
- 等同
subject.isAuthenticated()
@RequiresUser- 是否被记忆
- 等同
subject.isRemembered()
@RequiresGuest- 是否是游客
subject.getPrincipal()为 null
@RequiresRoles({"admin"})- 是否是 admin 角色
@RequiresPermissions({"read:file", "write:file"})- 必须同时有
"read:file","write:file"权限
- 必须同时有
# 3.6.3. 角色验证
MyShiroRealm:
package org.example.realm;
@Component
public class MyShiroRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1. 获取用户名
String username = principalCollection.getPrimaryPrincipal().toString();
System.out.println("doGetAuthorizationInfo. username = " + username);
// 2. 根据用户名查询角色
List<String> roleList = Arrays.asList("admin", "employee" /*, "boss"*/);
// 3. 将 角色 添加进 AuthorizationInfo 并返回
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(roleList);
return simpleAuthorizationInfo;
}
// 登录
// ...
}
UserController:
@GetMapping("/sysUser/callMethodRequiresAdminRole")
@RequiresRoles("admin")
@ResponseBody
public String callMethodRequiresAdminRole() {
System.out.println("访问需要 admin 角色的方法");
return "OK";
}
// 无 boss 角色,则抛出 AuthorizationException 异常
@GetMapping("/sysUser/callMethodRequiresBossRole")
@RequiresRoles("boss")
@ResponseBody
public String callMethodRequiresBossRole() {
System.out.println("访问需要 boss 角色的方法");
return "OK";
}
# 3.6.4. 权限验证
MyShiroRealm:
package org.example.realm;
@Component
public class MyShiroRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1. 获取用户名
String username = principalCollection.getPrimaryPrincipal().toString();
System.out.println("doGetAuthorizationInfo. username = " + username);
// 2.1 根据用户名查询角色
List<String> roleList = Arrays.asList("admin", "employee" /*, "boss"*/);
// 2.2 根据角色查询权限
List<String> permissionList = Arrays.asList("user:add", "user:edit"/*, "user:delete"*/);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 3.1 将 角色 添加进 AuthorizationInfo
simpleAuthorizationInfo.addRoles(roleList);
// 3.2 将 角色 添加进 AuthorizationInfo
simpleAuthorizationInfo.addStringPermissions(permissionList);
return simpleAuthorizationInfo;
}
// 登录
// ...
}
UserController:
@GetMapping("/sysUser/callMethodRequiresAddPermission")
@RequiresPermissions("user:add")
@ResponseBody
public String callMethodRequiresAddPermission() {
System.out.println("访问需要 user:add 权限的方法");
return "OK";
}
// 无 user:delete 权限,则抛出 AuthorizationException 异常
@GetMapping("/sysUser/callMethodRequiresDeletePermission")
@RequiresPermissions("user:delete")
@ResponseBody
public String callMethodRequiresDeletePermission() {
System.out.println("访问需要 user:delete 权限的方法");
return "OK";
}
# 3.6.5. 全局异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(AuthorizationException.class)
public String handleAuthorizationException() {
return "无权限";
}
}
# 3.7. 缓存
说明:
- 每次调用需要 授权 的方法,都会执行一次
AuthorizingRealm.doGetAuthorizationInfo() - 使用缓存
defaultWebSecurityManager.setCacheManager()可以避免频繁获取 授权
# 3.8. 会话管理
说明:
- 在 shiro 过滤器中对 request 做了封装(ShiroHttpServletRequest)
- 通过 subject.getSession() / request.getSession() 获取到的 session, 两者是等价的
示例:
@GetMapping("/sysUser/login")
public String login(String username, String password, HttpSession session, HttpServletRequest request) {
Subject subject = SecurityUtils.getSubject();
Session sessionFromShiro = subject.getSession();
HttpSession sessionFromRequest = request.getSession();
System.out.println("request = " + request);
//=> org.apache.shiro.web.servlet.ShiroHttpServletRequest
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
User user = userService.getUserInfoByUsername(username);
session.setAttribute("user", user);
System.out.println("sessionFromShiro.getAttribute(user) = " + sessionFromShiro.getAttribute("user"));
System.out.println("sessionFromRequest.getAttribute(user) = " + sessionFromRequest.getAttribute("user"));
try {
subject.login(token);
return "redirect:/main";
} catch (AuthenticationException e) {
e.printStackTrace();
return "login";
}
}
上一篇: 下一篇: