一、微服务网关整合OAuth2.0设计思路

1.1 网关在认证体系中的角色定位

在微服务架构中,API网关作为统一的入口,在认证授权体系中承担着至关重要的角色。常见的整合方案有两种:

方案一:网关集中式认证(推荐)

  • 授权服务器生成令牌

  • 所有请求统一在网关层进行令牌验证和权限判断

  • 网关解析令牌并将用户信息转发给下游微服务

方案二:资源服务分布式认证

  • 网关只负责请求转发

  • 各资源服务自行处理令牌验证和权限控制

1.2 网关认证的核心职责

网关在认证授权体系中主要负责两件事:

  1. 作为OAuth2.0资源服务器角色

    • 实现接入方访问权限拦截

    • 验证令牌的有效性和权限

  2. 令牌解析与用户信息转发

    • 解析令牌获取用户身份和权限信息

    • 将明文用户信息转发给下游微服务

1.3 微服务端职责

微服务收到网关转发的用户信息后需要:

  1. 用户授权拦截:检查当前用户是否有权访问该资源

  2. 用户信息存储:将用户信息存储到当前线程上下文,便于后续业务逻辑随时获取


二、搭建微服务授权中心

2.1 创建授权中心服务

项目结构: wolfmall-authcenter

引入核心依赖:

xml

<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>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
</dependency>

2.2 数据库配置

application.yml配置:

yaml

server:
  port: 9999

spring:
  application:
    name: tulingmall-authcenter
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: 6cd8d896-4d19-4e33-9840-26e4bee9a618
  datasource:
    url: jdbc:mysql://wolf.com:3306/tlmall_oauth?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    druid:
      initial-size: 5
      min-idle: 10
      max-active: 20

2.3 配置授权服务器

基于DB模式存储客户端信息

创建客户端信息表:

sql

CREATE TABLE oauth_client_details (
  `client_id` varchar(128) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

授权服务器配置类:

java

@Configuration
@EnableAuthorizationServer
public class WolfAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Autowired
    private DataSource dataSource;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 基于DB存储客户端信息
        clients.withClientDetails(clientDetails());
    }
    
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }
}
基于内存模式配置(开发环境)

java

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
        .withClient("client")                    // 客户端ID
        .secret(passwordEncoder().encode("123123")) // 客户端密钥
        .authorizedGrantTypes("password", "authorization_code", "refresh_token") // 授权类型
        .scopes("all")                           // 授权范围
        .accessTokenValiditySeconds(3600)        // 访问令牌有效期
        .refreshTokenValiditySeconds(86400);     // 刷新令牌有效期
}

2.4 配置Spring Security

安全配置类:

java

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Autowired
    private TulingUserDetailsService wolfUserDetailsService;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(tulingUserDetailsService);
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // OAuth2密码模式需要这个Bean
        return super.authenticationManagerBean();
    }
}

用户详情服务:

java

@Slf4j
@Component
public class WolfUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UmsMemberFeignService umsMemberFeignService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 加载用户信息
        if (StringUtils.isEmpty(username)) {
            throw new UsernameNotFoundException("用户名不能为空");
        }
        
        UmsMember umsMember = getByUsername(username);
        if (null == umsMember) {
            throw new UsernameNotFoundException("用户不存在");
        }
        
        // 封装用户信息
        return new MemberDetails(umsMember);
    }
    
    public UmsMember getByUsername(String username) {
        // 通过Feign从会员服务获取用户信息
        CommonResult<UmsMember> result = umsMemberFeignService.loadUserByUsername(username);
        return result.getData();
    }
}

用户详情实现:

java

public class MemberDetails implements UserDetails {
    
    private UmsMember umsMember;
    
    public MemberDetails(UmsMember umsMember) {
        this.umsMember = umsMember;
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 返回用户权限
        return Arrays.asList(new SimpleGrantedAuthority("TEST"));
    }
    
    @Override
    public String getPassword() {
        return umsMember.getPassword();
    }
    
    @Override
    public String getUsername() {
        return umsMember.getUsername();
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return umsMember.getStatus() == 1;
    }
    
    public UmsMember getUmsMember() {
        return umsMember;
    }
}

2.5 测试用户登录

授权码模式(最安全)

适用于第三方应用接入,如微信、QQ登录等。

