Spring Cloud OAuth2认证案例

wen java案例 3

本文目录导读:

Spring Cloud OAuth2认证案例

  1. 项目结构
  2. 认证服务器 (auth-server)
  3. 资源服务器 (resource-server)
  4. API 网关 (gateway-server)
  5. 客户端调用示例
  6. 实际应用案例
  7. 安全最佳实践

我来为您提供一个完整的Spring Cloud OAuth2认证案例。

项目结构

oauth2-demo
├── auth-server           # 认证服务器(端口:9000)
├── resource-server      # 资源服务器(端口:9001)
├── gateway-server       # API网关(端口:8080)
└── user-service         # 用户微服务(端口:9002)

认证服务器 (auth-server)

1 pom.xml

<?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 
         http://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>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>auth-server</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

2 application.yml

server:
  port: 9000
spring:
  application:
    name: auth-server
  datasource:
    url: jdbc:mysql://localhost:3306/oauth2_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: localhost
    port: 6379
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.authserver.entity
logging:
  level:
    com.example.authserver: DEBUG

3 认证服务器配置

package com.example.authserver.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private UserServiceDetail userServiceDetail;
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("oauth2-jwt-secret");
        return converter;
    }
    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenEnhancer(jwtAccessTokenConverter());
        tokenServices.setAccessTokenValiditySeconds(3600); // 1小时
        tokenServices.setRefreshTokenValiditySeconds(86400); // 24小时
        return tokenServices;
    }
    @Bean
    public ClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()")
            .allowFormAuthenticationForClients();
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
            .tokenStore(tokenStore())
            .tokenServices(tokenServices())
            .userDetailsService(userServiceDetail)
            .authenticationManager(authenticationManager)
            .accessTokenConverter(jwtAccessTokenConverter());
    }
}

4 安全管理配置

