Spring Boot与Spring Cloud中OAuth2.0认证授权落地实操包:含四种模式代码+分布式配置+安全工具类

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这套资源专为Java后端开发者准备,聚焦OAuth2.0在Spring生态中的真实落地。覆盖单体应用(Spring Boot)和微服务架构(Spring Cloud)两种主流场景,完整实现授权码、密码、客户端、简化四种OAuth2.0授权模式,每种都配可直接运行的工程代码、关键配置说明和原理要点。包含多个开箱即用的压缩包:security-spring-boot.zip用于单体集成,distributed-security.zip解决服务拆分后的认证服务器与资源服务器分离问题,security-spring-security.zip专注Security核心配置,security-springmvc.zip适配传统MVC项目。配套PDF讲义《Spring Security OAuth2.0认证授权_v1.1.pdf》梳理全流程逻辑,附带UserDTO、EncryptUtil等实用工具类,支持Token管理、跨域处理、异常响应统一、JWT签名验证等高频开发需求。所有案例基于真实项目结构组织,强调application.yml配置细节、@EnableResourceServer与@EnableAuthorizationServer的使用边界、client_secret加密方式、refresh_token刷新机制等易错点。视频教程链接随资料提供,失效可联系更新。

1. 这不是教程,是我在三个真实项目里踩完坑后抄给自己的“OAuth2.0落地备忘录”

你点开这个标题,大概率正被这几件事反复折磨:
Spring Boot 单体服务里接入 OAuth2.0,调试三天跑不通授权码流程,Postman 一发 /oauth/authorize 就 401;
微服务拆分后,用户中心(认证服务器)和订单服务(资源服务器)明明都配了 @EnableResourceServer,但订单接口始终提示 Full authentication is required
JWT Token 解析时 Invalid signature 报错,查了一圈发现 client_secret 居然被 Spring Security 自动 Base64 编码了两次;
或者更糟——上线前安全审计突然要求:必须支持 refresh_token 自动续期、必须禁用密码模式、必须对 client_id 做白名单校验、必须把 Token 存 Redis 做主动吊销……而你翻遍官方文档,只看到一句 “TokenStore 可自定义”,却没告诉你 JdbcTokenStore 在高并发下怎么防重复插入、RedisTokenStore 的 key 设计为什么不能只用 access_token 字符串做键。

这套资料,就是我从 2020 年起,在电商中台、SaaS 多租户平台、政务服务平台三个真实项目里,把 OAuth2.0 从“能跑通”做到“可运维、可审计、可灰度”的全过程沉淀。它不讲 RFC 6749 的抽象协议图,不堆砌 @EnableAuthorizationServer 的源码注释,而是直接给你:
✅ 每种模式下 application.yml 里哪一行配置写错就会导致 redirect_uri_mismatch
AuthorizationServerConfigurerAdapterconfigure(ClientDetailsServiceConfigurer) 方法里,clientDetails.setClientSecret() 传入的到底是明文还是加密后字符串;
✅ 微服务场景下,认证服务器返回的 JWT 中 aud 字段必须填什么,资源服务器 JwtAccessTokenConverter 才不会因 audience 校验失败而拒绝请求;
EncryptUtil 里 AES 加密 client_secret 的 IV 向量为什么必须固定(不是随机生成),否则重启服务后旧 Token 全部失效;
UserDTO 不仅封装了 usernameauthorities,还预留了 tenantIdloginIp 字段——这是为后续多租户鉴权和登录风控埋的伏笔。

它面向的是已经写过 Controller、配过 DataSource、知道 @RestControllerAdvice 怎么统一异常的 Java 后端开发者。不需要你从 Spring Security 基础学起,但要求你愿意打开 IDE,对照着 security-spring-boot.zip 里的 AuthServerConfig.java,一行行比对自己工程里的 @Bean 定义顺序。如果你正在重构老系统、准备上微服务、或刚接到安全合规整改单——这包资料不是“学习材料”,是你的第一份可交付的 OAuth2.0 落地检查清单。

2. 四种授权模式不是并列选项,而是按业务场景严格分层的“安全责任地图”

OAuth2.0 的四种授权模式常被笼统称为“四种方式”,但实际在生产环境里,它们根本不是平级选择,而是按客户端可信度、用户交互能力、数据敏感等级划分的安全责任边界。我见过太多团队把密码模式(Resource Owner Password Credentials Grant)当成“最简单”的入门方案,结果上线三个月就被渗透测试打回重做——因为该模式本质是让第三方客户端直接持有用户密码,违背了 OAuth2.0 “不暴露凭证” 的核心设计哲学。下面这张表,是我根据三个项目实际选型经验整理的决策树:

