从零到一:Spring Boot + Shiro + JWT 构建无状态认证系统实战指南

从零到一:Spring Boot + Shiro + JWT 构建无状态认证系统实战指南

引言:你还在为分布式系统认证头疼吗?

在前后端分离架构盛行的今天,传统基于Session的认证方式面临诸多挑战:服务器内存占用高、分布式部署困难、跨域问题突出。你是否正在寻找一种轻量级、无状态、高安全性的认证方案?本文将带你从零构建基于Spring Boot、Shiro和JWT(JSON Web Token)的认证系统,彻底解决这些痛点。

读完本文你将获得:

  • 掌握JWT(JSON Web Token)的工作原理与最佳实践
  • 学会Shiro(权限框架)在Spring Boot中的优雅配置
  • 实现完全无状态的前后端认证流程
  • 理解RBAC(基于角色的访问控制)模型设计
  • 具备应对高并发场景的认证系统优化思路

技术栈选型与架构设计

核心技术栈对比

技术版本作用优势
Spring Boot1.5.8.RELEASE快速开发框架自动配置、 starter依赖、独立运行
Apache Shiro1.3.2安全框架轻量级、易于理解、灵活扩展
JWT3.2.0身份令牌无状态、自包含、跨域支持
Maven3.x项目构建依赖管理、标准化构建流程
JDK1.8开发环境Lambda表达式、Stream API支持

系统架构流程图

mermaid

环境搭建与项目初始化

快速开始:3步搭建开发环境

  1. 克隆项目仓库
git clone https://gitcode.com/gh_mirrors/sp/Spring-Boot-Shiro.git
cd Spring-Boot-Shiro
  1. 核心依赖解析(pom.xml关键配置)
<!-- Spring Boot Web支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>1.5.8.RELEASE</version>
</dependency>

<!-- Shiro安全框架 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>

<!-- JWT令牌支持 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.2.0</version>
</dependency>
  1. 项目结构概览
src/main/java/org/inlighting/
├── WebApplication.java          // 应用入口
├── bean/                        // 数据模型
│   └── ResponseBean.java        // 统一响应格式
├── controller/                  // 控制器
│   ├── ExceptionController.java // 全局异常处理
│   └── WebController.java       // 业务接口
├── database/                    // 数据访问层
│   ├── DataSource.java          // 模拟数据源
│   ├── UserBean.java            // 用户模型
│   └── UserService.java         // 用户服务
├── exception/                   // 自定义异常
│   └── UnauthorizedException.java
├── shiro/                       // Shiro配置
│   ├── JWTFilter.java           // JWT过滤器
│   ├── JWTToken.java            // 令牌封装
│   ├── MyRealm.java             // 安全领域
│   └── ShiroConfig.java         // Shiro主配置
└── util/                        // 工具类
    └── JWTUtil.java             // JWT工具

核心组件深度解析

JWT工具:安全令牌的生成与验证

JWT(JSON Web Token)是实现无状态认证的核心,我们的工具类提供三大核心功能:生成令牌、验证令牌、提取信息。

public class JWTUtil {
    // 过期时间5分钟(生产环境建议30分钟内)
    private static final long EXPIRE_TIME = 5*60*1000;

