(源码地址见最后)
使用版本:
jdk 17,nacos-3.0.2,springboot-3.2.5,springcloud 2023.0.1,springcloudalibaba 2023.0.1.0.
来由:上篇写了sso的密码模式 https://qinfeng.blog.csdn.net/article/details/159653468,因为注解都提示废弃了,索性升级下,springcloud版本从2021升级到了2023版本,也顺便升级到了授权码模式。
解决:
项目还是2个模块


sso
pom
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.dp</groupId>
<artifactId>alibaba_jwt_sso_2023</artifactId>
<version>1.0.0</version>
<name>alibaba_jwt_sso</name>
<description>alibaba_jwt_sso</description>
<packaging>pom</packaging>
<modules>
<module>auth</module>
<module>resource</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<spring-auth-server.version>1.2.4</spring-auth-server.version>
<jjwt.version>0.11.5</jjwt.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>${spring-auth-server.version}</version>
</dependency>
<!-- JJWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
auth项目
pom
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.dp</groupId>
<artifactId>alibaba_jwt_sso_2023</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>auth</artifactId>
<name>auth</name>
<description>auth</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
<!-- JJWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
AuthServerConfig.java
java
package com.dp.auth.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.savedrequest.SavedRequest;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 认证服务器配置(新版 Spring Authorization Server)
* ============================================
* 认证方式:OAuth2 密码模式(Password Grant)
*
* 变更说明:
* 1. 移除 @EnableAuthorizationServer 注解
* 2. 移除 WebSecurityConfigurerAdapter 继承
* 3. 使用 SecurityFilterChain Bean 替代
* 4. 使用 RegisteredClientRepository 替代 ClientDetailsServiceConfigurer
* 5. Token 端点从 /oauth/token 改为 /oauth2/token
* ============================================
*/
@Configuration
@EnableWebSecurity
public class AuthServerConfig {
// ==================== 1. 用户认证配置(保持不变) ====================
/**
* 密码编码器
* 使用 BCrypt 加密,存储时自动加盐
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 创建支持多种编码方式的密码编码器
String defaultEncoding = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(defaultEncoding, encoders);
delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
return delegatingPasswordEncoder;
}
/**
* 用户详情服务
* 定义测试用户:admin/123456, user/123456
* 密码使用 BCrypt 加密
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 密码: 123456 的 BCrypt 加密结果
UserDetails admin = User.withUsername("admin")
.password("$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi")
.roles("ADMIN", "USER")
.build();
UserDetails user = User.withUsername("user")
.password("$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi")
.roles("USER")
.build();
manager.createUser(admin);
manager.createUser(user);
return manager;
}
// ==================== 2. OAuth2 客户端配置(替代 AuthorizationServerConfig) ====================
/**
* 客户端注册信息仓库
* 替代旧版的 ClientDetailsServiceConfigurer
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
RegisteredClient gatewayClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("gateway-client")
.clientSecret(encoder.encode("gateway-secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 添加密码授权类型
.authorizationGrantType(AuthorizationGrantType.PASSWORD) // 新增
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("https://oauth.pstmn.io/v1/callback")
// 添加 admin scope
.scope("openid")
.scope("profile")
.scope("admin") // 新增 admin scope
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
return new InMemoryRegisteredClientRepository(gatewayClient);
}
// ==================== 3. JWT 配置(替代 JwtAccessTokenConverter) ====================
/**
* JWT 密钥源 - 生成 RSA 密钥对
* 替代旧版的 JwtAccessTokenConverter
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 生成 RSA 密钥对
* 用于 JWT 签名和验证
*/
private static KeyPair generateRsaKey() {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return generator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
/**
* JWT 解码器
* 用于验证 Token
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* Token 存储(新版使用 JwtDecoder,不需要单独的 TokenStore)
* 注意:新版不需要 TokenStore Bean,JwtDecoder 已经处理
*/
// ==================== 4. 安全过滤器链配置(替代 WebSecurityConfigurerAdapter) ====================
/**
* OAuth2 授权服务器安全配置(优先级最高)
* 替代旧版的 AuthorizationServerSecurityConfigurer
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 应用默认的 OAuth2 授权服务器配置
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 启用 OIDC 协议
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
// 启用表单登录(替代旧版的 allowFormAuthenticationForClients)
return http.formLogin(Customizer.withDefaults()).build();
}
/**
* 默认安全配置(优先级次之)
* 替代旧版的 SecurityConfig 内部类
* 对应旧版的 configure(HttpSecurity http) 方法
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/.well-known/**", "/favicon.ico", "/error").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.successHandler((request, response, authentication) -> {
// 从 session 中获取保存的请求
SavedRequest savedRequest = (SavedRequest) request.getSession()
.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if (savedRequest != null) {
// 重定向回原始的 OAuth2 授权请求
String targetUrl = savedRequest.getRedirectUrl();
response.sendRedirect(targetUrl);
} else {
// 默认跳转
response.sendRedirect("/");
}
})
.permitAll()
);
return http.build();
}
/**
* 授权服务器设置
* 配置授权服务器的基础 URL 和端点
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("http://localhost:9000") // 服务签发者
.authorizationEndpoint("/oauth2/authorize") // 授权端点(对应旧版的 /oauth/authorize)
.tokenEndpoint("/oauth2/token") // Token 端点(对应旧版的 /oauth/token)
.jwkSetEndpoint("/oauth2/jwks") // JWK 端点
.oidcUserInfoEndpoint("/userinfo") // 用户信息端点
.build();
}
}
AuthApplication.java
java
package com.dp.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
application.yml
java
server:
port: 9001
spring:
application:
name: auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: public
logging:
level:
org.springframework.security: DEBUG
org.springframework.security.oauth2: DEBUG
resource项目
pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.dp</groupId> <artifactId>alibaba_jwt_sso_2023</artifactId> <version>1.0.0</version> </parent> <artifactId>resource</artifactId> <name>resource</name> <description>resource</description> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security OAuth2 Resource Server --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> </dependency> <!-- JJWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <scope>runtime</scope> </dependency> <!-- Nacos Discovery --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
ResourceApplication.java
package com.dp.resource; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class ResourceApplication { public static void main(String[] args) { SpringApplication.run(ResourceApplication.class, args); } }
ResourceServerConfig.java
package com.dp.resource.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; /** * 资源服务器配置(新版) * 使用 RSA 公钥验证 JWT 签名 */ @Configuration @EnableWebSecurity @EnableMethodSecurity public class ResourceServerConfig { /** * 授权服务器的 JWK 端点地址 * 资源服务器从这里获取公钥来验证 JWT */ private static final String JWK_SET_URI = "http://localhost:9001/oauth2/jwks"; @Bean public JwtDecoder jwtDecoder() { // 从授权服务器的 JWK 端点获取公钥 return NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI).build(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/user/**").hasAnyAuthority("SCOPE_profile", "SCOPE_openid") .requestMatchers("/api/admin/**").hasAnyAuthority("SCOPE_admin", "ROLE_ADMIN") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.decoder(jwtDecoder())) ) .csrf(csrf -> csrf.disable()); return http.build(); } }
UserController.java
package com.dp.resource.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; import java.util.HashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/api") public class UserController { /** * 公开接口 - 不需要 Token */ @GetMapping("/public/health") public Map<String, Object> health() { Map<String, Object> result = new HashMap<>(); result.put("status", "UP"); result.put("service", "resource-server"); result.put("message", "Service is running"); return result; } /** * 用户信息接口 - 需要 Token * 通过 Principal 获取当前登录用户 */ @GetMapping("/user/info") public Map<String, Object> getUserInfo(Principal principal) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); log.info("Username : {}", authentication.getName()); Map<String, Object> result = new HashMap<>(); result.put("username", principal.getName()); result.put("authenticated", authentication.isAuthenticated()); result.put("authorities", authentication.getAuthorities()); result.put("service", "resource-server"); return result; } /** * 管理员接口 - 需要 ADMIN 角色 */ @GetMapping("/admin/dashboard") public Map<String, Object> adminDashboard() { Map<String, Object> result = new HashMap<>(); result.put("data", "Admin Dashboard Data"); result.put("service", "resource-server"); return result; } }
启动项目:
1.启动nacos
2.启动auth项目-启动applicaiton.java
3.启动resource项目
测试:
弹出登陆页面 admin 123456
f12看code


- postman 测试
post http://localhost:9001/oauth2/token
grant_type:authorization_code
code:YAA6oaH7xxtvfwxG9yOys_OvTccIorPLEHcClW-vO4j31-A5ip6xV7fNsuiBoHd9DfQeIGhuC_5fq6bpxhjrQujpBAcjzRjAy5dAAkH9fzL0T7wryzcL3ZJCAsXJo109
redirect_uri:https://oauth.pstmn.io/v1/callback

Authorization:Basic Z2F0ZXdheS1jbGllbnQ6Z2F0ZXdheS1zZWNyZXQ=
其中Z2F0ZXdheS1jbGllbnQ6Z2F0ZXdheS1zZWNyZXQ=是base64编码

3.token 测试资源和权限
3.1 测试资源
http://localhost:9002/api/user/info
Authorization:Bearer eyJraWQiOiJlOTIxMGI3Yi01NjAzLTQ0MGUtYWVlZS0xOGEwNjY1Nzk1MDUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImdhdGV3YXktY2xpZW50IiwibmJmIjoxNzc0OTM2Njk1LCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwiYWRtaW4iXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwiZXhwIjoxNzc0OTQwMjk1LCJpYXQiOjE3NzQ5MzY2OTUsImp0aSI6IjVmYzMzYjdlLWIyZTItNGY2My04NTdlLWIzYjhmNTdmOTllNCJ9.NM1e5CY7SZk8uleHDdmWnvK1cL3Ux9_3h_VOnz3XQMK-OEA4oH5EX8_xXGiZJP0-5Rdt3Izx4jB8B2ASbBaLYyd2l3ijAzFPJzDj_IXu5rfv38ZBp9wDVjjwEFYbAZy_bqPv6gXJmA7yZB5O7xJV_g2slAUq4euoDdwuBGyRXeQvBEjax9C8d9a9I8ov2y2xWAt_cllSzNtqrlwpe3TGaedHVkkt8fdsEF39UWWi6e2ESkSwBMng2RT5OlmuWWhi2c5W4WVA_Dk-p6kxnt-YLpfr2VqWXyfSxp0SkpVqSHaRlU-FrG7aiykpBLoSx_2WLrqovY6h909_7RzVHvYo6A

3.2 测试权限
localhost:9002/api/admin/dashboard

done.
源码地址:https://github.com/lvan-jone/springcloudalibaba_sso_authcode