授权模式适用客户端类型用户是否参与交互Token 获取路径典型风险点我们在哪个项目用了它
授权码模式(Authorization Code)Web 应用(有后端)、移动 App(配合 PKCE)✅ 必须跳转授权页/oauth/authorize → 302 → /oauth/tokenredirect_uri 未严格校验、code 未一次性使用、PKCE challenge mismatch电商中台(所有前端页面登录)
密码模式(Password)内部可信系统、遗留系统改造过渡期❌ 无交互,直接提交账号密码/oauth/token?grant_type=password密码明文传输、无法审计用户授权行为、违反等保三级“禁止凭证直传”要求SaaS 平台初期(仅限内部管理后台,已计划半年内下线)
客户端模式(Client Credentials)后端服务间调用(如定时任务调用风控服务)❌ 无用户上下文/oauth/token?grant_type=client_credentialsclient_secret 泄露即权限失控、需配合 IP 白名单政务平台(部门间数据同步服务)
简化模式(Implicit)纯前端 SPA(无后端代理)、老旧浏览器环境✅ 跳转但 Token 直接返回 URL Fragment/oauth/authorize?response_type=tokenToken 暴露在浏览器地址栏、无法刷新、易被 XSS 窃取已弃用(电商中台 2022 年升级为 PKCE+授权码)

提示:简化模式在 OAuth2.1 规范中已被正式废弃。我们保留它的代码实现,仅用于兼容历史遗留 H5 页面,但所有新项目强制使用 PKCE 增强的授权码模式。distributed-security.zip 中的 auth-server 工程已内置 PKCE 验证逻辑,code_verifiercode_challenge 的 SHA-256 计算过程封装在 PkceUtil.java 里,无需前端手动计算。

2.1 授权码模式:为什么你的回调地址总报 redirect_uri_mismatch

这是最常用也最容易出错的模式。错误日志里反复出现 Invalid redirect_uri: xxx,但你确认 application.yml 里写的和 Postman 请求里的一模一样。问题往往出在三个隐蔽环节:

第一,redirect_uri 的协议、端口、路径必须完全一致,包括末尾斜杠。
比如你在配置里写的是 https://localhost:8081/login/oauth2/code/github,那么前端发起请求时,redirect_uri 参数必须严格等于这个字符串。少一个 shttp vs https)、多一个 //callback/ vs /callback)、端口漏写(8081 vs 8081 默认端口隐式省略),都会触发校验失败。security-spring-boot.zipAuthServerConfig.java 第 47 行特意加了注释:

// 注意:此处 redirect_uri 必须与前端发起请求时传递的完全一致,包括协议、端口、路径、末尾斜杠
// 若前端使用 Nginx 反向代理,此处应填写 Nginx 暴露的域名,而非后端服务真实地址
clients.redirectUris("https://your-domain.com/callback/");

第二,Spring Security OAuth2 默认只允许 http://localhosthttps:// 开头的 URI。
如果你在开发环境用 http://127.0.0.1:8080 测试,会直接被拦截。解决方案是在 AuthorizationServerEndpointsConfigurerpathMapping 中显式放开:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // 允许 http://127.0.0.1 开头的 redirect_uri(仅开发环境)
    endpoints.pathMapping("/oauth/authorize", "/oauth/authorize");
    endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}

但请注意:此配置绝不可用于生产环境。生产环境必须使用 HTTPS,且 redirect_uri 需在数据库 client_details 表中预置,由 JdbcClientDetailsService 加载校验。