    /**
     * 生成签名
     * @param username 用户名
     * @param secret 用户密码(作为加密密钥)
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withClaim("username", username)  // 附带用户名信息
                    .withExpiresAt(date)               // 设置过期时间
                    .sign(algorithm);                  // 签名
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     * 校验token是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            verifier.verify(token);  // 验证token
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 从token中获取用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}
JWT安全最佳实践
  • 密钥管理:使用用户密码作为加密密钥,确保每个用户密钥唯一
  • 过期策略:短期有效期(5-30分钟)+ 令牌刷新机制
  • 载荷内容:仅包含必要信息,避免敏感数据
  • 传输安全:务必通过HTTPS传输,防止中间人攻击

Shiro核心配置:打造自定义安全体系

Shiro的配置是整个认证系统的灵魂,我们需要实现四大核心组件:

1. JWTToken:令牌载体
public class JWTToken implements AuthenticationToken {
    private String token;  // 存储JWT令牌
    
    public JWTToken(String token) {
        this.token = token;
    }
    
    @Override
    public Object getPrincipal() { return token; }
    
    @Override
    public Object getCredentials() { return token; }
}
2. MyRealm:认证与授权核心
@Service
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    
    // 必须重写此方法,否则Shiro无法识别JWTToken
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }
    
    /**
     * 授权逻辑:获取用户角色和权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        UserBean user = userService.getUser(username);
        
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRole(user.getRole());  // 添加角色
        // 添加权限(逗号分隔转Set)
        info.addStringPermissions(Arrays.asList(user.getPermission().split(",")));
        return info;
    }
    
    /**
     * 认证逻辑:验证用户身份
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) 
            throws AuthenticationException {
        String token = (String) auth.getCredentials();
        String username = JWTUtil.getUsername(token);
        
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }
        
        UserBean user = userService.getUser(username);
        if (user == null) {
            throw new AuthenticationException("User didn't existed!");
        }
        
        if (!JWTUtil.verify(token, username, user.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }
        
        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}
3. JWTFilter:请求拦截与令牌校验
public class JWTFilter extends BasicHttpAuthenticationFilter {
    /**
     * 判断是否为登录请求(检查Authorization头)
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        return req.getHeader("Authorization") != null;
    }
    
    /**
     * 执行登录验证
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        
        JWTToken token = new JWTToken(authorization);
        getSubject(request, response).login(token);  // 提交给Realm验证
        return true;
    }
    
    /**
     * 权限检查逻辑
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);  // 验证失败返回401
            }
        }
        return true;  // 允许访问,由Controller层注解控制权限
    }
    
    /**
     * 支持跨域请求
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        
        // 设置跨域响应头
        httpServletResponse.setHeader("Access-control-Allow-Origin", 
                httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", 
                "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", 
                httpServletRequest.getHeader("Access-Control-Request-Headers"));
        
        // OPTIONS请求直接返回200
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        return super.preHandle(request, response);
    }
}
4. ShiroConfig:整合所有组件
@Configuration
public class ShiroConfig {
    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(realm);
        
        // 关闭Shiro自带session(实现无状态)
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        
        return manager;
    }
    
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        
        // 注册自定义过滤器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);
        
        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");
        
        // URL过滤规则
        Map<String, String> filterRuleMap = new HashMap<>();
        filterRuleMap.put("/**", "jwt");        // 所有请求通过JWT过滤器
        filterRuleMap.put("/401", "anon");      // 401页面无需认证
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        
        return factoryBean;
    }
    
    // 启用Shiro注解支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

控制器设计:RESTful接口与权限控制

我们设计了5个核心接口,覆盖不同权限级别:

@RestController
public class WebController {
    @Autowired
    private UserService userService;
    
    /**
     * 登录接口:验证凭据并生成令牌
     */
    @PostMapping("/login")
    public ResponseBean login(@RequestParam("username") String username,
                              @RequestParam("password") String password) {
        UserBean userBean = userService.getUser(username);
        if (userBean.getPassword().equals(password)) {
            return new ResponseBean(200, "Login success", 
                    JWTUtil.sign(username, password));  // 生成JWT令牌
        } else {
            throw new UnauthorizedException();
        }
    }
    
    /**
     * 公开接口:任何人可访问
     */
    @GetMapping("/article")
    public ResponseBean article() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            return new ResponseBean(200, "You are already logged in", null);
        } else {
            return new ResponseBean(200, "You are guest", null);
        }
    }
    
    /**
     * 需认证接口:登录用户可访问
     */
    @GetMapping("/require_auth")
    @RequiresAuthentication  // Shiro注解:需认证
    public ResponseBean requireAuth() {
        return new ResponseBean(200, "You are authenticated", null);
    }
    
    /**
     * 角色控制接口:仅admin角色可访问
     */
    @GetMapping("/require_role")
    @RequiresRoles("admin")  // Shiro注解:需admin角色
    public ResponseBean requireRole() {
        return new ResponseBean(200, "You are visiting require_role", null);
    }
    
    /**
     * 权限控制接口:需特定权限组合
     */
    @GetMapping("/require_permission")
    @RequiresPermissions(logical = Logical.AND, value = {"view", "edit"})  // 需同时拥有view和edit权限
    public ResponseBean requirePermission() {
        return new ResponseBean(200, "You are visiting permission require edit,view", null);
    }
}

全局异常处理:打造友好的错误响应

使用@RestControllerAdvice统一处理异常:

@RestControllerAdvice
public class ExceptionController {
    // 处理Shiro异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public ResponseBean handle401(ShiroException e) {
        return new ResponseBean(401, e.getMessage(), null);
    }
    
    // 处理自定义未授权异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseBean handle401() {
        return new ResponseBean(401, "Unauthorized", null);
    }
    
    // 处理其他所有异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
        return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);
    }
    
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        return statusCode != null ? HttpStatus.valueOf(statusCode) : HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

实战演练:从零到一测试完整流程

测试环境准备

我们使用HashMap模拟数据库,预置了两个测试用户:

用户名密码角色权限
smithsmith123userview
dannydanny123adminview,edit

认证流程测试(使用PostMan)

1. 用户登录获取令牌
POST /login
Content-Type: application/x-www-form-urlencoded

username=smith&password=smith123

成功响应

{
  "code": 200,
  "msg": "Login success",
  "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."  // JWT令牌
}
2. 访问公开接口(无需令牌)
GET /article

响应

