Basic Auth 大概是 Web 认证里最朴素的一种放在 Spring Boot 项目里简单到几乎不值得单独写一篇博客。但这段时间我把一个内部接口改造为带认证访问时发现越是简单的东西落到 Spring Security 这种框架里反而容易被若干细节卡住。这篇就把我实践下来的一套方案记录下来从配置到调用、从坑到排查一次性讲透给同样在 Spring Boot 里做 API 访问认证的同学一个直接能抄的参考。先说下场景一个基于 Spring Boot 的微服务提供一组接口给内部系统调用同时有一部分接口要给前端页面匿名访问。内部调用方希望用最简单的用户名密码方式认证不想为了一个小接口上 OAuth2 那一大套。最终定的方案就是 Basic Auth把需要保护的 API 用 Spring Security 挡在门外不需要认证的路径按白名单放行。下面从选型思路开始讲。1. 项目背景与认证方案选型1.1 需求拆解哪些接口要保护哪些要放行在动手写 Security 配置之前我第一件事是整理接口清单。这个步骤特别重要因为 Basic Auth 的配置本质就是一张准入名单你得先清楚自己的资源分布。我当时把接口分成了三类完全公开的比如健康检查、验证码、某些基础数据字典查询这些不需要任何凭证前端页面直接调用。需要登录态但不需要精细化权限的比如内部系统查询订单、推送消息只要能证明你是内部系统的合法调用方就行。需要更高权限的比如管理员重置密码、导出报表这类操作必须区分调用者身份。这个分类直接决定了后面的permitAll()、authenticated()、hasRole()三种写法。很多新手上来就写一个anyRequest().authenticated()把所有接口锁死结果前端页面全挂再急急忙忙开白名单来回折腾。先花十分钟列接口清单比什么都值。1.2 为什么选 Basic Auth而不是 Session、Token 或 OAuth2论坛里一谈到 API 认证基本都是 JWT、OAuth2反而 Basic Auth 被当成过时方案。但选型这事没有绝对好坏只有匹配不匹配。Basic Auth 的核心机制很简单客户端把用户名:密码用 Base64 编码后放进Authorization请求头服务端解码校验。它的典型优势是实现成本极低客户端和服务端都不需要复杂 SDK。无状态服务端不需要维护会话非常适合服务间调用。基于 HTTP 标准几乎所有 HTTP 客户端原生支持curl、Postman、Java HttpClient、浏览器都认识它。对比一下其他方案的适用边界方案是否需要服务端会话客户端接入成本适合场景Session Cookie是低浏览器自动带面向浏览器的传统 Web 应用JWT/Bearer Token否中需处理令牌刷新前后端分离、有复杂权限模型OAuth2视模式而定高需对接授权服务器第三方授权、多应用互通Basic Auth否极低内部系统/服务间调用、简单管理接口我这个项目全是内部系统调用没有第三方授权需求也没有复杂的令牌刷新逻辑用 Basic Auth 是最省事的选择。它的最大短板是每次请求都明文传凭证虽然经过 Base64 编码但可逆所以必须配合 HTTPS 使用。这一点我在方案评审时专门强调过——裸奔 HTTP 下的 Basic Auth 等于把密码贴在大门上。2. 认证配置的核心实现2.1 引入依赖锁定版本我用的是 Spring Boot 2.7 系列对应的 Spring Security 是 5.7 的配置风格。如果你用的是 Spring Boot 3.x配置方式有一些变化尤其是HttpSecurity相关的 Lambda 写法。我这里贴的是适配 Spring Boot 2.7 的写法也是目前存量项目里最常见的版本。在pom.xml里只需要加一个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency引入这个依赖后Spring Boot 会主动给所有接口加上默认认证启动后访问任何 API 都会弹 401。第一次接触的人经常被这个灵异现象卡住——项目没写任何安全代码怎么接口全部进不去了这是 Spring Security 的自动配置在起作用默认生成一个随机密码账号是user启动日志里可以找到。后面我们写了自己的配置类之后这个默认行为就会被覆盖。2.2 最简配置类写法Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/public/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .httpBasic(withDefaults()); return http.build(); } }这段配置干了三件事关闭 CSRF 防护。对于纯 API 接口、没有 Session 的无状态场景CSRF 防护没有意义因为 Basic Auth 的凭证在请求头里CSRF 攻击的核心是利用浏览器自动携带的 Cookie我们根本不依赖 Cookie。用路径匹配划分权限边界。/api/public/**完全开放/api/admin/**需要 ADMIN 角色其他所有请求只要认证通过就能访问。启用 HTTP Basic 认证。.httpBasic()是这整套方案的核心它让 Spring Security 从请求头里解析Authorization并完成认证。有一个细节值得注意requestMatchers的匹配规则是AntPathMatcher/api/public/**会匹配/api/public/下的多层路径但不会匹配/api/public本身。如果你希望两者都放行可以写成/api/public和/api/public/**两个规则或者直接用/api/public/**.equals这套自测一下。2.3 配置类的隐藏坑创建多个 SecurityFilterChain我在实际项目中遇到过一个问题配置类生效了但有些接口的认证行为不符合预期。排查后发现是项目里存在多个配置类各自定义了SecurityFilterChainSpring 按Order决定执行顺序导致某些请求走了另一套没有 Basic Auth 的过滤器链。如果你在一个项目里同时拥有多个安全配置比如一个给 API、一个给管理后台页面务必用Order控制先后而且每套链路的requestMatchers要把路径分清楚不要让它们出现交集。否则你会看到一种很诡异的现象同样的请求换一个端口就变认证方式了那多半就是过滤器链串了。3. 用户存储与密码校验细节3.1 从内存用户起步跑通链路SecurityFilterChain只是定义了认证方式用户数据本身还得单独配。最快速的验证方式是基于内存的用户存储Bean public UserDetailsService userDetailsService(PasswordEncoder encoder) { UserDetails internalUser User.withUsername(service) .password(encoder.encode(service123)) .roles(USER) .build(); UserDetails adminUser User.withUsername(admin) .password(encoder.encode(admin2024)) .roles(ADMIN) .build(); return new InMemoryUserDetailsManager(internalUser, adminUser); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }这里的User.withUsername()是 Spring Security 提供的建造器roles(ADMIN)实际上会转化成ROLE_ADMIN的权限标识这也是为什么配置里用hasRole(ADMIN)时不需要写ROLE_前缀的原因。新手经常在这里犯迷糊为什么我hasAuthority(ROLE_ADMIN)反而匹配不了因为hasRole会自动补前缀hasAuthority不会两者选一个用不要混。再说密码。BCryptPasswordEncoder是一个自适应哈希算法每次对同一个密码加密得到的密文都不同验证时再从密文中提取盐重新计算比对。这跟MD5、SHA这类可逆碰撞风险高的做法有本质区别是目前 Spring 生态里的默认推荐。很多旧项目直接把明文密码放进数据库一旦开启 Basic Auth那等于把密码存储在随时可能被拖库的源头上强烈不建议这么做。3.2 换成数据库用户自定义 UserDetailsService内存用户只适合原型阶段真实项目里用户一定在数据库里。这时需要实现自己的UserDetailsServiceService public class DbUserDetailsService implements UserDetailsService { private final UserMapper userMapper; public DbUserDetailsService(UserMapper userMapper) { this.userMapper userMapper; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user userMapper.findByUsername(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } return org.springframework.security.core.userdetails.User.builder() .username(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles().toArray(new String[0])) .build(); } }有个细节必须说这里的username来自Authorization头解码后的结果要避免直接用它拼接 SQL否则就是典型的注入点。loadUserByUsername只负责按用户名取用户密码校验由DaoAuthenticationProvider内部完成不是我们手动比对的。数据库表里至少要包含用户名、密码BCrypt 哈希、角色三个字段。角色字段可以用逗号分隔存在一个字段里也可以拆成多张关联表取决于项目复杂度。我这边初期只有两类角色直接一个roles字段搞定省事。4. API 暴露范围与权限细分实践4.1 放行规则的正确打开方式permitAll()不代表这个接口不需要任何处理它只是跳过认证过滤器允许匿名访问。使用时有几个常见场景健康检查端点/actuator/health通常要放行但/actuator/env、/actuator/dump这类敏感端点绝对要保护起来。静态资源前端页面、Swagger UI、OpenAPI 文档。部分业务接口比如登录、注册、验证码、公开查询。放行规则的范围控制要尽量精确。有的人图省事直接requestMatchers(/**).permitAll()把所有路径放行然后再用PreAuthorize在方法上做控制。这在小型项目里可行但容易漏保护。我的习惯是默认全部认证少数路径放行这样即使在白名单里漏写了接口结果也只是多了一个要被认证的接口不会出现敏感接口意外暴露的问题。4.2 基于角色的差异化授权当内部调用方不只一个时通常要区分角色。比如普通内部系统账号只能调用查询、推送类接口。管理员账号能调用配置修改、数据导出类接口。在配置类里可以这样扩展.authorizeHttpRequests(auth - auth .requestMatchers(/api/public/**).permitAll() .requestMatchers(/api/internal/query/**).hasAnyRole(USER, ADMIN) .requestMatchers(/api/internal/admin/**).hasRole(ADMIN) .anyRequest().authenticated() )区分这么细之后内部系统的服务账号就用最小权限的 USER 角色避免一个账号泄漏导致所有接口都被调用。之前有一个项目就是因为所有内部服务共用一个超级账号后来某个调用方的人离职后凭证没有回收导致风险面非常大。后来强制按系统拆分账号每个服务一个用户名密码权限各管各的排查问题也容易——看到请求日志里的用户名就知道是哪个系统在调用。4.3 匿名访问时的 401 与专门匿名接口如果你用了permitAll()Spring Security 会为这个请求创建一个匿名认证对象在 Controller 里可以通过AuthenticationPrincipal拿到一个匿名标识。但有个地方容易踩坑Swagger 或 Springdoc 的文档页面需要放行但文档背后的接口还是受保护的浏览器里打开 Swagger 页面没问题点Try it out时如果没有带凭证就会 401。这不是配置失效而是因为 Swagger 页面和 API 接口是两条不同的过滤规则。5. 客户端联调与请求头处理实录5.1 curl 和 Postman 直接验证配置写完第一个要验证的就是 Basic Auth 请求头本身是否生效。根本不需要写代码一条 curl 就够curl -u service:service123 http://localhost:8080/api/internal/query/order/1001curl -u会自动帮我把service:service123做 Base64 编码生成Authorization: Basic c2VydmljZTpzZXJ2aWNlMTIz然后发起请求。如果认证成功返回正常结果如果失败返回 401。也可以在 Postman 里选 Authorization 标签页Type 选 Basic Auth填上用户名密码即可。注意 Postman 的请求头里会生成一个Authorization: Basic xxxxxx这个值你在浏览器里直接手写也是一样的因为 Base64 本身就是公开的编码规则别把它当加密看。我的验证顺序是无凭证访问受保护接口确认返回 401。错误密码访问确认返回 401。正确凭证访问确认返回 200。无凭证访问公开接口确认返回 200。这四个用例走通说明过滤链路是通的。5.2 Java 后端服务之间的调用写法内部系统大多用 Java 用 Spring 的RestTemplate或WebClient调用。以RestTemplate为例HttpHeaders headers new HttpHeaders(); String auth service:service123; byte[] encodedAuth Base64.encodeBase64(auth.getBytes(StandardCharsets.UTF_8)); headers.set(Authorization, Basic new String(encodedAuth)); HttpEntityString entity new HttpEntity(headers); ResponseEntityString resp restTemplate.exchange(url, HttpMethod.GET, entity, String.class);用 Spring Cloud OpenFeign 的项目更简单直接在FeignClient里配请求拦截器或者用配置属性指定用户名密码。我推荐一个方向把用户名密码放到配置中心或环境变量里不要硬编码在代码仓库里。我在维护的项目里见过有人在 Controller 里写死一个Basic YWRtaW46YWRtaW4那和把密码贴在代码里没有区别。5.3 前端浏览器和 JS 调用时的注意事项如果做前后端分离前端在axios里也有原生支持const resp await axios.get(/api/internal/query/order/1001, { auth: { username: service, password: service123 } });浏览器在这种情况下会自动生成请求头并发送。但有一个麻烦如果认证失败浏览器会弹出系统自带的基础认证对话框这个弹窗无法用前端代码优雅控制体验比较差。因此如果是面向真实用户的浏览器应用Basic Auth 通常不是首选它更适合隐藏在前端背后的服务间调用。前端页面若要走认证一般做一个登录页把凭证交给后端由后端用 Basic Auth 去调用其他内部服务而不是让浏览器直接与 Basic Auth 打交道。6. 实际操作中踩过的坑与排查技巧6.1 401 和 403 傻傻分不清这是问得最多的问题。Spring Security 返回 401 表示未认证意味着请求里没有凭证或者凭证校验失败返回 403 表示已认证但没有权限比如你成功登录了但你不是 ADMIN 角色访问管理接口就会 403。排查时先看响应码再看响应体里的异常信息。常见的情况凭证正确却一直 401检查 Basic Auth 字符串拼接Basic后面必须有一个空格大小写也必须是Basic。凭证正确但 403检查角色配置hasRole(ADMIN)需要用户拥有ROLE_ADMIN权限数据库里存的权限标识写法对不对。无凭证访问公开接口也 401检查permitAll()的路径是否精确覆盖了你要放行的路径。6.2 浏览器缓存导致的认证失效假象有一个很隐蔽的问题浏览器一旦在某个域名下成功用 Basic Auth 访问过接口之后访问同一个域名的其他接口浏览器会自动带上之前的凭证这看似是记住密码实际上是浏览器缓存的认证信息。如果后端的账号密码改了浏览器还带着旧凭证去请求你会看到服务端一直返回 401但换了 curl 一测又一切正常。遇到这种情况我通常直接清空浏览器站点数据或者用一个无痕窗口重新验证。这也是为什么我在排查认证问题时让前端同学先别急先用 curl 确认服务端逻辑再怀疑浏览器缓存。6.3 Base64 只是编码不要当加密用Authorization: Basic dXNlcjoxMjM0NTY里的那一串看起来很神秘但任何人都能解码还原成user:123456。只要在网上一搜 Base64 解码工具一秒就能还原。所以 Basic Auth 的两个硬性安全前提是必须在 HTTPS 下运行否则凭证在传输链路中是明文可达的。密码必须存放在服务端的安全介质中并且用 BCrypt 这类算法哈希存储不能明文存数据库。很多人以为用了 Basic Auth 就加密安全了这是个严重的认知偏差。6.4 从 Basic Auth 平滑升级到 Token 体系项目后期如果接入方变多Basic Auth 的缺点会越来越明显每个调用方都要保存一套用户名密码无法做到细粒度的令牌吊销。到时候可以考虑迁移到 Bearer Token 或 JWT。Spring Security 的过滤器链设计允许你在同一个应用中同时支持 Basic Auth 和 Bearer Token.httpBasic(withDefaults()) .oauth2ResourceServer(oauth2 - oauth2 .jwt(jwt - jwt.decoder(jwtDecoder())) )两种方式共存并不会冲突Basic Auth 作为内部服务间调用的轻量认证JWT 作为面向外部应用的用户认证等于把一条过滤器链上的入口扩成两个。我个人的建议是如果你的接口数量少、调用方是完全可以信任的内部服务Basic Auth 是一个性价比极高的方案一旦调用方数量超过三四个或者涉及外部第三方尽早把认证中心化换 Token 体系省得后面一个个接入小系统时疲于奔命。最后再分享一个实践细节生产环境里我为每个外部接入方单独建一个账号密码定期轮换并在网关层记录每次请求的用户名和来源 IP。Basic Auth 这种方案简单但审计和告警一定不能省——认证做好了接下来就要靠日志知道你哪些接口被谁调用了。这个习惯保持下来后面系统出问题定位会快很多。
阅读完成 · 觉得有帮助?