Java项目做鉴权,选型上就三条路:Spring Security、Apache Shiro、自己写。三种方案我都用过或者评估过。2018年我在做一个新项目的技术选型时,花了一段时间研究Spring Security,最后放弃了。原因后面会讲。到现在几年下来,回头看这个决定没有后悔过。
先说结论:如果你的项目没有OAuth2、OIDC这类标准协议的对接需求,我建议自己写鉴权。 一个Filter加一个JWT库,能覆盖绝大多数业务系统的需求,代码量少、完全可控、团队每个人都看得懂。
下面把三种方案分开来聊,适用场景是什么,坑在哪里,怎么选。
鉴权要解决的问题
在对比框架之前,先把鉴权这件事理清楚。不管用什么框架,鉴权要解决的就两个问题:
认证:你是谁。用户提交凭证(用户名密码、手机验证码、第三方授权码),系统验证凭证的合法性,验证通过后签发一个token。
授权:你能干什么。后续每次请求带上token,系统根据token找到这个用户的权限列表,判断当前请求的资源(通常就是URL + HTTP方法)是否在权限范围内。
我之前就写过一个鉴权库,核心就是一个泛型接口:
public interface Authenticator<P, C, T> {
// 验证凭证,返回token和权限列表
AuthResult<T> authenticate(P principal, C credentials);
// 根据token获取当前用户的权限集合
Set<String> getAuthorities(T token);
// 判断当前请求是否有权限访问
boolean hasAccess(T token, String uri, String method);
}
实现这个接口的时候,authenticate方法里查数据库验证用户名密码,验证通过就生成一个token,同时把用户的权限列表缓存起来。hasAccess方法里拿token找到权限列表,用Ant风格的路径匹配判断当前URL是否在权限范围内。
配合一个Servlet Filter或者Spring的HandlerInterceptor,在请求进入业务逻辑之前做一次拦截,整个鉴权流程就跑通了。这个思路我用了很多年,换过不同的项目和团队,从来没遇到过它解决不了的场景。
鉴权的复杂度大部分来自框架的抽象,不是来自需求本身。 如果你把鉴权拆成认证和授权两步,每一步的逻辑都是清晰的。
Spring Security为什么这么复杂
Spring Security的复杂不是因为它设计得不好,恰恰相反,它设计得太全面了。
看一下它要覆盖的场景:表单登录、HTTP Basic认证、OAuth2 Client、OAuth2 Resource Server、SAML2、LDAP集成、OpenID Connect、Remember Me、CSRF防护、Session并发控制、方法级别的安全注解(@PreAuthorize、@Secured)、响应式安全(WebFlux)。这些场景的认证流程、数据模型、交互方式各不相同,要把它们全部纳入一个统一的框架,抽象层就肯定得做的比较厚。
看Spring Boot 2.7里Spring Security的默认行为。引入spring-boot-starter-security这个依赖之后,SpringBootWebSecurityConfiguration会自动注册一条默认的SecurityFilterChain:
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 所有请求都需要认证
http.authorizeRequests().anyRequest().authenticated();
// 开启表单登录
http.formLogin();
// 开启HTTP Basic
http.httpBasic();
return http.build();
}
三行配置,默认开启了表单登录和HTTP Basic,所有请求都需要认证。如果你的项目恰好是传统的服务端渲染Web应用,这套默认值能直接用,Spring Security的开箱体验是不错的。
不过现在很多项目是前后端分离的API服务,认证方式用的是JWT。这种场景下,表单登录、HTTP Basic、CSRF防护(前后端分离通常不依赖Cookie)、Session这些默认行为都用不上,得手动关掉。
Spring Security在配置体验上一直在改进。6.x引入了Lambda DSL,用Lambda表达式替代以前的.and()链式调用,配置的可读性好了不少。到7.0(配合Spring Boot 4.x),Lambda DSL成了唯一的配置方式,旧的.and()链式调用和authorizeRequests被彻底移除,MvcRequestMatcher也换成了PathPatternRequestMatcher。
用7.0的Lambda DSL写一个JWT场景的配置,大致是这样:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated());
// 加入自定义的JWT验证Filter
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
比起以前的写法确实清爽了一些,不用再写.and()来回跳转了。Spring Security团队这几年在降低使用门槛上做了不少工作,7.0还合并了Spring Authorization Server,原生支持了多因素认证,新增了SPA场景的CSRF配置(csrf.spa())。
配置语法的改进是实打实的进步,但Spring Security的根本复杂度没有变。它仍然是一个覆盖十几种安全场景的全能框架,AuthenticationManager、AuthenticationProvider、UserDetailsService、SecurityContextHolder这些核心抽象层还在。你要做[Java]()鉴权,仍然需要先关掉一堆默认行为,然后自己实现JWT验证的Filter,实现UserDetailsService来对接你的用户表。框架帮你做的事情,和你自己额外做的事情相比,性价比不高。
还有一个实际用户体验问题:Spring Security的扩展点分散在不同的抽象层里。AuthenticationManager负责认证流程的分发,AuthenticationProvider负责具体的认证逻辑,UserDetailsService负责加载用户信息,SecurityContextHolder负责存储认证结果。你要改某个环节的行为,得先搞清楚这个环节在哪个抽象层,对应哪个接口,怎么注入进去。我之前见过一个同事,为了把Session存到Redis里,折腾了很久才找到正确的扩展点。这种事情花在网上搜索和调试的时间,往往比自己写一个Filter的时间还长。
Spring Security不是为你的业务场景设计的,它是为所有可能的安全场景设计的。 这是它强大的原因,也是大多数项目用起来觉得重的原因。
Shiro的定位和现状
Apache Shiro走的是另一条路。它的核心概念就三个:
- Subject:当前用户的安全操作入口。登录、登出、权限检查都通过它
- SecurityManager:安全管理器,负责协调认证和授权请求
- Realm:数据源。你实现这个接口来告诉Shiro从哪里获取用户信息和权限数据
做鉴权只需要写一个自定义的Realm,继承AuthorizingRealm,重写两个方法:
public class MyRealm extends AuthorizingRealm {
// 认证:根据token查询用户信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
// 从数据库查询用户
User user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(
user.getUsername(), user.getPassword(), getName());
}
// 授权:根据用户查询权限列表
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 从数据库查询角色和权限
info.setRoles(userService.getRoles(username));
info.setStringPermissions(userService.getPermissions(username));
return info;
}
}
和Spring Security相比,Shiro的认证授权在概念上确实简洁。你只需要关心「从哪拿数据」和「数据长什么样」,框架帮你处理后续的匹配和校验。
Shiro 2.x做了一些现代化的改进:默认密码哈希算法从SHA-256换成了Argon2id,新增了BearerHttpAuthenticationFilter来支持JWT和OAuth2的Bearer Token认证,支持Jakarta EE的命名空间迁移(javax到jakarta),也提供了Spring Boot 3的集成。核心架构(Subject、SecurityManager、Realm这条线)没有大的变化。2.0在2023年2月正式发布,最新的稳定版是2.1.0(2025年2月发布),3.0.0-alpha-1也已经出了预发行版。从版本迭代的节奏来看,Shiro团队还在持续投入,但和Spring Security的更新频率相比,差距明显。
Shiro的问题在于生态和社区的体量。它是Apache基金会的项目,维护团队的规模远不如Spring Security。遇到问题去搜,Spring Security的资料满地都是,Shiro相关的内容并不多,尤其是2.x之后的中文资料更少。对比Spring Security背后有Spring团队持续投入,Shiro在长期维护和社区支持上差一个量级。
另外,Shiro不和Spring绑定这个特点,在十年前是优势(那时候还有很多非Spring的[Java]()项目),但放到现在,绝大多数Java项目都在用Spring Boot,这个优势的意义已经不大了。
自己写鉴权
大厂的实际做法,可能和很多人想的不一样。阿里、字节、美团、快手这些公司,基本不直接用Spring Security或者Shiro。它们的鉴权体系都是内部自研的,原因是大厂的需求远比框架的默认方案复杂(多租户、多端登录互踢、动态权限、灰度放权),框架的抽象在这些场景下反而是阻碍。
中小公司的情况更直观。很多团队用的就是一个Filter加JWT,干净利落,团队每个人都看得懂,出了问题排查也方便。
自己写鉴权的完整方案分三部分。
认证流程:用户调登录接口提交凭证,验证通过后用JWT签发token。token里放用户ID和过期时间,用HS256或RS256签名,返回给客户端。客户端后续每次请求在Authorization Header里带上这个token。
JWT签发和解析的核心代码:
// 签发token
String token = Jwts.builder()
.setSubject(String.valueOf(userId))
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireMillis))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
// 解析token
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
String userId = claims.getSubject();
授权流程:写一个Filter,在请求进入Controller之前拦截。从Header里取token,解析出用户ID,根据用户ID查询权限列表(权限数据缓存在Redis里,不用每次查数据库),判断当前请求的URL和HTTP方法是否在用户的权限范围内。
Filter的核心骨架:
public class AuthFilter implements Filter {
// 不需要鉴权的路径
private static final Set<String> WHITE_LIST = Set.of(
"/api/auth/login", "/api/auth/register"
);
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String uri = request.getRequestURI();
// 白名单直接放行
if (WHITE_LIST.contains(uri)) {
chain.doFilter(req, res);
return;
}
// 从Header取token并解析
String token = request.getHeader("Authorization");
Claims claims = parseToken(token);
if (claims == null) {
response.setStatus(401);
return;
}
// 查询权限并匹配
String userId = claims.getSubject();
Set<String> authorities = getAuthorities(userId);
if (!matchPermission(authorities, uri, request.getMethod())) {
response.setStatus(403);
return;
}
// 把用户信息放到请求上下文中,后续业务代码可以直接获取
request.setAttribute("userId", userId);
chain.doFilter(req, res);
}
}
getAuthorities方法从Redis查权限列表,matchPermission方法做URL的Ant风格匹配。这两个方法加起来不超过20行代码。
微服务场景:如果是微服务架构,鉴权在网关层统一做,业务服务不需要安全框架。网关解析token、查权限,把用户信息通过请求头传给下游服务。业务服务从请求头里取用户ID就行了,完全不用引入任何安全相关的依赖。
有一种担心是:自己写会不会遗漏[安全]()细节?这个担心有道理,但要区分开。密码哈希、token签名这些是加密层面的事情,直接用成熟的库(jjwt做JWT,BCrypt做密码哈希),不需要自己实现加密算法。CSRF、CORS这些是HTTP协议层面的事情,Spring MVC自带@CrossOrigin注解和CorsFilter,也不需要安全框架来处理。需要自己写的部分就是认证授权的业务逻辑,这部分不管用什么框架最终都得自己写。
如果项目需要对接OAuth2、OpenID Connect这类标准协议,可以单独引入spring-security-oauth2-client或者nimbus-oauth2-oidc-sdk这类专门的库,不用把整个Spring Security引进来。
选型决策
三种方案的核心差异:
| 维度 |
Spring Security |
Shiro |
自己写 |
| 学习曲线 |
陡峭,概念多,扩展点分散 |
平缓,核心概念三个 |
取决于团队对鉴权的理解 |
| OAuth2/OIDC支持 |
完善的原生支持 |
2.x支持Bearer Token,协议实现需自己补 |
需单独引入协议库 |
| RBAC权限控制 |
内置支持 |
内置支持 |
自己实现,代码量不大 |
| 与Spring Boot集成 |
深度集成,自动配置 |
有shiro-spring-boot-starter |
天然集成,就是一个Filter |
| 社区和维护 |
Spring团队持续维护,非常活跃 |
Apache项目,2.x在持续更新但体量小 |
不依赖外部社区 |
| 定制灵活度 |
高,但找到正确的扩展点成本高 |
中等 |
完全可控 |
| 代码可读性 |
依赖对框架的熟悉程度 |
较好 |
团队内每个人都能看懂 |
按场景的选型建议:
我的默认推荐是自己写。 大部分业务系统(后台管理系统、APP后端、B端SaaS),鉴权需求就是JWT认证加URL级别的权限控制。一个Filter + JWT库 + Redis缓存权限数据,代码量少、可控性强、排查问题方便。这个方案我自己用了很多年,也是大部分中小公司和互联网大厂在用的做法。
需要对接OAuth2或OpenID Connect的系统(对外开放平台、需要接入企业SSO的项目),Spring Security有优势。这些协议的握手流程、token交换、刷新机制细节很多,自己实现成本高。这是Spring Security真正有不可替代价值的场景。
微服务架构下,鉴权放在网关层统一处理。业务服务不引入任何安全框架,从请求头里取用户信息直接用。这是目前大多数微服务项目的做法。
Shiro在当前的技术环境下比较尴尬。Spring技术栈的项目,要么用Spring Security(需要标准协议支持时),要么自己写(大部分情况),「Shiro很难找到一个非它不可的场景」。
小结
安全框架的选型,就是在选复杂度放在哪里。用Spring Security,复杂度在框架里,你需要投入时间去理解框架的内部机制,才能正确地使用和扩展它。自己写,复杂度在你的业务代码里,但这部分代码是你完全掌控的,出了问题你知道去哪里找。
我的选择是后者。鉴权的业务逻辑,不管用什么框架最终都得自己写。既然核心代码跑不掉,引入一个重量级框架来管理这些代码的执行顺序,对大部分项目来说收益有限。一个Filter、一个JWT库、一份Redis里的权限缓存,这三样东西能覆盖的场景比很多人想象的要广。
不管选哪种方案,有几条安全基线是必须做到的:密码存储用BCrypt或Argon2id,不能明文也不能用MD5;token要有过期时间,长期不操作的用户必须重新登录;生产环境必须走HTTPS;敏感操作(修改密码、支付)要做二次验证。这些跟框架选型无关,选什么方案都要做。
希望这篇从真实项目经验出发的分析,能帮你做出更适合自己团队的选型决策。想了解更多关于系统架构和Java开发的深度内容,欢迎访问 云栈社区 与其他开发者交流。