Shiro 安全框架概述

发布于 2026-09-06 22:03 更新于 2026-10-13 17:14 3183 字 16 min read ... 访问量 AI 审阅 文章内容已由 AI 审阅过,但仍需谨慎判断。 AI 改写 文章内容主要由他人撰写,AI 帮我改写或润色。

AI 审阅 文章内容已由 AI 审阅过,但仍需谨慎判断。

本文介绍了 Apache Shiro 安全框架的核心功能与架构,重点讲解了其在前后端分离项目中结合 JWT 实现无状态认证的完整流程。通过自定义 Token、过滤器、Realm 和全局配置,Shiro 能够在不依赖服务端会话的情况下完成用户认证与权限校验,并支持注解式授权,实现灵活、轻量的安全控制。同时指出,真实项目需从数据库加载用户信息,避免明文密码存储,并需结合令牌失效机制保障安全性。

Shiro 安全框架概述

Shiro 核心概述

Shiro 简介

Apache Shiro 是一个轻量级、灵活的 Java 安全框架,主要用于处理认证、授权、会话管理和密码学相关功能。与 Spring Security 相比,Shiro 的核心 API 较为直观,对具体容器和 Web 框架的依赖较少,因此常用于中小型项目和后台管理系统中的权限控制。

Shiro 四大核心功能

  • Authentication(认证):确认当前用户是谁,例如校验用户名和密码。
  • Authorization(授权):判断当前用户能够执行哪些操作,例如角色校验和资源权限校验。
  • Session Management(会话管理):管理用户会话的创建、读取和销毁。前后端分离项目既可以使用 Session,也可以根据需要改造成 JWT 等无状态方案。
  • Cryptography(密码学支持):提供哈希、编码等密码学相关工具。实际项目中保存密码时通常还需要配合随机盐和专门的密码哈希方案。

Shiro 核心架构

image-001
image-001

核心组件

  1. Subject(主体):表示当前与系统交互的用户或程序,是业务代码使用 Shiro 安全能力的主要入口。
  2. SecurityManager(安全管理器):Shiro 的核心调度器,负责协调认证、授权、会话等安全组件。
  3. Realm(域):Shiro 与应用安全数据之间的桥梁,可以从数据库、缓存或其他数据源中加载用户、角色和权限信息。一个应用可以配置一个或多个 Realm。
  4. Authenticator(认证器):负责组织认证流程,并调用 Realm 获取认证信息。
  5. Authorizer(授权器):负责角色和权限判断,并调用 Realm 获取授权信息。
  6. SessionManager(会话管理器):负责 Shiro Session 的生命周期管理;无状态方案中可以关闭 Session 存储。
  7. CacheManager(缓存管理器):用于缓存认证或授权相关数据,减少重复查询。

环境搭建

核心依赖

pom.xml 中可以引入 Spring Boot、Shiro 和 JWT 相关依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
    <relativePath/>
</parent>