{
  "code": 200,
  "msg": "You are guest",
  "data": null
}
3. 访问需认证接口(携带令牌)
GET /require_auth
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

成功响应

{
  "code": 200,
  "msg": "You are authenticated",
  "data": null
}
4. 权限测试对比
用户访问 /require_role (admin角色)访问 /require_permission (view+edit权限)
smith401 Unauthorized401 Unauthorized
danny200 OK200 OK

认证失败场景测试

测试场景预期结果
无效令牌401 Unauthorized
令牌过期401 Unauthorized
错误密码401 Unauthorized
无令牌访问保护接口401 Unauthorized
权限不足401 Unauthorized

进阶优化:从可用到优秀的关键改进

1. 令牌过期策略优化

当前JWT工具类中令牌过期时间固定为5分钟,生产环境建议实现动态刷新机制:

// 优化建议:JWTUtil增加刷新令牌方法
public static String refreshToken(String token, String secret) {
    String username = getUsername(token);
    return sign(username, secret);  // 重新生成令牌
}

前端实现思路:

// 前端拦截器示例(Axios)
axios.interceptors.response.use(
    response => response,
    error => {
        const originalRequest = error.config;
        // 如果是401且未尝试刷新令牌
        if (error.response.status === 401 && !originalRequest._retry) {
            originalRequest._retry = true;
            // 调用刷新令牌接口
            return axios.post('/refreshToken', { token: localStorage.getItem('token') })
                .then(res => {
                    localStorage.setItem('token', res.data.data);
                    originalRequest.headers['Authorization'] = res.data.data;
                    return axios(originalRequest);
                });
        }
        return Promise.reject(error);
    }
);

2. 安全加固建议

  1. 密码加密存储
// 优化建议:使用BCrypt加密密码
public class PasswordUtil {
    public static String encrypt(String password) {
        return BCrypt.hashpw(password, BCrypt.gensalt());
    }
    
    public static boolean verify(String password, String hashed) {
        return BCrypt.checkpw(password, hashed);
    }
}
  1. 请求频率限制
// 优化建议:使用Redis实现限流
@Component
public class RateLimiter {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public boolean allowRequest(String ip) {
        String key = "rate_limit:" + ip;
        Long count = redisTemplate.opsForValue().increment(key, 1);
        if (count == 1) {
            redisTemplate.expire(key, 60, TimeUnit.SECONDS);
        }
        return count <= 100;  // 每分钟最多100次请求
    }
}
  1. 敏感信息脱敏
// 优化建议:响应数据脱敏
public class SensitiveInfoUtil {
    public static String maskPhone(String phone) {
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }
}

3. 性能优化方向

  1. 缓存用户权限
// 优化建议:在MyRealm中添加缓存
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    String username = JWTUtil.getUsername(principals.toString());
    
    // 从缓存获取权限信息
    String cacheKey = "auth:" + username;
    AuthorizationInfo cachedInfo = redisTemplate.opsForValue().get(cacheKey);
    if (cachedInfo != null) {
        return cachedInfo;
    }
    
    // 数据库查询并缓存(设置10分钟过期)
    UserBean user = userService.getUser(username);
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    // ... 权限设置逻辑 ...
    
    redisTemplate.opsForValue().set(cacheKey, info, 10, TimeUnit.MINUTES);
    return info;
}
  1. 异步处理
// 优化建议:使用@Async处理非关键流程
@Service
public class LogService {
    @Async
    public CompletableFuture<Void> logAccess(String username, String url) {
        // 异步记录访问日志
        return CompletableFuture.runAsync(() -> {
            // 日志记录逻辑
        });
    }
}

总结与展望

通过本文的学习,你已经掌握了使用Spring Boot、Shiro和JWT构建无状态认证系统的核心技术。我们从基础架构到实际编码,再到测试优化,完整覆盖了系统开发的全流程。

关键知识点回顾

  1. 无状态认证原理:基于JWT的令牌机制,摆脱服务器Session依赖
  2. Shiro扩展点:自定义Realm、Filter和Token实现灵活认证
  3. RBAC权限模型:基于角色和权限的访问控制
  4. 前后端分离最佳实践:统一响应格式、全局异常处理、跨域支持

未来演进方向

  1. OAuth2.0集成:支持第三方登录(GitHub、微信等)
  2. 多因素认证:增加短信、邮箱验证码等二次验证
  3. 细粒度权限控制:基于资源的权限管理
  4. 分布式Session:使用Redis实现集群环境下的会话共享

最后

安全认证是任何系统不可或缺的组成部分,希望本文能帮助你构建更安全、更可靠的应用系统。如果你有任何疑问或优化建议,欢迎在评论区留言讨论!

别忘了点赞收藏本文,关注作者获取更多技术干货!

下一篇预告:《Spring Cloud微服务架构下的统一认证解决方案》

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值