找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4724

积分

0

好友

658

主题
发表于 1 小时前 | 查看: 2| 回复: 0

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的根本复杂度没有变。它仍然是一个覆盖十几种安全场景的全能框架,AuthenticationManagerAuthenticationProviderUserDetailsServiceSecurityContextHolder这些核心抽象层还在。你要做[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开发的深度内容,欢迎访问 云栈社区 与其他开发者交流。




上一篇:干货类:2026年AI应用趋势预测:5个普通人可实操的创收与增效方向
下一篇:被裁后秒删同事微信,职场关系真的只是同事一场?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-31 05:10 , Processed in 0.669252 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表