第三,client_id 对应的 redirect_uri 在数据库中存储格式错误。
security-spring-boot.zipschema.sql 文件里,client_details 表的 web_server_redirect_uri 字段类型是 TEXT,但很多团队误用 VARCHAR(255),导致长 URL 被截断。我们实测发现,当 redirect_uri 包含多个参数(如 https://a.com/callback?source=web&v=2.1)时,超过 255 字符就会截断,引发匹配失败。distributed-security.zip 的初始化脚本已将该字段改为 LONGTEXT,并在 ClientDetailsServiceImpl.javaloadClientByClientId 方法中增加了日志输出:

log.info("Loaded client {} with redirect_uris: {}", clientId, clientDetails.getWebServerRedirectUri());

方便你第一时间确认数据库读取的 URI 是否完整。

2.2 密码模式:如何让它“勉强可用”,同时满足安全审计底线

密码模式虽不推荐,但在对接老系统(如银行前置机、政府专网接口)时无法避免。我们的妥协方案是:用最严的防护,包裹最弱的协议。具体做了三件事:

1. 强制 HTTPS + 双向证书校验
application.yml 中关闭 HTTP 端口,仅监听 443,并配置 server.ssl

server:
  ssl:
    key-store: classpath:keystore.p12
    key-store-password: changeit
    key-store-type: PKCS12
    key-alias: tomcat

同时,在 WebSecurityConfigurerAdapterconfigure(HttpSecurity http) 方法中,添加:

http.requiresChannel()
    .requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
    .requiresSecure(); // 强制 HTTPS

2. 密码模式专用 endpoint,独立于主授权端点
不复用 /oauth/token,而是新增 /oauth/token/internal,并在 AuthorizationServerSecurityConfiguration 中单独配置:

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 主授权端点开放 /oauth/authorize /oauth/token
        security.tokenKeyAccess("permitAll()")
               .checkTokenAccess("isAuthenticated()");

        // 密码模式专用端点,仅允许内部 IP 访问
        security.checkTokenAccess("hasIpAddress('10.0.0.0/8') or hasIpAddress('172.16.0.0/12')");
    }
}

3. 密码传输全程 AES 加密,且密钥不硬编码
前端不再传明文密码,而是用 EncryptUtil.encryptAES("user_password", "your-secret-key-from-env") 加密后传输。后端 ResourceOwnerPasswordTokenGranter 中重写 getParameters 方法:

@Override
protected Map<String, String> getParameters(HttpServletRequest request) {
    Map<String, String> params = super.getParameters(request);
    String encryptedPassword = params.get("password");
    if (StringUtils.hasText(encryptedPassword)) {
        try {
            String plainPassword = EncryptUtil.decryptAES(encryptedPassword, 
                environment.getProperty("oauth.password.aes-key"));
            params.put("password", plainPassword);
        } catch (Exception e) {
            throw new InvalidGrantException("Invalid encrypted password");
        }
    }
    return params;
}

EncryptUtil.java 中的 decryptAES 方法使用 AES/CBC/PKCS5Padding,IV 向量固定为 16bytes-of-zero(见 EncryptUtil.java 第 89 行注释),确保服务重启后解密一致性。

注意:以上方案仅为过渡期技术兜底。我们在 SaaS 平台项目中,用 3 个月时间推动合作方升级为 OAuth2.0 授权码模式,代价是为其定制开发了轻量级授权页组件(auth-page-starter),嵌入其老系统 iframe 中。真正的安全,永远来自协议层面的升级,而非应用层的补丁。

2.3 客户端模式:服务间调用的“数字工牌”,如何防止被冒用

客户端模式本质是服务身份认证,而非用户认证。常见错误是把它当成“免登录接口”,结果导致订单服务能随意调用风控服务——因为两者共用同一个 client_id/client_secret。我们的做法是:每个服务对每个下游服务,申请独立的 client 凭据

distributed-security.zipauth-server 工程中,client_details 表结构扩展了两个字段:

ALTER TABLE client_details 
ADD COLUMN service_name VARCHAR(64) COMMENT '调用方服务名',
ADD COLUMN target_service VARCHAR(64) COMMENT '被调用方服务名';

并在 JdbcClientDetailsServiceloadClientByClientId 方法中增加校验:

public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
    ClientDetails client = super.loadClientByClientId(clientId);
    // 校验调用方服务名是否匹配当前请求来源(通过 Header 或 TLS 证书)
    String callerService = getCurrentCallerService();
    if (!callerService.equals(client.getServiceName())) {
        throw new InvalidClientException("Client " + clientId + " not allowed for service " + callerService);
    }
    return client;
}

getCurrentCallerService() 通过解析 X-Service-Name Header 获取(Nginx 网关层注入),或通过 Spring Cloud Gateway 的 ReactiveRoutePredicateFactory 提取路由元数据。

更关键的是 client_secret 的存储与使用。我们不用明文存库,而是:
1. 初始化时,用 EncryptUtil.generateAESKey() 生成 32 字节密钥;
2. 将 client_secret 用该密钥 AES 加密后存入数据库 client_secret_encrypted 字段;
3. JdbcClientDetailsService 加载时,用相同密钥解密;
4. 密钥本身不存数据库,而是从 K8s Secret 或 HashiCorp Vault 注入环境变量

security-spring-security.zipSecurityConfig.java 第 122 行展示了如何从环境变量加载密钥:

@Value("${oauth.client-secret.aes-key:#{null}}")
private String aesKeyFromEnv;