授权流程:

  1. 用户访问客户端,客户端将用户导向授权服务器

  2. 用户选择是否授权

  3. 用户授权后,授权服务器返回授权码

  4. 客户端使用授权码换取访问令牌

授权URL示例:

text

http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all

密码模式(高度信任)

适用于自家公司内部应用,用户直接提供用户名密码。

获取令牌URL:

text

http://localhost:9999/oauth/token?username=test&password=test&grant_type=password&client_id=client&client_secret=123123&scope=all

2.6 配置资源服务器

java

@Configuration
@EnableResourceServer
public class TulingResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().authenticated(); // 所有请求需要认证
    }
}

资源接口示例:

java

@RestController
@RequestMapping("/user")
public class UserController {
    
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
        return authentication.getPrincipal(); // 返回当前用户信息
    }
}

三、JWT令牌详解与优化

3.1 JWT基础知识

JWT(JSON Web Token) 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。

核心概念:

  • JWS(JSON Web Signature):签过名的JWT

  • JWK(JSON Web Key):JWT的密钥或公私钥对

3.2 JWT组成结构

一个JWT由三部分组成,用点(.)分隔:

text

header.payload.signature

示例JWT:

text

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
1. Header(头部)

描述JWT的基本信息:类型和签名算法。

json

{
  "alg": "HS256",
  "typ": "JWT"
}
2. Payload(载荷)

存放有效信息的地方,包含三种类型的声明:

标准注册声明:

  • iss:签发者

  • sub:面向的用户

  • aud:接收方

  • exp:过期时间

  • nbf:生效时间

  • iat:签发时间

  • jti:唯一标识

公共声明和私有声明: 可根据业务需求添加自定义字段。

3. Signature(签名)

对header和payload的签名,防止数据篡改。

javascript

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

3.3 JWT应用场景

  1. 一次性验证:如用户注册邮件激活

  2. RESTful API无状态认证:客户端和服务端共享secret

  3. 单点登录:配合Redis实现会话管理(不推荐纯JWT)

3.4 集成JWT到OAuth2

1. 引入依赖

xml

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.9.RELEASE</version>
</dependency>
2. 配置JWT Token存储

java

@Configuration
public class JwtTokenStoreConfig {
    
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123123"); // 对称密钥
        return converter;
    }
}
3. 授权服务器配置JWT

java

@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.authenticationManager(authenticationManagerBean)
        .tokenStore(tokenStore)                    // 使用JWT存储策略
        .accessTokenConverter(jwtAccessTokenConverter)
        .reuseRefreshTokens(false)                 // 不重复使用刷新令牌
        .userDetailsService(tulingUserDetailsService)
        .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}

3.5 JWT优化:非对称加密

1. 生成JKS证书文件

bash

keytool -genkeypair \
  -alias jwt \
  -keyalg RSA \
  -keysize 2048 \
  -keystore D:/jwt/jwt.jks \
  -storepass 123456 \
  -keypass 123123 \
  -validity 365 \
  -dname "CN=fox, OU=tuling, O=tuling, L=changsha, ST=hunan, C=china"
2. 查看公钥信息

bash

keytool -list -rfc -keystore jwt.jks | openssl x509 -inform pem -pubkey
3. JWT配置属性类

java

@ConfigurationProperties(prefix = "tuling.jwt")
public class JwtCAPProperties {
    private String keyPairName;         // 证书名称
    private String keyPairAlias;        // 证书别名
    private String keyPairSecret;       // 证书私钥
    private String keyPairStoreSecret;  // 证书存储密钥
}
4. 配置非对称加密

java

@Bean
public KeyPair keyPair() {
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
        new ClassPathResource(jwtCAPProperties.getKeyPairName()),
        jwtCAPProperties.getKeyPairSecret().toCharArray()
    );
    return keyStoreKeyFactory.getKeyPair(
        jwtCAPProperties.getKeyPairAlias(),
        jwtCAPProperties.getKeyPairStoreSecret().toCharArray()
    );
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setKeyPair(keyPair()); // 使用非对称加密
    return converter;
}
5. JWT内容增强

java

