1.1、什么是 Shiro?
- Apache Shiro 是一个 Java 的安全(权限)框架。
- Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
- Shiro 可以完成,认证,授权,加密,会话管理,Web 集成,缓存等。
- 下载地址:Apache Shiro | Simple. Java. Security.

1.2 功能提示
- Authentication:身份认证、登录,用于验证用户是否拥有相应的身份;
- Authorization:授权,即权限验证,用于确认某个已认证的用户是否具备某项权限,进而判断其能否执行相应操作。例如,验证某个用户是否拥有某个角色,或者细粒度地验证该用户对某个资源是否具有访问权限。
- Session Manager:会话管理,即用户登录后便开启第一次会话,在退出之前,其所有信息都保存在会话中;会话既适用于普通的 JavaSE 环境,也适用于 Web 环境。
- Cryptography:加密,用于保护数据的安全性,例如将密码加密后存储到数据库中,而非明文存储。
- Web Support:Web 支持,可以非常方便地集成到 Web 环境中。
- Caching:缓存,例如用户登录后,其用户信息、角色和权限无需每次查询,从而提升系统效率。
- Concurrency:Shiro 支持多线程应用的并发验证,即在一个线程中开启另一个线程时,能够将权限自动传播过去。
- Testing:提供测试支持。
- Run As:允许一个用户以另一个用户(若被允许)的身份进行访问。
- Remember Me:记住我,这是一项非常常见的功能,即一次登录后,下次访问时无需再次登录。
1.3 Shiro架构、核心对象

- subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager;Subject其实是一个门面,SecurityManager才是实际的执行者
- SecurityManager:安全管理器,即所有与安全有关的操作都会与SecurityManager交互,并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色
- Realm:Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource;
内部架构图(供参考)

下述参考博主@True Roal写的流程图


为何从下向上开发,原因是SecurityManager会使用得到Realm对象,这样开发的时候更加直观。
2.1 非web项目中使用shiro
核心要点:
- 不用Servlet、没有HttpSession、没有ShiroFilter
- 使用
DefaultSecurityManager,依赖shiro-core - Subject 不会自动保存,多线程要手动处理
- 可以用
shiro.ini或者自定义Realm两种方式
方式1:ini配置文件方式(就是你之前看到的IniSecurityManagerFactory)
pom.xml
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>2.0.2</version>
</dependency>
<!-- 日志门面,shiro需要 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
resources/shiro.ini
[users]
admin=123456,admin
zhangsan=654321,user
[roles]
admin=user:add,user:delete
user=user:query
主程序代码
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
public class ShiroNoWebDemo {
public static void main(String[] args) {
// 1. 加载ini,创建SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// 2. 全局绑定securityManager
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取当前Subject(非web:代表当前线程用户)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("登录成功");
// 授权校验
if(subject.hasRole("admin")){
System.out.println("拥有admin角色");
}
if(subject.isPermitted("user:add")){
System.out.println("拥有user:add权限");
}
} catch (UnknownAccountException e) {
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
System.out.println("密码错误");
}
subject.logout();
}
}


