Spring Cloud Gateway + OAuth2 + JWT 单点登录(SSO)实现方案
一、概述
基于Spring Cloud微服务架构,通过Gateway网关、OAuth2协议和JWT令牌实现分布式系统的单点登录,允许用户一次登录后访问所有互信的微服务。核心技术组件包括:
- Spring Cloud Gateway:统一请求入口,负责路由、Token验证和转发
- OAuth2协议:定义授权流程,实现用户认证和授权码交换
- JWT(Json Web Token):作为无状态令牌载体,包含用户身份和权限信息
二、系统架构与服务组件
1. 服务架构图
+----------------+ +----------------+ +----------------+
| | | | | |
| 客户端应用 |<--->| API网关 |<--->| 认证中心 |
| (sso-client) | | (api-gateway) | | (sso-auth-server)|
| | | | | |
+----------------+ +--------+-------+ +--------+-------+|v
+----------------+ +----------------+
| | | |
| 用户服务 | | 订单服务 |
| (user-service) | |(order-service)|
| | | |
+----------------+ +----------------+
2. 服务职责说明
服务名称 | 职责描述 |
---|---|
认证中心 | 处理用户登录、生成JWT令牌、管理客户端注册信息,作为OAuth2授权服务器 |
API网关 | 统一请求入口,实现路由分发、Token验证与传递,集成OAuth2客户端配置 |
资源服务 | 提供业务数据接口(如用户、订单服务),验证Token并基于权限控制访问 |
客户端应用 | 用户交互入口,引导登录、获取Token并调用资源服务,展示业务数据 |
三、核心技术实现
1. 认证中心(auth-server)配置
<!-- 认证中心依赖配置(pom.xml) -->
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency>
</dependencies>
// 认证服务器核心配置(AuthorizationServerConfig.java)
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowired private AuthenticationManager authenticationManager;@Autowired private UserDetailsService userDetailsService;@Autowired private TokenStore tokenStore;@Autowired private JwtAccessTokenConverter accessTokenConverter;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("sso-client").secret("{noop}sso-secret").authorizedGrantTypes("authorization_code", "refresh_token", "password").scopes("read", "write", "profile").redirectUris("http://localhost:8081/login/oauth2/code/custom").accessTokenValiditySeconds(3600).refreshTokenValiditySeconds(86400);}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService).tokenStore(tokenStore).accessTokenConverter(accessTokenConverter).allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);}
}
// Spring Security配置(SecurityConfig.java)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Bean@Overridepublic UserDetailsService userDetailsServiceBean() throws Exception {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withUsername("user").password("{noop}password").roles("USER").authorities("READ", "WRITE").build());return manager;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/oauth/authorize", "/login").permitAll().anyRequest().authenticated().and().formLogin().permitAll();}@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(accessTokenConverter());}@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("sso-jwt-secret-key");return converter;}
}
# 认证中心配置文件(application.yml)
server:port: 8080spring:application:name: sso-auth-serversecurity:oauth2:resourceserver:jwt:issuer-uri: http://localhost:8080/
2. 网关服务(gateway)配置
<!-- 网关依赖配置(pom.xml) -->
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
</dependencies>
# 网关核心配置(application.yml)
server:port: 8081spring:application:name: sso-gatewaycloud:gateway:routes:- id: user-serviceuri: lb://USER-SERVICEpredicates: Path=/user/**filters: [TokenRelay, RewritePath=/user/(?<segment>.*), /$\{segment}]- id: resource-serviceuri: lb://RESOURCE-SERVICEpredicates: Path=/resource/**filters: [TokenRelay, RewritePath=/resource/(?<segment>.*), /$\{segment}]globalcors:cors-configurations:'[/**]':allowed-origins: "*"allowed-methods: GET, POST, PUT, DELETEsecurity:oauth2:client:provider:custom:authorization-uri: http://localhost:8080/oauth/authorizetoken-uri: http://localhost:8080/oauth/tokenregistration:custom:client-id: sso-clientclient-secret: sso-secretauthorization-grant-type: authorization_coderedirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
// 网关安全配置(GatewaySecurityConfig.java)
@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfig {@Beanpublic SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {http.authorizeExchange(exchanges -> exchanges.pathMatchers("/login/**", "/oauth2/**").permitAll().anyExchange().authenticated()).oauth2Login().and().oauth2ResourceServer().jwt();return http.build();}
}
3. 资源服务(user-service)配置
// 资源服务器安全配置(ResourceServerConfig.java)
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/public/**").permitAll().antMatchers("/user/info").hasAnyRole("USER").antMatchers("/user/admin").hasRole("ADMIN").anyRequest().authenticated().and().oauth2ResourceServer().jwt();}
}
// 资源接口示例(UserController.java)
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/info")public Map<String, Object> getUserInfo(Principal principal) {Map<String, Object> result = new HashMap<>();result.put("username", principal.getName());result.put("timestamp", System.currentTimeMillis());result.put("message", "访问用户信息接口成功");return result;}@GetMapping("/admin")public String adminOnly() {return "这是管理员专用接口,只有ADMIN角色可访问";}
}
# 资源服务配置文件(application.yml)
server:port: 8082spring:application:name: user-servicesecurity:oauth2:resourceserver:jwt:issuer-uri: http://localhost:8080/
4. 客户端应用(client)配置
// 客户端登录控制器(SsoController.java)
@Controller
public class SsoController {@Autowired private RestTemplate restTemplate;@GetMapping("/login")public void login(HttpServletResponse response) throws IOException {String authUrl = "http://localhost:8080/oauth/authorize" +"?response_type=code&client_id=sso-client" +"&redirect_uri=http://localhost:8081/login/oauth2/code/custom";response.sendRedirect(authUrl);}@GetMapping("/callback")public String callback(@RequestParam("code") String code, Model model) {String tokenUri = "http://localhost:8080/oauth/token";HttpHeaders headers = new HttpHeaders();headers.setBasicAuth("sso-client", "sso-secret");MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("grant_type", "authorization_code");params.add("code", code);params.add("redirect_uri", "http://localhost:8081/login/oauth2/code/custom");ResponseEntity<Map> response = restTemplate.exchange(tokenUri, HttpMethod.POST, new HttpEntity<>(params, headers), Map.class);String accessToken = (String) response.getBody().get("access_token");model.addAttribute("accessToken", accessToken);return "home";}
}
# 客户端配置文件(application.yml)
server:port: 8083spring:application:name: sso-clientsecurity:oauth2:client:provider:custom:authorization-uri: http://localhost:8080/oauth/authorizetoken-uri: http://localhost:8080/oauth/tokenregistration:custom:client-id: sso-clientclient-secret: sso-secretauthorization-grant-type: authorization_coderedirect-uri: "http://localhost:8081/login/oauth2/code/custom"
5.项目结构图
5.1 项目结构图
sso-example/
├── auth-server/ # 认证中心服务
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/example/authserver/
│ │ │ │ ├── AuthServerApplication.java # 主启动类
│ │ │ │ ├── config/ # 配置类目录
│ │ │ │ │ ├── AuthorizationServerConfig.java # OAuth2认证服务器配置
│ │ │ │ │ ├── SecurityConfig.java # Spring Security配置
│ │ │ │ │ └── JwtConfig.java # JWT配置
│ │ │ │ └── entity/ # 实体类目录
│ │ │ │ └── User.java # 用户实体
│ │ │ └── resources/
│ │ │ ├── application.yml # 应用配置文件
│ │ │ └── static/
│ │ │ └── login.html # 登录页面
│ │ └── test/
│ └── pom.xml # 模块依赖配置
│
├── api-gateway/ # 网关服务
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/example/gateway/
│ │ │ │ ├── GatewayApplication.java # 主启动类
│ │ │ │ └── config/
│ │ │ │ └── GatewaySecurityConfig.java # 网关安全配置
│ │ │ └── resources/
│ │ │ └── application.yml # 应用配置文件
│ │ └── test/
│ └── pom.xml
│
├── user-service/ # 用户资源服务
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/example/userservice/
│ │ │ │ ├── UserServiceApplication.java # 主启动类
│ │ │ │ ├── config/
│ │ │ │ │ └── ResourceServerConfig.java # 资源服务器安全配置
│ │ │ │ └── controller/
│ │ │ │ └── UserController.java # 用户接口控制器
│ │ │ └── resources/
│ │ │ └── application.yml # 应用配置文件
│ │ └── test/
│ └── pom.xml
│
├── sso-client/ # 客户端应用
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/example/client/
│ │ │ │ ├── ClientApplication.java # 主启动类
│ │ │ │ └── controller/
│ │ │ │ └── SsoController.java # 单点登录控制器
│ │ │ └── resources/
│ │ │ ├── application.yml # 应用配置文件
│ │ │ ├── templates/
│ │ │ │ ├── home.html # 首页
│ │ │ │ └── resource.html # 资源页面
│ │ └── test/
│ └── pom.xml
│
└── pom.xml # 父项目依赖配置
5.2 核心模块功能说明
模块名称 | 功能定位 | 关键组件 |
---|---|---|
auth-server | 认证中心,负责用户登录、Token生成和OAuth2授权管理 | - AuthorizationServerConfig :OAuth2服务器配置- SecurityConfig :用户认证配置- JwtConfig :JWT签名配置 |
api-gateway | 统一网关,负责请求路由、Token验证和转发 | - GatewaySecurityConfig :网关安全配置- application.yml :路由规则和OAuth2客户端配置 |
user-service | 资源服务示例,提供用户相关接口,需验证Token才能访问 | - ResourceServerConfig :资源服务器安全配置- UserController :用户信息接口 |
sso-client | 客户端应用,处理用户交互、登录流程和资源访问请求 | - SsoController :登录、回调和资源访问逻辑- application.yml :OAuth2客户端配置 |
5.3 模块依赖关系
四、用户登录流程详解
- 访问客户端应用:用户访问
sso-client
,未认证时重定向到认证中心 - 认证中心登录:输入
user/password
,认证中心生成授权码并回调客户端 - 换取访问令牌:客户端用授权码向认证中心请求AccessToken
- 资源访问:携带Token访问
user-service
,网关和资源服务验证Token有效性 - Token刷新:过期前通过RefreshToken获取新令牌,避免重复登录
五、部署与集成要点
- 服务启动顺序:认证中心→网关→资源服务→客户端应用
- 跨域配置:在网关和各服务中添加
allowed-origins
等CORS规则 - 生产环境优化:
- 用数据库存储客户端和用户信息,替代内存存储
- 配置HTTPS保障通信安全,避免Token明文传输
- 实现分布式会话管理,支持集群部署
六、方案优势与适用场景
1. 核心优势
- 统一认证入口:一次登录访问所有微服务,提升用户体验
- 无状态认证:基于JWT实现无状态会话,避免分布式会话共享问题
- 细粒度权限控制:通过
@PreAuthorize
和角色/作用域限制接口访问
2. 适用场景
- 分布式微服务架构的企业级应用
- 多系统统一登录的平台(如中台系统)