public class WolfTokenEnhancer implements TokenEnhancer {
    
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, 
                                     OAuth2Authentication authentication) {
        MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();
        Map<String, Object> additionalInfo = new HashMap<>();
        
        // 添加自定义字段到JWT
        additionalInfo.put("memberId", memberDetails.getUmsMember().getId());
        additionalInfo.put("nickName", memberDetails.getUmsMember().getNickname());
        additionalInfo.put("integration", memberDetails.getUmsMember().getIntegration());
        
        Map<String, Object> retMap = new HashMap<>();
        retMap.put("additionalInfo", additionalInfo);
        
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(retMap);
        return accessToken;
    }
}
6. 配置Token增强器

java

@Autowired
private WolfTokenEnhancer wolfTokenEnhancer;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // 配置Token增强器链
    TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
    List<TokenEnhancer> delegates = new ArrayList<>();
    delegates.add(tulingTokenEnhancer);
    delegates.add(jwtAccessTokenConverter);
    enhancerChain.setTokenEnhancers(delegates);
    
    endpoints.authenticationManager(authenticationManagerBean)
        .tokenStore(tokenStore)
        .accessTokenConverter(jwtAccessTokenConverter)
        .tokenEnhancer(enhancerChain) // 配置内容增强器
        .reuseRefreshTokens(false)
        .userDetailsService(tulingUserDetailsService)
        .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}

四、接入网关服务

4.1 网关路由配置

application.yml配置:

yaml

server:
  port: 8888

spring:
  application:
    name: wolfmall-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: 80a98d11-492c-4008-85aa-32d889e9b0d0
    gateway:
      routes:
        - id: wolfmall-authcenter
          uri: lb://wolfmall-authcenter
          predicates:
            - Path=/oauth/**
        - id: wolfmall-member
          uri: lb://wolfmall-member
          predicates:
            - Path=/member/**

4.2 全局认证过滤器

1. 过滤器核心逻辑

java

@Component
@Order(0)
@EnableConfigurationProperties(value = NotAuthUrlProperties.class)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {
    
    @Autowired
    private NotAuthUrlProperties notAuthUrlProperties;
    
    @Autowired
    private RestTemplate restTemplate;
    
    private PublicKey publicKey;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String currentUrl = exchange.getRequest().getURI().getPath();
        
        // 1. 过滤不需要认证的URL
        if (shouldSkip(currentUrl)) {
            return chain.filter(exchange);
        }
        
        // 2. 获取Token
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isEmpty(authHeader)) {
            throw new GateWayException(ResultCode.AUTHORIZATION_HEADER_IS_EMPTY);
        }
        
        // 3. 校验Token
        Claims claims = JwtUtils.validateJwtToken(authHeader, publicKey);
        
        // 4. 存储用户信息到请求头
        ServerWebExchange webExchange = wrapHeader(exchange, claims);
        
        return chain.filter(webExchange);
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化时获取公钥
        this.publicKey = JwtUtils.genPublicKey(restTemplate);
    }
    
    private boolean shouldSkip(String currentUrl) {
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String skipPath : notAuthUrlProperties.getShouldSkipUrls()) {
            if (pathMatcher.match(skipPath, currentUrl)) {
                return true;
            }
        }
        return false;
    }
    
    private ServerWebExchange wrapHeader(ServerWebExchange exchange, Claims claims) {
        // 从JWT中提取用户信息
        String memberId = claims.get("additionalInfo", Map.class).get("memberId").toString();
        String nickName = claims.get("additionalInfo", Map.class).get("nickName").toString();
        
        // 添加到请求头
        ServerHttpRequest request = exchange.getRequest().mutate()
            .header("username", claims.get("user_name", String.class))
            .header("memberId", memberId)
            .header("nickName", nickName)
            .build();
        
        return exchange.mutate().request(request).build();
    }
}
2. JWT工具类

java

@Slf4j
public class JwtUtils {
    
    private static final String CLIENT_ID = "wolfmall-gateway";
    private static final String CLIENT_SECRET = "123123";
    private static final String AUTH_TOKEN_KEY_URL = "http://wolfmall-auth/oauth/token_key";
    private static final String AUTH_HEADER = "bearer ";
    
    /**
     * 远程调用获取Token Key
     */
    private static String getTokenKeyByRemoteCall(RestTemplate restTemplate) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET);
        
        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(null, headers);
        
        try {
            ResponseEntity<Map> response = restTemplate.exchange(
                AUTH_TOKEN_KEY_URL, HttpMethod.GET, entity, Map.class);
            String tokenKey = response.getBody().get("value").toString();
            log.info("获取Token Key: {}", tokenKey);
            return tokenKey;
        } catch (Exception e) {
            log.error("获取Token Key失败", e);
            throw new GateWayException(ResultCode.GET_TOKEN_KEY_ERROR);
        }
    }
    
    /**
     * 生成公钥
     */
    public static PublicKey genPublicKey(RestTemplate restTemplate) {
        String tokenKey = getTokenKeyByRemoteCall(restTemplate);
        try {
            // 处理公钥字符串
            String dealTokenKey = tokenKey
                .replaceAll("-*BEGIN PUBLIC KEY-*", "")
                .replaceAll("-*END PUBLIC KEY-*", "")
                .trim();
            
            Security.addProvider(new BouncyCastleProvider());
            X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(dealTokenKey));
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
            
            log.info("生成公钥成功");
            return publicKey;
        } catch (Exception e) {
            log.error("生成公钥异常", e);
            throw new GateWayException(ResultCode.GEN_PUBLIC_KEY_ERROR);
        }
    }
    
    /**
     * 验证JWT Token
     */
    public static Claims validateJwtToken(String authHeader, PublicKey publicKey) {
        try {
            String token = StringUtils.substringAfter(authHeader, AUTH_HEADER);
            Jwt<JwsHeader, Claims> parseClaimsJwt = Jwts.parser()
                .setSigningKey(publicKey)
                .parseClaimsJws(token);
            return parseClaimsJwt.getBody();
        } catch (Exception e) {
            log.error("校验Token异常", e);
            throw new GateWayException(ResultCode.JWT_TOKEN_EXPIRE);
        }
    }
}
3. RestTemplate配置