shiro.ini解释:
#user的格式
用户名 = 密码, 角色1, 角色2, ...
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
# `root` 用户,密码 `secret`,拥有角色 `admin`
# `darkhelmet` 用户,密码 `ludicrousspeed`,拥有两个角色:`darklord`、`schwartz`
#roles 格式
角色名 = 权限1,权限2...
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
1. `admin = *`
- `*` 通配符:**代表拥有全部权限**
2. `schwartz = lightsaber:*`
- `lightsaber`:资源(光剑)
- `*`:任意操作
- 含义:schwartz 角色,可以对 lightsaber 做任何操作(增删改查)
3. `goodguy = winnebago:drive:eagle5`
- Shiro 权限三段式:`资源:操作:实例`
- winnebago:资源(房车)
- drive:操作(驾驶)
- eagle5:实例 ID(这辆编号 eagle5 的房车)
- 含义:goodguy 角色**只能驾驶编号 eagle5 的那一台房车**,不能开别的房车,也不能做其他操作。
## 权限通配符规则(重点)
A:B:C
- A:资源
- B:操作
- C:资源实例
- `*` 代表任意
示例:
- user:*:对 user 资源所有操作
- user:delete:只能删除用户
- user:*:1001:对 id=1001 的用户,可以做任何操作
方式2:自定义Realm方式(不用ini,企业常用)
自定义Realm
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.apache.shiro.authz.SimpleAuthorizationInfo;
public class MyRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("admin");
info.addStringPermission("user:add");
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
if("admin".equals(username)){
//用户名,密码,realm名称
return new SimpleAuthenticationInfo(username,"123456",getName());
}
return null;
}
}
主程序
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
public class ShiroRealmNoWebDemo {
public static void main(String[] args) {
//1. 创建SecurityManager + 绑定Realm
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(new MyRealm());
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin","123456");
subject.login(token);
System.out.println(subject.isPermitted("user:add"));
subject.logout();
}
}
⚠️非Web项目最容易踩坑:Subject绑定在线程上
SecurityUtils.getSubject() 底层是ThreadLocal:
- 同一个线程:登录后,全程可以拿到subject
- 切换新线程(异步、MQ、定时任务):ThreadLocal失效!Subject丢失
解决办法: 登录拿到subject后,手动绑定到新线程:
Subject subject = SecurityUtils.getSubject();
Runnable runnable = new Runnable() {
@Override
public void run() {
// 把主线程的subject绑定到当前新线程
Subject threadSubject = new Subject.Builder(subject).buildSubject();
threadSubject.execute(() -> {
System.out.println(threadSubject.hasRole("admin"));
});
}
};
new Thread(runnable).start();
对比小结
| 项目类型 | SecurityManager | 依赖包 | 会话 | 拦截 |
|---|---|---|---|---|
| Web | DefaultWebSecurityManager | shiro-spring-boot-web-starter | HttpSession,cookie维持 | ShiroFilter自动拦截URL |
| 非Web | DefaultSecurityManager | shiro-core | ThreadLocal绑定线程 | 无过滤器,代码手动校验权限 |
使用场景
适合:定时任务、消息队列消费、客户端程序、命令行工具。 不适合:浏览器访问的后台系统。

