在微服务架构流行的今天,很多团队都会面临一个棘手的问题:多个独立的API服务,各自做一套登录逻辑,不仅开发重复,用户体验也差——比如在订单服务登过录,去用户中心还要再输一遍密码,时间久了用户可能直接流失。同时,权限管理分散,每个服务的接口权限都要单独配置,管理员要在每个服务里改权限,效率极低。要解决这些问题,最常用的方案就是整合Spring Cloud Security和OAuth2,实现统一的认证和权限传递。
一、为什么要整合Spring Cloud Security与OAuth2?
1.1 微服务下的统一认证痛点
之前我接手过一个电商项目,有6个微服务,每个服务都单独写了登录接口,每次加一个服务,就要重复写一套登录校验的代码,而且每次改密码策略,要改6个服务的代码,测试的时候特别麻烦,经常出现某个服务的密码规则没改到位,用户登录失败的情况。另外,用户登录后,每个服务都要单独校验令牌,令牌过期的提示也不一样,用户根本搞不清为什么登录失效。
1.2 该组合的核心价值
把Spring Cloud Security和OAuth2组合起来,核心就是做一个“认证中枢”:所有用户的登录请求都先到这个中枢,拿到统一的令牌后,调用任何微服务都只需要带这个令牌,各个微服务不用管登录逻辑,只需要校验令牌的权限,这样就能实现一次登录、全服务通行,权限也统一管理,不用每个服务重复造轮子。
二、最简可用的示例配置(从0到1搭起来)
这里用的技术栈是Spring Boot 2.7 + Spring Security 5.7 + OAuth2,全程用这个栈,没有混合其他技术,示例都带详细注释,你可以直接复制到项目里跑。
2.1 授权服务器(认证中枢)的配置
授权服务器就是统一的登录入口,负责给登录的用户发令牌,配置分两部分:首先是application.yml的配置,然后是Java的Security配置。
# 授权服务器的application.yml配置
server:
port: 9000 # 认证中枢的端口,固定用9000方便测试
spring:
security:
user:
name: admin # 测试时的用户名,实际项目要从数据库查
password: $2a$10$EixZaY999z2u8fLqX7pYLe9K9t9fLqX7pYLe9K9t9fLqX7pYLe9K # 加密后的密码,实际项目用BCrypt加密
application:
name: auth-server # 服务名,后面资源服务要对接这个
oauth2:
issuer-uri: http://localhost:9000 # 授权中心的地址,资源服务必须和这个一致
然后是Java的配置,定义授权规则和令牌生成逻辑:
// 授权服务器核心配置,全是必要代码,没有多余逻辑
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
// 密码加密器,Spring Security要求必须用,防止明文密码
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置客户端(这里的客户端就是前端或者App,用来拿令牌的)
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 测试用内存存储,实际项目用数据库
.withClient("frontend-client") // 客户端ID,前端要对应这个值
.secret(passwordEncoder().encode("frontend-secret")) // 客户端密钥,加密存储
.authorizedGrantTypes("password", "authorization_code") // 支持两种登录模式:密码模式(测试用)和授权码模式(生产用)
.scopes("user", "admin") // 权限范围,对应后面接口的权限要求
.accessTokenValiditySeconds(3600); // 令牌有效期1小时,太长不安全,太短用户体验差
}
// 配置令牌的生成规则,用JWT,非对称加密保证安全
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
}
// JWT令牌存储,用来生成和解析令牌
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
// JWT转换器,配置签名算法,用RS256非对称加密,比对称加密安全
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyPairGenerator().generateKeyPair(); // 实际项目要存好公私钥,不要每次生成
converter.setKeyPair(keyPair);
return converter;
}
}
2.2 资源服务器(API服务)的配置
资源服务器就是各个微服务,比如订单服务、用户服务,负责处理业务逻辑,只需要校验令牌的权限:
// 资源服务器配置,所有微服务都用这个,只要改接口权限就行
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll() // 公开接口,不用认证,比如首页
.antMatchers("/user/**").hasAuthority("SCOPE_user") // 要求user权限的接口,对应授权服务器的scope
.antMatchers("/admin/**").hasAuthority("SCOPE_admin") // 要求admin权限的接口
.anyRequest().authenticated(); // 其他所有接口都需要认证
}
// 和授权服务器的令牌解析器一致,保证能正确校验令牌
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyPairGenerator().generateKeyPair(); // 要和授权服务器的密钥对一致
converter.setKeyPair(keyPair);
return converter;
}
}
三、必须避开的权限传递陷阱(我踩过的坑全在这里)
很多人整合这个组合的时候,跑不通或者权限不对,都是踩了这些坑,我整理了最常见的三个:
3.1 陷阱一:令牌校验失败,401错误
我之前第一次搭的时候,授权服务器用了RS256非对称加密,结果资源服务器图省事,配成了HS256对称加密,导致每次请求都报401,后来查了半天才发现:JWT的签名算法必须和授权服务器的一致,资源服务器要正确拿到授权中心的公钥,或者用相同的密钥对,否则校验令牌会失败。解决方法:资源服务器的JWT转换器必须和授权服务器用同一个密钥对,或者直接配置授权中心的公钥地址,不要随便改算法。
3.2 陷阱二:跨服务调用时令牌丢失,下游服务不认
这个坑更常见:比如前端调用订单服务(A),A服务需要调用用户服务(B)拿收货地址,但是A服务调用B的时候,没有把前端传过来的Authorization令牌一起带过去,结果B服务返回401。解决方法:用Feign或者RestTemplate的拦截器,自动把当前请求的令牌带到下游请求里,不用手动传。这里给Feign的配置(Spring Cloud常用的微服务调用工具):
// Feign拦截器,自动传递当前请求的Authorization令牌,所有跨服务调用都能用
@Configuration
public class FeignTokenInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从当前Spring Security的上下文里拿到当前登录用户的令牌
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
// 把令牌放到下游请求的Header里,名字是Authorization,固定格式是Bearer 令牌值
template.header("Authorization", details.getTokenType() + " " + details.getTokenValue());
}
}
}
3.3 陷阱三:scope权限配错,接口返回403
这个坑是新手最容易踩的:授权服务器给的权限是user,但是资源服务器的接口用的是hasRole("USER"),或者反过来,OAuth2里的scope权限默认是SCOPE_开头的,资源服务里要写hasAuthority("SCOPE_user"),而不是hasRole("USER"),我之前就是把接口权限写成了hasRole("user"),导致用户有user权限也访问不了,找了好久才发现这个小细节。另外,还要注意授权服务器的scope和资源服务器的权限对应,不要写错名字。
四、该技术组合的优缺点与注意事项
4.1 优点
- 统一认证:所有服务用一个登录入口,不用重复写登录逻辑;
- 权限集中:权限都在认证中枢配置,不用每个服务改;
- 适合微服务:多服务架构下,能大幅减少开发量,提升效率。
4.2 缺点
- 配置复杂:需要对齐授权服务器、资源服务器的配置,调试麻烦;
- 依赖中心:如果认证中枢挂了,所有服务都没法登录,要做高可用;
- 踩坑多:令牌传递、权限配置的坑不少,新手容易卡壳。
4.3 注意事项
- 令牌加密用非对称算法:比如RS256,不要用HS256,避免密钥泄露;
- 令牌有效期合理:不要太长(比如超过2小时),也不要太短(比如小于10分钟),用刷新令牌(refresh_token)处理过期;
- 跨服务自动传令牌:一定要用拦截器,不要手动写,避免漏传;
- scope和接口权限对应:资源服务的权限要和授权服务器的scope一致,不要写错名字。
五、总结
整合Spring Cloud Security和OAuth2是微服务统一认证的主流方案,只要避开我刚才说的三个核心陷阱,配置好授权服务器和资源服务器,就能实现一次登录、全服务通行,权限统一管理,大大减少重复开发的工作量。这个方案适合有3个以上微服务的项目,不管是电商、SaaS还是其他架构,都能稳定运行,只要注意配置的一致性和令牌的安全,就能解决绝大多数认证和权限的问题。
Comments