从零到一:Spring Boot + Shiro + JWT 构建无状态认证系统实战指南
引言:你还在为分布式系统认证头疼吗?
在前后端分离架构盛行的今天,传统基于Session的认证方式面临诸多挑战:服务器内存占用高、分布式部署困难、跨域问题突出。你是否正在寻找一种轻量级、无状态、高安全性的认证方案?本文将带你从零构建基于Spring Boot、Shiro和JWT(JSON Web Token)的认证系统,彻底解决这些痛点。
读完本文你将获得:
- 掌握JWT(JSON Web Token)的工作原理与最佳实践
- 学会Shiro(权限框架)在Spring Boot中的优雅配置
- 实现完全无状态的前后端认证流程
- 理解RBAC(基于角色的访问控制)模型设计
- 具备应对高并发场景的认证系统优化思路
技术栈选型与架构设计
核心技术栈对比
| 技术 | 版本 | 作用 | 优势 |
|---|---|---|---|
| Spring Boot | 1.5.8.RELEASE | 快速开发框架 | 自动配置、 starter依赖、独立运行 |
| Apache Shiro | 1.3.2 | 安全框架 | 轻量级、易于理解、灵活扩展 |
| JWT | 3.2.0 | 身份令牌 | 无状态、自包含、跨域支持 |
| Maven | 3.x | 项目构建 | 依赖管理、标准化构建流程 |
| JDK | 1.8 | 开发环境 | Lambda表达式、Stream API支持 |
系统架构流程图
环境搭建与项目初始化
快速开始:3步搭建开发环境
- 克隆项目仓库
git clone https://gitcode.com/gh_mirrors/sp/Spring-Boot-Shiro.git
cd Spring-Boot-Shiro
- 核心依赖解析(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>
- 项目结构概览
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模拟数据库,预置了两个测试用户:
| 用户名 | 密码 | 角色 | 权限 |
|---|---|---|---|
| smith | smith123 | user | view |
| danny | danny123 | admin | view,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权限) |
|---|---|---|
| smith | 401 Unauthorized | 401 Unauthorized |
| danny | 200 OK | 200 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. 安全加固建议
- 密码加密存储:
// 优化建议:使用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);
}
}
- 请求频率限制:
// 优化建议:使用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次请求
}
}
- 敏感信息脱敏:
// 优化建议:响应数据脱敏
public class SensitiveInfoUtil {
public static String maskPhone(String phone) {
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
3. 性能优化方向
- 缓存用户权限:
// 优化建议:在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;
}
- 异步处理:
// 优化建议:使用@Async处理非关键流程
@Service
public class LogService {
@Async
public CompletableFuture<Void> logAccess(String username, String url) {
// 异步记录访问日志
return CompletableFuture.runAsync(() -> {
// 日志记录逻辑
});
}
}
总结与展望
通过本文的学习,你已经掌握了使用Spring Boot、Shiro和JWT构建无状态认证系统的核心技术。我们从基础架构到实际编码,再到测试优化,完整覆盖了系统开发的全流程。
关键知识点回顾
- 无状态认证原理:基于JWT的令牌机制,摆脱服务器Session依赖
- Shiro扩展点:自定义Realm、Filter和Token实现灵活认证
- RBAC权限模型:基于角色和权限的访问控制
- 前后端分离最佳实践:统一响应格式、全局异常处理、跨域支持
未来演进方向
- OAuth2.0集成:支持第三方登录(GitHub、微信等)
- 多因素认证:增加短信、邮箱验证码等二次验证
- 细粒度权限控制:基于资源的权限管理
- 分布式Session:使用Redis实现集群环境下的会话共享
最后
安全认证是任何系统不可或缺的组成部分,希望本文能帮助你构建更安全、更可靠的应用系统。如果你有任何疑问或优化建议,欢迎在评论区留言讨论!
别忘了点赞收藏本文,关注作者获取更多技术干货!
下一篇预告:《Spring Cloud微服务架构下的统一认证解决方案》
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