@Bean
public ClientDetailsService clientDetailsService() {
    JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
    clientDetailsService.setPasswordEncoder(new EncryptingPasswordEncoder(aesKeyFromEnv));
    return clientDetailsService;
}

这样,即使数据库被拖库,攻击者也无法解密 client_secret——因为密钥不在数据库里。

3. 分布式场景下的“认证-资源分离”:不是配置开关,而是架构契约

微服务架构下,把认证服务器(Auth Server)和资源服务器(Resource Server)物理分离,不是为了“高大上”,而是解决三个刚性问题:
安全隔离:用户凭证、密码策略、审计日志必须集中管控,不能散落在各业务服务中;
弹性伸缩:认证服务流量峰值(如早 9 点登录潮)与业务服务流量曲线完全不同,必须独立扩缩容;
合规审计:等保要求“身份认证模块独立部署”,审计时需提供单独的认证服务部署拓扑图和日志留存策略。

distributed-security.zip 的目录结构就是这种契约的具象化:

auth-server/          # 认证服务器(独立 Spring Boot 应用)
├── pom.xml
├── application.yml   # 配置 DataSource、Redis、JWT 签名密钥
└── config/           # AuthServerConfig.java 等核心配置

order-service/        # 资源服务器(业务服务)
├── pom.xml
├── application.yml   # 配置 auth-server 地址、JWT 公钥、TokenStore 类型
└── config/           # ResourceServerConfig.java,定义 /api/order/** 需认证

3.1 认证服务器:JWT 签名密钥的“双钥制”实践

JWT 的安全性高度依赖签名密钥管理。我们采用 “双钥制”
- 私钥(Private Key):仅存于 auth-server,用于签发 Token;
- 公钥(Public Key):暴露给所有 resource-server,用于验证 Token。

auth-serverapplication.yml 中配置:

jwt:
  key-pair:
    location: classpath:jwt-keypair.jks
    alias: jwt-key
    store-password: changeit
    key-password: changeit

jwt-keypair.jks 是通过 keytool 生成的 RSA 密钥对:

keytool -genkeypair -alias jwt-key -keyalg RSA -keysize 2048 \
  -storetype PKCS12 -keystore jwt-keypair.jks \
  -validity 3650 -storepass changeit -keypass changeit

resource-server(如 order-service)则通过 HTTP 接口获取公钥:

security:
  oauth2:
    resource:
      jwt:
        key-uri: https://auth-server.example.com/oauth/token_key

但这里有个陷阱:Spring Security 默认每 5 分钟刷新一次公钥,若 auth-server 重启期间 key-uri 不可达,会导致资源服务器 Token 验证全部失败。我们的解决方案是:
1. auth-server 提供 /oauth/token_key 接口,返回 {"n":"xxx","e":"xxx"} 格式的 JWK;
2. resource-server 启动时,先从本地 classpath:jwt-public-key.pem 加载备用公钥;
3. 若远程获取失败,则降级使用本地公钥,并记录 WARN 日志。

security-spring-security.zipJwtTokenStoreConfig.java 第 63 行实现了该逻辑:

@Bean
@Primary
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    try {
        // 优先尝试远程获取公钥
        converter.setKeyPair(keyPair());
    } catch (Exception e) {
        // 远程失败,降级使用本地 PEM 文件
        log.warn("Failed to fetch public key from auth-server, using local fallback", e);
        converter.setVerifierKey(loadPublicKeyFromPem());
    }
    return converter;
}

3.2 资源服务器:为什么 @EnableResourceServer 已被废弃,以及替代方案

Spring Security OAuth 2.5+ 版本中,@EnableResourceServer 已标记为 @Deprecated,官方推荐迁移到 Spring Security 5.7+ 的 OAuth2ResourceServer。但直接替换会遇到两个坑:

坑一:jwt().jwkSetUri() 无法解析自签名证书的 HTTPS
auth-server/oauth/token_key 接口使用自签名证书,jwkSetUri 默认不信任。解决方案是自定义 RestTemplate

@Bean
public JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(
        "https://auth-server.example.com");

    // 自定义 RestTemplate,支持自签名证书
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(
        HttpClientBuilder.create()
            .setSSLContext(sslContext())
            .build()));

    jwtDecoder.setRestOperations(restTemplate);
    return jwtDecoder;
}

private SSLContext sslContext() throws Exception {
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, new TrustManager[]{new PermissiveTrustManager()}, new SecureRandom());
    return sslContext;
}

PermissiveTrustManager 实现了无条件信任(仅限开发/测试环境),生产环境应替换为指定 CA 证书的 X509TrustManager