java

@Configuration
public class RibbonConfig {
    
    @Autowired
    private LoadBalancerClient loadBalancer;
    
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(
            Collections.singletonList(new LoadBalancerInterceptor(loadBalancer))
        );
        return restTemplate;
    }
}

4.3 测试验证

1. 获取令牌

text

POST http://localhost:8888/oauth/token?username=test&password=test&grant_type=password&client_id=client&client_secret=123123&scope=all
2. 访问受保护资源

text

GET http://localhost:8888/member/coupons
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
3. 测试结果
  • 有效令牌:正常返回数据,网关成功解析用户信息并转发

  • 无效令牌:返回401 Unauthorized错误

  • 过期令牌:返回Token过期错误


五、方案总结与最佳实践

5.1 方案优势

  1. 统一认证入口:网关集中处理认证,简化微服务实现

  2. 无状态架构:JWT支持无状态认证,扩展性好

  3. 安全性高:非对称加密确保令牌安全

  4. 性能优化:网关校验减少微服务调用压力

  5. 灵活扩展:支持多种授权模式和自定义字段

5.2 最佳实践建议

  1. 令牌管理

    • 设置合理的令牌有效期

    • 实现令牌刷新机制

    • 记录令牌使用日志

  2. 安全性考虑

    • 使用HTTPS传输令牌

    • 定期更换密钥对

    • 实现黑名单机制

  3. 性能优化

    • 网关层缓存公钥

    • 使用连接池优化远程调用

    • 监控认证服务性能

  4. 监控告警

    • 监控认证失败率

    • 设置令牌过期告警

    • 记录异常访问日志

5.3 常见问题解决

问题 解决方案
令牌泄露 缩短有效期,实现黑名单
性能瓶颈 网关缓存公钥,优化JWT解析
跨域问题 网关统一处理CORS
服务宕机 多实例部署,负载均衡

5.4 扩展方向

  1. 多因子认证:集成短信、邮件验证码

  2. 社交登录:支持微信、QQ等第三方登录

  3. 权限细化:基于RBAC的细粒度权限控制

  4. 审计日志:完整的操作审计和追溯


结语

微服务架构下的统一认证方案是保障系统安全的核心组件。本文通过电商项目实战,详细讲解了从OAuth2.0授权服务器搭建、JWT令牌优化到网关认证集成的完整流程。该方案已在生产环境中验证,具备高可用、高性能、易扩展的特点,可为微服务架构的安全认证提供可靠保障。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