<properties>
    <java.version>17</java.version>
    <jjwt.version>0.12.5</jjwt.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Shiro -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-web-starter</artifactId>
        <version>1.13.0</version>
    </dependency>

    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>${jjwt.version}</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>${jjwt.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>${jjwt.version}</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.31</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.52</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

前后端分离中的无状态方案

前后端分离并不等于必须放弃 Session。本笔记采用的是 Shiro + JWT 无状态认证方案,因此需要进行以下适配:

  • 登录成功后由后端签发 JWT,前端保存 Token。
  • 后续请求在请求头中携带 Token。
  • 自定义 Shiro 过滤器读取 Token,并交给 Shiro 完成认证。
  • 自定义 Realm 负责校验 JWT 并提供用户身份信息。
  • 关闭 Shiro 的 Session 存储,避免依赖服务端会话保存登录状态。

Shiro 无状态 JWT 认证实现

本章按照实际请求链路依次实现自定义 AuthenticationToken、认证过滤器、Realm 和全局配置类。

自定义登录 Token

AuthenticationToken 是 Shiro 的认证凭证接口,用于封装认证过程中需要提交给 Shiro 的身份信息和凭据信息,是 subject.login(token) 的参数类型之一。

在本例中,JWT 字符串同时作为 principal 和 credentials,由自定义 ShiroToken 进行封装:

package com.hyxy.sdemo.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * 前后端分离场景中使用 JWT 作为认证凭证。
 */
public class ShiroToken implements AuthenticationToken {

    private final String token;

    public ShiroToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

ShiroToken 只负责保存认证数据,不负责校验 Token 是否有效。真正的认证逻辑仍然由 Realm 完成。

自定义 Shiro 过滤器

过滤器负责拦截需要认证的请求,从 Authorization 请求头中读取 JWT,然后调用 Shiro 的登录流程。

package com.hyxy.sdemo.shiro;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 从请求头中读取 JWT,并交给 Shiro 认证。
 */
public class ShiroAuthFilter extends AuthenticatingFilter {

    private static final String TOKEN_HEADER = "Authorization";

    @Override
    protected AuthenticationToken createToken(
            ServletRequest request,
            ServletResponse response) {
        String token = getRequestToken((HttpServletRequest) request);
        if (StrUtil.isBlank(token)) {
            return null;
        }
        return new ShiroToken(token);
    }

    @Override
    protected boolean onAccessDenied(
            ServletRequest request,
            ServletResponse response) throws Exception {
        String token = getRequestToken((HttpServletRequest) request);
        if (StrUtil.isBlank(token)) {
            responseJson(
                    response,
                    HttpStatus.UNAUTHORIZED.value(),
                    "用户未登录,请先登录"
            );
            return false;
        }

        // 有 Token 时执行登录,随后进入 Realm 校验 JWT。
        return executeLogin(request, response);
    }

    @Override
    protected boolean onLoginFailure(
            AuthenticationToken token,
            AuthenticationException e,
            ServletRequest request,
            ServletResponse response) {
        try {
            String msg = e.getMessage() != null ? e.getMessage() : "认证失败";
            responseJson(response, HttpStatus.UNAUTHORIZED.value(), msg);
        } catch (IOException ignored) {
        }
        return false;
    }

    /**
     * 读取 Authorization: Bearer xxx。
     */
    private String getRequestToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_HEADER);
        if (StrUtil.isNotBlank(token) && token.startsWith("Bearer ")) {
            return token.substring(7);
        }
        return token;
    }

    private void responseJson(ServletResponse response, int code, String msg)
            throws IOException {
        HttpServletResponse httpResp = (HttpServletResponse) response;
        httpResp.setContentType("application/json;charset=UTF-8");
        httpResp.setStatus(code);

        Map<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        httpResp.getWriter().write(JSON.toJSONString(result));
    }

    /**
     * 放行跨域预检 OPTIONS 请求。
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response)
            throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpRequest.getMethod())) {
            return true;
        }
        return super.preHandle(request, response);
    }
}

自定义 Realm

Realm 负责两类工作:认证时校验 JWT,授权时提供当前用户的角色和权限。

package com.hyxy.sdemo.shiro;

import cn.hutool.core.util.StrUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;

/**
 * 自定义 Realm:认证时校验 JWT,授权时加载角色和权限。
 */
@Component
public class CustomShiroRealm extends AuthorizingRealm {

    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 当前 Realm 只处理自定义 ShiroToken。
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof ShiroToken;
    }

    /**
     * 授权:访问带角色或权限要求的接口时触发。
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 教学示例:暂时写死 admin 的角色和权限。
        if ("admin".equals(username)) {
            Set<String> roles = new HashSet<>();
            roles.add("admin");
            info.setRoles(roles);

            Set<String> permissions = new HashSet<>();
            permissions.add("order:list");
            permissions.add("order:add");
            info.setStringPermissions(permissions);
        }

        return info;
    }

    /**
     * 认证:过滤器调用登录流程时触发。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        String jwt = (String) token.getPrincipal();

        if (StrUtil.isBlank(jwt) || !jwtUtil.verify(jwt)) {
            throw new AuthenticationException("Token 无效或已过期,请重新登录");
        }

        String username = jwtUtil.getUsername(jwt);

        // principal 保存用户名,credentials 保存当前 JWT。
        return new SimpleAuthenticationInfo(username, jwt, getName());
    }
}

Shiro 全局配置

全局配置负责注册 Realm、过滤器链和注解式授权支持,并关闭 Shiro Session 存储。

package com.hyxy.sdemo.config;

import com.hyxy.sdemo.shiro.CustomShiroRealm;
import com.hyxy.sdemo.shiro.ShiroAuthFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro 无状态 JWT 配置。
 */
@Configuration
public class ShiroConfig {

    /**
     * 绑定 Realm,并关闭 Shiro Session 存储。
     */
    @Bean
    public DefaultWebSecurityManager securityManager(
            CustomShiroRealm customShiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customShiroRealm);

        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(evaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * 登录接口匿名访问,其余请求进入自定义 auth 过滤器。
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(
            DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filterMap = factoryBean.getFilters();
        filterMap.put("auth", new ShiroAuthFilter());

        // LinkedHashMap 保证规则按插入顺序匹配。
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/**", "auth");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);

        return factoryBean;
    }

    /**
     * 开启 @RequiresPermissions、@RequiresRoles 等注解支持。
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor =
                new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

Shiro 认证体系

JWT 无状态认证流程

本例中的认证流程如下:

  1. 用户向登录接口提交账号和密码。
  2. 后端校验账号和密码,校验成功后签发 JWT。
  3. 前端保存 JWT。
  4. 后续请求通过 Authorization 请求头携带 JWT。
  5. ShiroAuthFilter 将 JWT 封装为 ShiroToken,并执行 Shiro 登录流程。
  6. CustomShiroRealm#doGetAuthenticationInfo() 校验 JWT,并将用户名作为 principal 保存到当前 Subject。
  7. 请求继续进入受保护接口;如果接口还要求角色或权限,则继续执行授权检查。

登录接口

下面的代码使用写死账号模拟数据库校验。这里的 subject.login(new ShiroToken(token)) 只是在当前请求中再次让 Shiro 校验刚生成的 JWT;由于 Session 存储已经关闭,它不会建立可跨请求复用的服务端登录会话。

package com.hyxy.sdemo.controller;

import cn.hutool.core.util.StrUtil;
import com.hyxy.sdemo.shiro.JwtUtil;
import com.hyxy.sdemo.shiro.ShiroToken;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 登录接口教学示例。
 */
@RestController
public class LoginController {

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public Map<String, Object> login(
            @RequestParam String username,
            @RequestParam String password) {
        if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
            return result(400, "账号密码不能为空", null);
        }

        // 模拟数据库校验。
        if (!"admin".equals(username) || !"123456".equals(password)) {
            return result(400, "账号或密码错误", null);
        }

        // 1. 生成 JWT。
        String token = jwtUtil.createToken(username);

        // 2. 当前请求中交给 Shiro 校验 JWT。
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new ShiroToken(token));
        } catch (Exception e) {
            return result(400, "登录失败:" + e.getMessage(), null);
        }

        Map<String, String> data = new HashMap<>();
        data.put("token", token);
        return result(200, "登录成功", data);
    }

    @PostMapping("/logout")
    public Map<String, Object> logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return result(200, "退出成功", null);
    }

    private Map<String, Object> result(int code, String msg, Object data) {
        Map<String, Object> res = new HashMap<>();
        res.put("code", code);
        res.put("msg", msg);
        res.put("data", data);
        return res;
    }
}