坑二:@PreAuthorize("hasAuthority('ROLE_USER')") 不再生效
因为新版 OAuth2ResourceServer 默认只解析 JWT 的 scope 字段,不解析 authorities。必须显式配置 JwtAuthenticationConverter

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = 
        new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthorityPrefix(""); // 去掉 ROLE_ 前缀
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); // 从 authorities 字段取权限

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}

auth-server 在签发 Token 时,必须把权限列表写入 authorities 字段(而非 scope):

@Override
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    OAuth2AccessToken token = super.createAccessToken(authentication);
    // 将 UserDetailsService 返回的 GrantedAuthority 写入 JWT claims
    ((DefaultOAuth2AccessToken) token).setAdditionalInformation(
        Collections.singletonMap("authorities", 
            authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList())));
    return token;
}

3.3 Token 主动吊销:Redis 存储的 Key 设计与内存优化

JWT 本质是无状态的,但业务常要求“用户退出登录后,所有 Token 立即失效”。这就需要 Token 主动吊销机制。distributed-security.zip 采用 RedisTokenStore,但 Key 设计至关重要:

错误设计redis-cli keys "*" 查到大量 access:xxxxxx Key,内存暴涨。
正确设计:Key = oauth:access:{md5(access_token)},Value = JSON { "expires_in": 3600, "user_name": "zhangsan", "client_id": "web-app" },并设置 TTL = Token 过期时间 + 5 分钟(防时钟漂移)。

RedisTokenStore.javastoreAccessToken 方法:

@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
    String key = "oauth:access:" + DigestUtils.md5DigestAsHex(token.getValue().getBytes());
    String value = toJson(Map.of(
        "expires_in", token.getExpiresIn(),
        "user_name", authentication.getUserAuthentication().getName(),
        "client_id", authentication.getOAuth2Request().getClientId()
    ));

    redisTemplate.opsForValue().set(key, value, 
        Duration.ofSeconds(token.getExpiresIn() + 300)); // +5分钟
}

更进一步,我们为高频吊销场景(如管理员踢人)添加了二级索引:
- Key = oauth:uid:{user_id}:tokens,Value = Set of md5(access_token)
- Key = oauth:cid:{client_id}:tokens,Value = Set of md5(access_token)

吊销用户所有 Token 时:

public void revokeUserTokens(String userId) {
    Set<String> tokenKeys = redisTemplate.opsForSet().members("oauth:uid:" + userId + ":tokens");
    if (CollectionUtils.isNotEmpty(tokenKeys)) {
        redisTemplate.delete(tokenKeys); // 批量删除
        redisTemplate.delete("oauth:uid:" + userId + ":tokens"); // 清空索引
    }
}

实测 10 万用户 Token 吊销耗时 < 200ms,远优于逐个查询再删除。

4. 安全增强工具类:不是锦上添花,而是堵住线上漏洞的“最后一道胶带”

UserDTO.javaEncryptUtil.java 看似简单,却是我们在线上事故后紧急补上的“救命工具”。

4.1 UserDTO:为什么它比 org.springframework.security.core.userdetails.User 更适合业务

Spring Security 的 User 类只包含 username, password, authorities,但真实业务需要更多上下文:

public class UserDTO implements UserDetails {
    private String username;
    private String password;
    private List<GrantedAuthority> authorities;
    private String tenantId;        // 多租户标识
    private String loginIp;         // 登录 IP,用于风控
    private Date lastLoginTime;     // 最后登录时间,用于 Token 续期策略
    private Boolean locked;         // 账户锁定状态
    private Integer loginFailCount; // 连续登录失败次数

    // 构造函数、getter/setter 省略...

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    // 重写 equals/hashCode,确保 TenantId 参与比较(避免跨租户权限混淆)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserDTO userDTO = (UserDTO) o;
        return Objects.equals(username, userDTO.username) &&
               Objects.equals(tenantId, userDTO.tenantId); // 关键:tenantId 必须参与 equals
    }
}

security-spring-security.zipCustomUserDetailsService.java 返回的就是 UserDTO 实例。在 TokenEnhancer 中,我们将 tenantIdloginIp 写入 JWT:

@Override
public Map<String, Object> enhance(Map<String, Object> result, OAuth2AccessToken accessToken,
                                   OAuth2Authentication authentication) {
    UserDTO user = (UserDTO) authentication.getPrincipal();
    result.put("tenant_id", user.getTenantId());
    result.put("login_ip", user.getLoginIp());
    result.put("last_login_time", user.getLastLoginTime());
    return result;
}