package com.example.authserver.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceDetail userServiceDetail;
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceDetail)
            .passwordEncoder(passwordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/oauth/**", "/login", "/logout", "/actuator/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .permitAll()
            .and()
            .logout()
            .logoutSuccessUrl("/");
    }
}

5 用户服务实现

package com.example.authserver.service;
import com.example.authserver.entity.User;
import com.example.authserver.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceDetail implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        // 从数据库获取用户角色
        userMapper.findRolesByUserId(user.getId()).forEach(role -> {
            authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
        });
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            user.getEnabled(),
            true, true, true,
            authorities
        );
    }
}

6 用户实体类

package com.example.authserver.entity;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String phone;
    private Boolean enabled;
    private Date createTime;
    private Date updateTime;
    // getter and setter 方法
    // ... 
}

7 数据库初始化脚本

-- 创建数据库
CREATE DATABASE IF NOT EXISTS oauth2_demo DEFAULT CHARACTER SET utf8mb4;
USE oauth2_demo;
-- OAuth2 客户端表
CREATE TABLE IF NOT EXISTS oauth_client_details (
    client_id VARCHAR(256) PRIMARY KEY,
    resource_ids VARCHAR(256),
    client_secret VARCHAR(256),
    scope VARCHAR(256),
    authorized_grant_types VARCHAR(256),
    web_server_redirect_uri VARCHAR(256),
    authorities VARCHAR(256),
    access_token_validity INTEGER,
    refresh_token_validity INTEGER,
    additional_information VARCHAR(4096),
    autoapprove VARCHAR(256)
);
-- 用户表
CREATE TABLE IF NOT EXISTS t_user (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    phone VARCHAR(20),
    enabled BOOLEAN DEFAULT TRUE,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE IF NOT EXISTS t_role (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    role_name VARCHAR(50) UNIQUE NOT NULL,
    description VARCHAR(255)
);
-- 用户角色关联表
CREATE TABLE IF NOT EXISTS t_user_role (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    FOREIGN KEY (user_id) REFERENCES t_user(id),
    FOREIGN KEY (role_id) REFERENCES t_role(id)
);
-- 插入测试数据
INSERT INTO oauth_client_details (
    client_id, resource_ids, client_secret, scope, 
    authorized_grant_types, web_server_redirect_uri, 
    authorities, access_token_validity, refresh_token_validity, 
    autoapprove
) VALUES (
    'client_app', 
    'resource-server',
    '$2a$10$X.N8kF7IuHq1uM1xVhjTOu1SnXqE7Lc5Yh5QZ1V8HJzJ8W5Y2U1fG', -- 密码:secret (BCrypt加密)
    'read,write',
    'authorization_code,password,refresh_token,client_credentials,implicit',
    'http://localhost:8080/login/oauth2/code/client',
    'ROLE_CLIENT',
    3600,
    86400,
    'true'
);
-- 插入用户 (密码:123456,BCrypt加密)
INSERT INTO t_user (username, password, email, phone, enabled) 
VALUES ('admin', '$2a$10$hK8fCvQm4bKaFh3hJ3zqkO7X5DyFhLJnUgKxNA0Pj8nCkQ7Mv3W0S', 'admin@example.com', '13800138000', true);
INSERT INTO t_user (username, password, email, phone, enabled) 
VALUES ('user', '$2a$10$hK8fCvQm4bKaFh3hJ3zqkO7X5DyFhLJnUgKxNA0Pj8nCkQ7Mv3W0S', 'user@example.com', '13800138001', true);
-- 插入角色
INSERT INTO t_role (role_name, description) VALUES ('ROLE_ADMIN', '管理员');
INSERT INTO t_role (role_name, description) VALUES ('ROLE_USER', '普通用户');
-- 关联用户和角色
INSERT INTO t_user_role (user_id, role_id) VALUES (1, 1); -- admin -> ROLE_ADMIN
INSERT INTO t_user_role (user_id, role_id) VALUES (2, 2); -- user -> ROLE_USER

资源服务器 (resource-server)

1 pom.xml

<?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 
         http://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>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>resource-server</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

2 application.yml

server:
  port: 9001
spring:
  application:
    name: resource-server
  redis:
    host: localhost
    port: 6379
  security:
    oauth2:
      client:
        client-id: client_app
        client-secret: secret
        user-authorization-uri: http://localhost:9000/oauth/authorize
        access-token-uri: http://localhost:9000/oauth/token
      resource:
        user-info-uri: http://localhost:9000/userinfo
        token-info-uri: http://localhost:9000/oauth/check_token
# JWT 配置
jwt:
  secret: oauth2-jwt-secret
management:
  endpoints:
    web:
      exposure:
        include: "*"

3 资源服务器配置

package com.example.resourceserver.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Value("${jwt.secret}")
    private String jwtSecret;
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(jwtSecret);
        return converter;
    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources
            .resourceId("resource-server")
            .tokenStore(jwtTokenStore())
            .stateless(true);
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/public/**").permitAll()
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
            .anyRequest().authenticated();
    }
}

4 用户控制器

package com.example.resourceserver.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class UserController {
    @GetMapping("/public/info")
    public Map<String, String> publicInfo() {
        Map<String, String> info = new HashMap<>();
        info.put("message", "This is public information");
        return info;
    }
    @GetMapping("/user/info")
    public Map<String, Object> userInfo(@AuthenticationPrincipal Authentication authentication) {
        Map<String, Object> result = new HashMap<>();
        result.put("username", authentication.getName());
        result.put("authorities", authentication.getAuthorities());
        result.put("message", "User information");
        return result;
    }
    @GetMapping("/user/profile")
    public Map<String, Object> userProfile(Principal principal) {
        Map<String, Object> result = new HashMap<>();
        result.put("profile", "User profile for " + principal.getName());
        return result;
    }
    @GetMapping("/admin/management")
    @PreAuthorize("hasRole('ADMIN')")
    public Map<String, Object> adminManagement() {
        Map<String, Object> result = new HashMap<>();
        result.put("message", "Admin management access");
        return result;
    }
    @GetMapping("/users")
    public Map<String, Object> getUsers() {
        Map<String, Object> result = new HashMap<>();
        result.put("users", new String[]{"admin", "user", "guest"});
        return result;
    }
}

API 网关 (gateway-server)

1 pom.xml

<?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 
         http://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>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>gateway-server</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

2 application.yml

server:
  port: 8080
spring:
  application:
    name: gateway-server
  redis:
    host: localhost
    port: 6379
  cloud:
    gateway:
      routes:
        - id: resource-server
          uri: lb://resource-server
          predicates:
            - Path=/api/**
          filters:
            - StripPrefix=0
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@ipKeyResolver}"
security:
  oauth2:
    client:
      client-id: client_app
      client-secret: secret
      access-token-uri: http://localhost:9000/oauth/token
      user-authorization-uri: http://localhost:9000/oauth/authorize
    resource:
      user-info-uri: http://localhost:9000/userinfo
      token-info-uri: http://localhost:9000/oauth/check_token

3 网关配置

package com.example.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectWebFilter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfig {
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .csrf().disable()
            .authorizeExchange()
            .pathMatchers("/oauth/**", "/login", "/actuator/**").permitAll()
            .pathMatchers("/api/public/**").permitAll()
            .pathMatchers("/api/admin/**").hasRole("ADMIN")
            .pathMatchers("/api/**").authenticated()
            .anyExchange().permitAll()
            .and()
            .oauth2Login();
        return http.build();
    }
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.addAllowedOrigin("*");
        corsConfig.addAllowedHeader("*");
        corsConfig.addAllowedMethod("*");
        corsConfig.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);
        return new CorsWebFilter(source);
    }
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
        );
    }
}

客户端调用示例

1 获取token

# 密码模式获取token
curl -X POST "http://localhost:9000/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=admin&password=123456&scope=read&client_id=client_app&client_secret=secret"
# 授权码模式 - 第一步:获取授权码
curl -X GET "http://localhost:9000/oauth/authorize?client_id=client_app&response_type=code&scope=read&redirect_uri=http://localhost:8080/login/oauth2/code/client"
# 授权码模式 - 第二步:获取token
curl -X POST "http://localhost:9000/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:8080/login/oauth2/code/client&client_id=client_app&client_secret=secret"
# 客户端凭证模式
curl -X POST "http://localhost:9000/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=client_app&client_secret=secret"
# 刷新token
curl -X POST "http://localhost:9000/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=client_app&client_secret=secret"

2 调用资源服务器

# 使用token调用受保护资源
curl -X GET "http://localhost:8080/api/user/info" \
  -H "Authorization: Bearer ACCESS_TOKEN"
# 调用管理接口
curl -X GET "http://localhost:8080/api/admin/management" \
  -H "Authorization: Bearer ACCESS_TOKEN"

实际应用案例

1 前端登录流程

// 使用axios封装的认证服务
class AuthService {
    constructor() {
        this.api = axios.create({
            baseURL: 'http://localhost:9000',
            headers: {
                'Content-Type': 'application/json'
            }
        });
    }
    async login(username, password) {
        try {
            const params = new URLSearchParams();
            params.append('grant_type', 'password');
            params.append('username', username);
            params.append('password', password);
            params.append('scope', 'read write');
            const authHeaders = {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Authorization': 'Basic ' + btoa('client_app:secret')
                }
            };
            const response = await this.api.post('/oauth/token', params, authHeaders);
            if (response.data.access_token) {
                localStorage.setItem('access_token', response.data.access_token);
                localStorage.setItem('refresh_token', response.data.refresh_token);
                return response.data;
            }
            return null;
        } catch (error) {
            console.error('Login failed:', error);
            throw error;
        }
    }
    async refreshToken() {
        const refreshToken = localStorage.getItem('refresh_token');
        if (!refreshToken) return false;
        try {
            const params = new URLSearchParams();
            params.append('grant_type', 'refresh_token');
            params.append('refresh_token', refreshToken);
            const authHeaders = {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Authorization': 'Basic ' + btoa('client_app:secret')
                }
            };
            const response = await this.api.post('/oauth/token', params, authHeaders);
            if (response.data.access_token) {
                localStorage.setItem('access_token', response.data.access_token);
                localStorage.setItem('refresh_token', response.data.refresh_token);
                return true;
            }
            return false;
        } catch (error) {
            console.error('Refresh token failed:', error);
            return false;
        }
    }
    async logout() {
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        // 可选:调用服务器的注销接口
        await this.api.post('/oauth/logout');
    }
    async getUserInfo(token) {
        try {
            const response = await this.api.get('/userinfo', {
                headers: {
                    'Authorization': 'Bearer ' + token
                }
            });
            return response.data;
        } catch (error) {
            console.error('Get user info failed:', error);
            throw error;
        }
    }
}
class ApiInterceptor {
    static attach(api) {
        api.interceptors.request.use(
            config => {
                const token = localStorage.getItem('access_token');
                if (token) {
                    config.headers.Authorization = `Bearer ${token}`;
                }
                return config;
            },
            error => Promise.reject(error)
        );
        api.interceptors.response.use(
            response => response,
            async error => {
                const originalRequest = error.config;
                if (error.response?.status === 401 && !originalRequest._retry) {
                    originalRequest._retry = true;
                    const authService = new AuthService();
                    const refreshed = await authService.refreshToken();
                    if (refreshed) {
                        const newToken = localStorage.getItem('access_token');
                        originalRequest.headers.Authorization = `Bearer ${newToken}`;
                        return api(originalRequest);
                    }
                }
                return Promise.reject(error);
            }
        );
    }
}

2 REST API 使用示例

// 使用RestTemplate调用认证服务器
@Service
public class AdminClientService {
    @Autowired
    private RestTemplate restTemplate;
    @Value("${auth-server.token-uri}")
    private String tokenUri;
    @Value("${auth-server.client-id}")
    private String clientId;
    @Value("${auth-server.client-secret}")
    private String clientSecret;
    public String getAdminToken() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth(clientId, clientSecret);
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "client_credentials");
        params.add("scope", "read");
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
        try {
            ResponseEntity<OAuth2TokenResponse> response = restTemplate.postForEntity(
                tokenUri,
                request,
                OAuth2TokenResponse.class
            );
            if (response.getStatusCode().is2xxSuccessful()) {
                return response.getBody().getAccessToken();
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to get admin token", e);
        }
        return null;
    }
    static class OAuth2TokenResponse {
        private String accessToken;
        private String tokenType;
        private int expiresIn;
        private String scope;
        // getter and setter
    }
}

安全最佳实践

1 自定义Token增强器

package com.example.authserver.config;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;
public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> additionalInfo = new HashMap<>();
        // 添加自定义信息
        additionalInfo.put("organization", "MyCompany");
        additionalInfo.put("environment", "production");
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            additionalInfo.put("username", userDetails.getUsername());
        }
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}

2 动态权限配置

package com.example.resourceserver.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    private UserPermissionService permissionService;
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        if (authentication == null || targetDomainObject == null || !(permission instanceof String)) {
            return false;
        }
        // 获取用户权限
        return permissionService.checkPermission(
            authentication.getName(),
            targetDomainObject.getClass().getSimpleName(),
            permission.toString()
        );
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        if (authentication == null || targetId == null || targetType == null || !(permission instanceof String)) {
            return false;
        }
        return permissionService.checkPermission(
            authentication.getName(),
            targetType,
            targetId,
            permission.toString()
        );
    }
}

3 使用OAuth2 Feign客户端

@FeignClient(name = "resource-server", configuration = OAuth2FeignConfig.class)
public interface UserClient {
    @GetMapping("/api/users")
    List<User> getUsers();
    @GetMapping("/api/users/{id}")
    User getUserById(@PathVariable("id") Long id);
    @GetMapping("/api/admin/users")
    List<User> getAdminUsers();
}
public class OAuth2FeignConfig {
    @Bean
    public RequestInterceptor o

抱歉,评论功能暂时关闭!