可按照步骤进行检查
2.2 Shiro1和Shiro3的区别
为了尽快赶上Java生态的变化,shiro在2024年2月份停止了对于第一版的更新,转为发布了shiro2,并于最近2026年6月份发布了shiro3.新版和旧版产生了比较大的差异,如下:
DefaultWebSecurityManager,shiro2开始,不再需要手动进行创建,将由集成的springBoot默认进行创建。
ShiroFilterFactoryBean,版本1当中,这一对象承担了工厂模式的工厂的功能,即创建对象;又承担了对滤链的解析的功能。功能复杂,新版的Shiro3中,ShiroFilterChainDefinition替代了滤链解析的功能。
-
shiro.allowAccessByDefault:这是 Shiro 3 一个重大的安全默认值变更。Shiro 1.x/2.x 默认"放行一 切未配置的 URL**"** ,而 Shiro 3 默认改为"拒绝一切未显式配置的 URL" 。这意味着,在 Shiro 3 中,你必须 在ShiroFilterChainDefinition里把登录页、静态资源等路径明确设为anon,否则默认全部拦截。如果你确实依赖"默认放行"的旧逻辑,才需要加shiro.allowAccessByDefault=true来恢复。 -
shiro.caseInsensitive:Shiro 3 默认开启了 URL大小写不敏感匹配 (例如/Admin和/admin被视为同一路径),这是为了防止因大小写处理差异导致的权限绕过。如果你的应用需要区分大小写,才需要设为false回退。
2.3 Spring Boot整合shiro框架(重点)
Shiro过滤器链顺序敏感所以采用LinkedHashMap来压入用户名和密码。(如果你使用的是老版本的话,新版本的SpringBoot4+shiro3中已经不需要了,DefaultWebFactoryBean对象将会由shiro自己默认创建,只需要完善ShiroFilterChainDefinition)
项目地址:
https://gitee.com/athanz/spring-shiro.git
package com.itheima.springshiro.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authz.Authorizer;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
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.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* 手动定义 Authorizer Bean,解决 Shiro 3 + Spring Boot 4 的启动报错
* 原因:UserRealm 间接实现了 Authorizer 接口,导致自动配置被跳过
*/
@Bean
public Authorizer authorizer() {
return new ModularRealmAuthorizer();
}
// Shiro3新版:定义URL拦截链(替代旧版ShiroFilterFactoryBean)
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
// shiro的内置过滤器
/*
* anno:无需认证就可以访问
* authc:必须认证后才可以访问
*user: 必须拥有 rememberMe 功能才能使用
* perms 拥有对某个资源的权限才能进行访问
* role 拥有某个角色的权限才可以进行访问
* */
// 首页 / 和 /index 匿名访问,不用登录
chain.addPathDefinition("/", "anon");
chain.addPathDefinition("/index", "anon");
// 登录页面 /toLogin 匿名访问,不登录才能进登录页
chain.addPathDefinition("/login", "anon");
chain.addPathDefinition("/toLogin", "anon");
// 这两个接口需要登录认证 authc
chain.addPathDefinition("/user/add", "authc");
chain.addPathDefinition("/user/update", "authc");
// 剩下所有请求,全部需要登录,必须写在最后!
chain.addPathDefinition("/**", "authc");
// 权限拦截 perms[]
chain.addPathDefinition("/user/add", "perms[user:add]");
chain.addPathDefinition("/user/update", "perms[user:update]");
// 其余所有请求,必须登录认证
chain.addPathDefinition("/**", "authc");
return chain;
}
// 创建realm对象,需要自定义 将返回值存储到指定容器当中
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
// 为thymeleaf 设置 shirodialect 的方言
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
}
package com.itheima.springshiro.config;
import com.itheima.springshiro.pojo.User;
import com.itheima.springshiro.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.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.LinkedHashMap;
import java.util.Map;
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
/**
* 模拟用户数据:用户名 -> 密码
* 使用 LinkedHashMap 保证插入顺序,便于调试
*/
// private static final Map<String, String> USERS = new LinkedHashMap<>();
//
// static {
// USERS.put("admin", "admin123");
// USERS.put("user", "user123");
// }
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println(" 执行了===》授权dogetAuthorizationInfo ");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:query");
// 拿到当前用户信息
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal();
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println(" 执行了===》认证dogetAuthorizationInfo ");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 链接真实的数据库
User user = userService.queryUserByName(userToken.getUsername());
if (user == null) {//不存在这个人
return null;
}
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginuser", user);
//密码认证 shiro自己干~ (也是考虑到了害怕密码泄露的问题)
return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}
}
package com.itheima.springshiro.controller;
import org.apache.shiro.SecurityUtils;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model) {
model.addAttribute("msg","hello,shiro");
return "index";
}
@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";
}
@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);
}catch (UnknownAccountException e){
model.addAttribute("msg2","该用户名不存在!请重新检查");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg2","密码错误");
return "login";
}
return "index";
}
@ResponseBody
@RequestMapping("/noAuth")
public String UnAuthotized(){
return "未经授权,无权访问此页面!";
}
}
package com.itheima.springshiro.mapper;
import com.itheima.springshiro.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
server:
port: 8082
shiro:
# Shiro3 默认拒绝所有没有显式配置的url
allow-access-by-default: false
case-insensitive: true
login-url: /toLogin
unauthorized-url: /noAuth
# 1. 类别名扫描包
mybatis:
type-aliases-package: com.itheima.springshiro.pojo
# 2. mapper xml文件位置
mapper-locations:
classpath:mapper/*.xml
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
cache: false
datasource:
username: root
password: admin
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/dbstudy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
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
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500