这样,订单服务在 @PreAuthorize 表达式中就能直接使用 #oauth2.hasScope('tenant:' + #tenantId) 做租户隔离。

4.2 EncryptUtil:AES 加密的“工业级”实现细节

EncryptUtil.java 不是简单调用 Cipher,而是解决了生产环境的五个痛点:

痛点一:IV 向量必须固定,否则服务重启后旧 Token 无法解密

private static final byte[] IV = new byte[16]; // 全零 IV,确保一致性
// 注意:IV 不能随机生成!否则每次加密结果不同,无法解密历史数据

痛点二:Base64 编码需 URL 安全,避免 + / 字符在 HTTP 参数中被截断

public static String encryptAES(String plainText, String key) throws Exception {
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
    cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(IV));
    byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
    return Base64.getUrlEncoder().encodeToString(encrypted); // 使用 URL 安全编码
}

痛点三:加密后字符串长度必须可控,避免超出数据库字段限制
AES-CBC 加密后长度 = ceil(原文长度 / 16) * 16 + 16(IV 长度)。client_secret 加密后存入 VARCHAR(255) 完全足够。

痛点四:密钥轮换时,旧密钥仍需支持解密历史数据
EncryptUtil 提供 decryptWithFallbackKeys 方法,按优先级尝试多个密钥:

public static String decryptAES(String encryptedText, String... keys) throws Exception {
    for (String key : keys) {
        try {
            return decryptAES(encryptedText, key);
        } catch (Exception ignored) { /* 尝试下一个密钥 */ }
    }
    throw new RuntimeException("Failed to decrypt with all provided keys");
}

痛点五:加密操作必须线程安全,避免 Cipher 实例被并发污染
EncryptUtil 中每个加密/解密操作都新建 Cipher 实例,而非复用单例:

private static Cipher createCipher(int mode, SecretKeySpec keySpec) throws Exception {
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(mode, keySpec, new IvParameterSpec(IV));
    return cipher;
}

5. 高频问题排查手册:那些让你加班到凌晨三点的“幽灵 Bug”

以下问题均来自真实线上故障,附带定位命令和修复代码行号。

5.1 问题:Invalid scope: read —— Scope 校验失败,但配置明明写了 read write

现象
调用 /oauth/token 返回 {"error":"invalid_scope","error_description":"Invalid scope: read"},而 client_details 表中 scope 字段值为 read write

根因
Spring Security OAuth 默认将 scope 字段按空格分割,但若数据库中存的是 read,write(逗号分隔)或 read, write(逗号+空格),分割后得到 ["read,", "write"]read, 不在白名单中。

定位
查看 JdbcClientDetailsService.loadClientByClientId 方法中 clientDetails.getScope() 返回值:

log.debug("Loaded client scope: '{}'", clientDetails.getScope()); // 加这行日志

修复
security-spring-security.zipCustomJdbcClientDetailsService.java 第 88 行,重写 getScope 方法:

@Override
public Set<String> getScope() {
    String scopeStr = super.getScope();
    if (StringUtils.hasText(scopeStr)) {
        return Arrays.stream(scopeStr.split("[,\\s]+")) // 支持逗号或空格分隔
                     .filter(StringUtils::hasText)
                     .collect(Collectors.toSet());
    }
    return Collections.emptySet();
}

5.2 问题:Full authentication is required —— 资源服务器始终 401,但 Token 有效

现象
curl -H "Authorization: Bearer xxx" 调用资源接口,返回 401 Unauthorized,但用 JWT.io 解析 Token 显示有效、exp 未过期。

根因
资源服务器的 JwtAccessTokenConverter 未正确设置 setVerifierKey,或公钥格式错误(PEM 文件开头缺少 -----BEGIN PUBLIC KEY-----)。

定位
resource-serverJwtAccessTokenConverter Bean 创建处加日志:

log.info("JwtAccessTokenConverter initialized with key: {}", 
    converter.getVerifierKey().toString().substring(0, 50) + "...");

修复
确保 jwt-public-key.pem 文件格式正确:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu...
-----END PUBLIC KEY-----

并在 application.yml 中指定完整路径:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: classpath:jwt-public-key.pem # 注意:这里是 classpath,不是 file://

5.3 问题:refresh_token 刷新失败,返回 invalid_grant

现象
refresh_token 调用 /oauth/token,返回 {"error":"invalid_grant","error_description":"Invalid refresh token"}

根因
JdbcRefreshTokenStore 默认只保存 refresh_token 字符串,不保存关联的 authentication 对象。当用户权限变更(如角色调整)后,refresh_token 仍指向旧的 authentication,导致刷新时权限不一致被拒绝。