在无状态 JWT 方案中,真正决定后续请求是否已认证的是客户端是否继续携带有效 JWT。单独调用 logout() 不能让已经签发的 JWT 自动失效;如果课程进一步实现“强制退出”,通常需要令牌黑名单、版本号或较短的过期时间等额外机制。

受保护接口

package com.hyxy.sdemo.controller;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 需要登录,并且需要 order:list 权限。
 */
@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping("/list")
    @RequiresPermissions("order:list")
    public Map<String, Object> list() {
        Map<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("msg", "查询订单成功");
        res.put("data", "订单列表数据");
        return res;
    }
}
image-002
image-002

访问 OrderController:

image-003
image-003
image-004
image-004

Vue 3 前端适配

前端的核心工作是保存登录接口返回的 Token,并在后续请求中通过 Authorization 请求头携带 Token。

// Vue 3 登录核心代码
const login = async () => {
    const res = await axios.post('/api/login', {
        username: 'admin',
        password: '123456'
    });

    if (res.data.code === 200) {
        localStorage.setItem('token', res.data.data.token);
        ElMessage.success('登录成功');
    }
};

// Axios 请求拦截器:统一携带 Token。
axios.interceptors.request.use(config => {
    const token = localStorage.getItem('token');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

使用数据库进行账号密码认证

前面的无状态示例中,登录接口为了突出 JWT 流程,直接写死了账号和密码。真实项目通常需要从数据库读取用户信息。

需要注意:下面的 UsernamePasswordToken 认证方式属于另一套认证入口,不能直接与前文“只支持 ShiroToken”的 CustomShiroRealm 混用。如果采用这种方式,应让对应 Realm 支持 UsernamePasswordToken,并由 Shiro 的凭据匹配器完成密码比较。

Realm 查询数据库

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
        throws AuthenticationException {
    String username = (String) token.getPrincipal();
    User user = userService.selectByUserName(username);

    if (user == null) {
        throw new UnknownAccountException("账号不存在");
    }

    return new SimpleAuthenticationInfo(
            user.getUserName(),
            user.getPassword(),
            getName()
    );
}

SimpleAuthenticationInfo 中保存的是数据库中的用户凭据。调用 subject.login(token) 后,Shiro 会通过 Realm 配置的 CredentialsMatcher 比较用户提交的密码和数据库中的密码。生产环境不应保存明文密码。

使用 UsernamePasswordToken 登录

@PostMapping("/login")
public Map<String, Object> login(
        @RequestParam String username,
        @RequestParam String password) {
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    Subject subject = SecurityUtils.getSubject();

    try {
        subject.login(token);
    } catch (UnknownAccountException e) {
        throw new RuntimeException("账号不存在", e);
    } catch (IncorrectCredentialsException e) {
        throw new RuntimeException("账号或密码错误", e);
    } catch (AuthenticationException e) {
        throw new RuntimeException("登录认证失败", e);
    }

    String jwt = jwtUtil.createToken(username);
    Map<String, String> data = new HashMap<>();
    data.put("token", jwt);
    return result(200, "登录成功", data);
}

如果登录后仍然要使用前文的 JWT 无状态过滤器处理后续请求,则需要同时保留“账号密码认证”和“JWT 认证”两类处理逻辑,不能简单用一个只支持 ShiroToken 的 Realm 替换全部认证流程。

Shiro 授权体系

授权核心原理

用户认证成功后,访问带角色或权限限制的接口时,Shiro 会根据当前 Subject 的 principal 触发 Realm 中的 doGetAuthorizationInfo(),加载角色和权限信息,再完成访问判断。

常见授权方式包括注解式授权和过滤器链授权。本笔记主要使用注解式授权。

常用权限注解

  • @RequiresAuthentication:要求当前用户已经完成认证。
  • @RequiresGuest:要求当前用户处于游客状态。
  • @RequiresRoles("admin"):要求当前用户拥有指定角色。
  • @RequiresPermissions("sys:user:list"):要求当前用户拥有指定资源权限。

注解权限示例

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/user")
public class UserController {

    /**
     * 需要 sys:user:list 权限。
     */
    @GetMapping("/list")
    @RequiresPermissions("sys:user:list")
    public Map<String, Object> userList() {
        Map<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("msg", "查询用户列表成功");
        res.put("data", "用户列表数据");
        return res;
    }

    /**
     * 需要 admin 角色。
     */
    @GetMapping("/delete")
    @RequiresRoles("admin")
    public Map<String, Object> deleteUser() {
        Map<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("msg", "删除用户成功");
        return res;
    }
}

在前文教学版 CustomShiroRealm 中,admin 用户只配置了 order:list 和 order:add 权限,因此如果直接访问上面的 sys:user:list 接口,会因为缺少该权限而被拒绝。实际项目中应从数据库或其他权限数据源动态加载角色和权限。

执笔 AI 改写

喜欢的话,留下你的评论吧~

加载中...