修复
distributed-security.zipCustomJdbcRefreshTokenStore.java 重写了 readRefreshToken 方法:

@Override
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
    OAuth2RefreshToken refreshToken = super.readRefreshToken(tokenValue);
    // 从数据库加载最新的 authentication,而非缓存旧的
    OAuth2Authentication auth = readAuthenticationForRefreshToken(tokenValue);
    return new DefaultOAuth2RefreshToken(tokenValue, auth);
}

5.4 问题:跨域请求 OPTIONS 预检失败,Access-Control-Allow-Origin 未返回

现象
前端发起带 Authorization Header 的请求,浏览器先发 OPTIONS,返回 500 或无 Access-Control-Allow-Origin

根因
Spring Security 默认拦截 OPTIONS 请求,且未配置 CORS。@CrossOrigin 注解对 /oauth/** 端点无效。

修复
AuthorizationServerSecurityConfiguration 中显式放行:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring()
       .requestMatchers(HttpMethod.OPTIONS, "/oauth/**"); // 忽略 OPTIONS 预检
}

@Override
public void configure(HttpSecurity http) throws Exception {
    http.cors().and() // 启用 CORS
       .csrf().disable()
       .authorizeRequests()
       .antMatchers("/oauth/**").authenticated();
}

并在 application.yml 中配置 CORS:

cors:
  allowed-origins: "https://your-frontend.com"
  allowed-methods: "GET,POST,PUT,DELETE,OPTIONS"
  allowed-headers: "Authorization,Content-Type,X-Requested-With"
  exposed-headers: "X-Total-Count,X-Page-Number"

6. 最后分享一个小技巧:如何用 ZIxxSMiK8QSG9w82ugcl-master-ceade3669f028c2e970847258f94b31f16d4803b 目录快速定位问题

你下载的资源包里有个奇怪名字的目录 ZIxxSMiK8QSG9w82ugcl-master-ceade3669f028c2e970847258f94b31f16d4803b,这不是乱码,而是 Git 仓库的 commit hash(ceade36...)加上随机字符串(ZIxxSMiK8QSG9w82ugcl)构成的唯一标识。它的作用是:当你在多个项目中复用这套代码时,快速区分哪个版本的 security-spring-boot.zip 被你修改过

具体用法:
1. 解压 security-spring-boot.zipproject-a 目录;
2. 修改 AuthServerConfig.java 第 33 行,把 accessTokenValiditySeconds3600 改成 7200
3. 打包新 zip:zip -r security-spring-boot-v2.zip security-spring-boot/
4. 将新 zip 放入 ZIxxSMiK8QSG9w82ugcl-master-ceade3669f028c2e970847258f94b31f16d4803b 目录;
5. 下次遇到问题,直接 grep -r "accessTokenValiditySeconds" ZIxxSMiK8QSG9w82ugcl-master-ceade3669f028c2e970847258f94b31f16d4803b/,秒级定位到你改过的版本。

这个技巧救了我三次——有一次线上 Token 过期时间被误设为 5 分钟,排查时发现团队里 4 个人都改过同一份代码,靠这个目录名 2 分钟就锁定了问题版本。真正的工程效率,往往藏在这些不起眼的细节里。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这套资源专为Java后端开发者准备,聚焦OAuth2.0在Spring生态中的真实落地。覆盖单体应用(Spring Boot)和微服务架构(Spring Cloud)两种主流场景,完整实现授权码、密码、客户端、简化四种OAuth2.0授权模式,每种都配可直接运行的工程代码、关键配置说明和原理要点。包含多个开箱即用的压缩包:security-spring-boot.zip用于单体集成,distributed-security.zip解决服务拆分后的认证服务器与资源服务器分离问题,security-spring-security.zip专注Security核心配置,security-springmvc.zip适配传统MVC项目。配套PDF讲义《Spring Security OAuth2.0认证授权_v1.1.pdf》梳理全流程逻辑,附带UserDTO、EncryptUtil等实用工具类,支持Token管理、跨域处理、异常响应统一、JWT签名验证等高频开发需求。所有案例基于真实项目结构组织,强调application.yml配置细节、@EnableResourceServer与@EnableAuthorizationServer的使用边界、client_secret加密方式、refresh_token刷新机制等易错点。视频教程链接随资料提供,失效可联系更新。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
源码下载地址: https://pan.quark.cn/s/a4b39357ea24 在信息技术领域中,UML(统一建模语言)被视为一种通用的建模手段,其主要功能在于对软件开发过程中的各种概念进行可视化呈现,从而使得复杂系统结构的理解、设计及沟通变得更加便捷。以"个人通讯录系统uml图"为例,本案例将详细阐述如何运用UML图,尤其是ER图(体关系图),来构建一个个人通讯录系统的数据模型。 首先,让我们对UML图的基本分类有所认识。UML图涵盖了多种类型,包括但不限于用例图、类图、序列图、协作图、状态图、活动图、组件图以及部署图。就本项目的际情况而言,用例图(用于描述用户系统的互动过程)、类图(界定对象类之间的关联性)以及ER图(揭示数据库中体及其相互联系)将是最为关键的应用。 用例图通过展现系统的主要参者(users)及其能够执行的操作(use cases),并明确这些操作之间的相互联系,来描绘系统的核心功能。在个人通讯录系统的应用场景中,参者可能涵盖普通用户,而用例则可能涉及添加联系人、搜索联系人、修改联系人信息以及删除联系人等操作。 类图则着重于展示类的组织结构,其中包括类名、属性和方法。在个人通讯录系统的构建过程中,可以设立一个"Contact"类来代表单个联系人,该类将包诸如姓名、电话、邮箱等属性。同时,还可以设计一个"AddressBook"类来负责管理多个联系人,此类将集成添加、删除和查找联系人的功能。 ER图作为数据库设计的核心工具,主要用于表达体、属性以及体间的关联关系。在个人通讯录系统的设计中,体可能包"User"(用户)和"Contact"(联系人)。"User"体可能具备用户名、密码等属性,而"Contact"...
已经博主授权,源码转载自 https://pan.quark.cn/s/a4b39357ea24 标题中所提及的“PB9转换utf-8例子”具体描述了在PowerBuilder 9(PB9)环境中,将数据从非UTF-8编码格式转变为UTF-8编码格式的一种具体方案。鉴于PB9本身并不具备直接进行此类编码转换的功能,开发人员通常需要借助外部库或者特定的编程策略来达成这一功能。在此例中,采用了ADODB.Stream对象,该对象是Microsoft ActiveX Data Objects (ADO)框架内的一部分,它能够支持多种类型流数据的处理,其中包括文本数据的编码变更。 描述部分指出,由于PowerBuilder 9及其以下版本未内建直接的字符编码转换机制,因此需要借助ADODB.Stream。这个对象提供了一种途径,通过读取原始编码的文本,并将其写入到新的以UTF-8编码的流中,从而现转换。这一过程一般包括启动一个流对象,设定其编码类型,读取原始数据,然后以目标编码(此处为UTF-8)写入到新流,最终保存结果。 标签“pb9 utf-8”清晰地表明了讨论的主题是关于PowerBuilder 9UTF-8编码相关的问题。UTF-8是一种应用广泛的Unicode字符编码,能够表示Unicode字符集中几乎所有字符,涵盖了全球多种语言文字。 在压缩包内的文件清单中,包了四个PowerBuilder相关的文件(utf-8.pbl、utf-8.pbt、utf-8.pbw)以及三个文本文件(aaa.txt、www.txt、bbb.txt)。这四个PB文件或许包了示例代码、项目配置和工作区信息,用于展示如何运用ADODB.Stream进行编码转换。而aaa.txt、www...
内容概要:本文提出了一种基于粒子群算法(PSO)融合动态窗口法(DWA)的无人机三维动态避障路径规划方法,旨在解决无人机在复杂、动态环境中安全高效飞行的路径规划难题。该方法通过Matlab代码现,有效结合了PSO算法的全局寻优能力DWA算法的局部时避障优势,能够在存在移动障碍物的三维空间中规划出平滑、安全且优化的飞行路径。研究内容涵盖算法原理设计、融合机制构建、仿真环境搭建、路径规划性能测试分析,充分验证了该融合策略在应对动态障碍物、提升避障时性路径质量方面的优越性。; 适合人群:具备一定编程基础和无人系统相关知识的科研人员,特别适用于从事无人机自主导航、智能优化算法、机器人路径规划等领域研究的研究生、高校教师及工程技术人员。; 使用场景及目标:①应用于城市、森林、灾害救援等复杂动态环境下的无人机自主飞行时避障;②为智能交通、物流配送、电力巡检、安防监控等领域的移动机器人路径规划提供先进的算法参考和技术解决方案;③作为智能优化算法机器人技术融合教学科研验的重要案例。; 阅读建议:建议读者结合提供的Matlab代码进行仿真验,深入理解PSODWA的融合逻辑、关键参数的敏感性分析及避障效果的评价指标,鼓励在掌握核心思想的基础上进行算法改进创新应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值