29、微服务安全:OAuth 2.0 与 JWT 实战指南

微服务安全:OAuth 2.0 与 JWT 实战指南

1. OAuth 2.0 授权服务器搭建

授权服务器的主要职责是向客户端发放令牌,并响应下游微服务的验证请求,同时它也充当安全令牌服务(STS)。目前有许多开源的 OAuth 2.0 授权服务器,如 WSO2 Identity Server、Keycloak、Gluu 等。在生产环境中可以选择使用这些开源服务器,但为了方便开发者测试,这里使用 Spring Boot 搭建一个简单的 OAuth 2.0 授权服务器。

1.1 添加 Maven 依赖

ch12/sample01/pom.xml 文件中添加以下依赖,这些依赖引入了新的注解,可将 Spring Boot 应用转换为 OAuth 2.0 授权服务器。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
</dependency>
1.2 配置授权服务器类

sample01/src/main/java/com/apress/ch12/sample01/TokenServiceApp.java 类中添加 @EnableAuthorizationServer 注解,将项目转换为 OAuth 2.0 授权服务器。同时添加 @EnableResourceServer 注解,使其作为资源服务器来验证访问令牌并返回用户信息。

// TokenServiceApp.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@SpringBootApplication
@EnableAuthorizationServer
@EnableResourceServer
public class TokenServiceApp {
    public static void main(String[] args) {
        SpringApplication.run(TokenServiceApp.class, args);
    }
}
1.3 注册客户端

sample01/src/main/java/com/apress/ch12/sample01/config/AuthorizationServerConfig.java 文件中注册客户端,设置客户端 ID、客户端密钥、可用范围、授权类型和访问令牌的有效期。

// AuthorizationServerConfig.java
import org.springframework.context.annotation.Configuration;
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 java.util.concurrent.TimeUnit;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("10101010")
               .secret("11110000")
               .scopes("foo", "bar")
               .authorizedGrantTypes("client_credentials", "password", "refresh_token")
               .accessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(1));
    }
}
1.4 配置用户存储

为了支持密码授权类型,授权服务器需要连接到用户存储。这里使用内存用户存储,在 sample01/src/main/java/com/apress/ch12/sample01/config/WebSecurityConguration.java 文件中添加用户。

// WebSecurityConguration.java
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
           .withUser("peter")
           .password("peter123")
           .roles("USER");
    }
}
1.5 关联用户存储与授权流程

sample01/src/main/java/com/apress/ch12/sample01/config/AuthorizationServerConfig.java 文件中关联内存用户存储与 OAuth 2.0 授权流程。

// AuthorizationServerConfig.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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 java.util.concurrent.TimeUnit;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("10101010")
               .secret("11110000")
               .scopes("foo", "bar")
               .authorizedGrantTypes("client_credentials", "password", "refresh_token")
               .accessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(1));
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }
}
1.6 启动授权服务器

ch12/sample01/ 目录下执行以下命令启动授权服务器:

mvn spring-boot:run
1.7 获取访问令牌
  • 客户端凭证授权类型 :使用以下命令获取访问令牌,需将 $CLIENTID $CLIENTSECRET 替换为实际值。
curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=client_credentials&scope=foo" https://localhost:8443/oauth/token
  • 密码授权类型 :使用以下命令获取访问令牌,需将 $CLIENTID $CLIENTSECRET $USERNAME $PASSWORD 替换为实际值。
curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=password&username=$USERNAME&password=$PASSWORD&scope=foo" https://localhost:8443/oauth/token
1.8 验证访问令牌

使用以下命令验证访问令牌,需将 $TOKEN 替换为实际的访问令牌。

curl -k -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" https://localhost:8443/user
2. 使用 OAuth 2.0 保护微服务

在 OAuth 术语中,受保护的微服务被称为资源服务器。这里以订单处理微服务为例,展示如何使用 OAuth 2.0 保护 Spring Boot 微服务。

2.1 添加注解和配置文件

sample02/src/main/java/com/apress/ch12/sample02/OrderProcessingApp.java 类中添加 @EnableResourceServer 注解,并在 sample02/src/main/resources/application.properties 文件中配置授权服务器的用户信息端点。

# application.properties
server.port=9443
server.ssl.key-store: keystore.jks
server.ssl.key-store-password: springboot
server.ssl.keyAlias: spring
security.oauth2.resource.user-info-uri=https://localhost:8443/user
#security.oauth2.resource.jwt.keyUri: https://localhost:8443/oauth/token_key
2.2 处理证书信任问题

由于订单处理微服务通过 HTTPS 调用授权服务器的用户信息端点,而使用的是自签名证书,会导致信任验证错误。可以通过以下步骤解决:
1. 导出授权服务器的公共证书:

keytool -export -keystore keystore.jks -alias spring -file cert.crt
  1. 创建新的信任存储:
keytool -import -file cert.crt -alias authserver -keystore trust-store.jks
  1. 在代码中设置信任存储的位置和密码:
// OrderProcessingApp.java
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import java.io.File;

public class OrderProcessingApp {
    static {
        String path = System.getProperty("user.dir");
        System.setProperty("javax.net.ssl.trustStore", path + File.separator + "trust-store.jks");
        System.setProperty("javax.net.ssl.trustStorePassword", "springboot");
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
    }
    // 其他代码
}
2.3 启动订单处理微服务

ch12/sample02/ 目录下执行以下命令启动订单处理微服务:

mvn spring-boot:run
2.4 测试服务
  • 无有效令牌调用
curl -k https://localhost:9443/order/11
  • 有有效令牌调用
curl -k -H "Authorization: Bearer $TOKEN" https://localhost:9443/order/11
3. 微服务安全流程
graph LR
    A[客户端] -->|请求令牌| B(授权服务器)
    B -->|发放令牌| A
    A -->|携带令牌请求资源| C(资源服务器)
    C -->|验证令牌| B
    B -->|返回验证结果| C
    C -->|根据结果响应请求| A
4. 访问令牌获取与验证对比
操作类型 命令示例 说明
客户端凭证授权获取令牌 curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=client_credentials&scope=foo" https://localhost:8443/oauth/token 适用于无用户交互场景,使用客户端 ID 和密钥获取令牌
密码授权获取令牌 curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=password&username=$USERNAME&password=$PASSWORD&scope=foo" https://localhost:8443/oauth/token 需要用户提供用户名和密码获取令牌
验证令牌 curl -k -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" https://localhost:8443/user 验证令牌的有效性并返回相关元数据

微服务安全:OAuth 2.0 与 JWT 实战指南

5. 使用自包含访问令牌(JWT)保护微服务

在前面的内容中,我们已经了解了如何使用 OAuth 2.0 来保护微服务。接下来,我们将探讨如何使用自包含访问令牌(JWT)来进一步增强微服务的安全性。

5.1 设置授权服务器以颁发 JWT

为了让授权服务器能够颁发 JWT,我们需要完成以下几个步骤:

  1. 创建密钥对和密钥库 :使用 keytool 命令创建一个新的密钥库和密钥对,用于签署 JWT。
keytool -genkey -alias jwtkey -keyalg RSA -keysize 2048 -dname "CN=localhost" -keypass springboot -keystore jwt.jks -storepass springboot

将生成的 jwt.jks 密钥库复制到 sample01/src/main/resources/ 目录下。

  1. 配置应用属性 :在 sample01/src/main/resources/application.properties 文件中设置相关属性,以启用 JWT 颁发功能。
spring.security.oauth.jwt: true
spring.security.oauth.jwt.keystore.password: springboot
spring.security.oauth.jwt.keystore.alias: jwtkey
spring.security.oauth.jwt.keystore.name: jwt.jks
  1. 添加 Maven 依赖 :在 pom.xml 文件中添加 spring-security-jwt 依赖,用于构建 JWT。
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-jwt</artifactId>
</dependency>
  1. 配置授权服务器代码 :在 sample01/src/main/java/com/apress/ch12/sample01/config/AuthorizationServerConfig.java 类中添加相关方法,以支持 JWT 的生成和存储。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
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.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.KeyStoreKeyFactory;

import java.io.File;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private Environment environment;

    @Bean
    protected JwtAccessTokenConverter jwtConeverter() {
        String pwd = environment.getProperty("spring.security.oauth.jwt.keystore.password");
        String alias = environment.getProperty("spring.security.oauth.jwt.keystore.alias");
        String keystore = environment.getProperty("spring.security.oauth.jwt.keystore.name");
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
                new File(keystore), pwd.toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        String useJwt = environment.getProperty("spring.security.oauth.jwt");
        if (useJwt != null && "true".equalsIgnoreCase(useJwt.trim())) {
            return new JwtTokenStore(jwtConeverter());
        } else {
            return new InMemoryTokenStore();
        }
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("10101010")
               .secret("11110000")
               .scopes("foo", "bar")
               .authorizedGrantTypes("client_credentials", "password", "refresh_token")
               .accessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(1));
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        String useJwt = environment.getProperty("spring.security.oauth.jwt");
        if (useJwt != null && "true".equalsIgnoreCase(useJwt.trim())) {
            endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtConeverter())
                     .authenticationManager(authenticationManager);
        } else {
            endpoints.authenticationManager(authenticationManager);
        }
    }
}
  1. 启动授权服务器 :在 ch12/sample01/ 目录下执行以下命令启动授权服务器。
mvn spring-boot:run
  1. 获取 JWT 访问令牌 :使用客户端凭证授权类型获取 JWT 访问令牌,将 $CLIENTID $CLIENTSECRET 替换为实际值。
curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=client_credentials&scope=foo" https://localhost:8443/oauth/token

返回的 JWT 是 Base64 URL 编码的,解码后的头部和有效负载示例如下:

{ "alg": "RS256", "typ": "JWT" }
{ "scope": [ "foo" ], "exp": 1524793284, "jti": "6e55840e-886c-46b2-bef7-1a14b813dd0a", "client_id": "10101010" }
5.2 使用 JWT 保护微服务

为了让订单处理微服务支持 JWT 验证,我们需要对配置文件进行修改。

sample02/src/main/resources/application.properties 文件中,注释掉 security.oauth2.resource.user-info-uri 属性,并取消注释 security.oauth2.resource.jwt.keyUri 属性。

#security.oauth2.resource.user-info-uri:https://localhost:8443/user
security.oauth2.resource.jwt.keyUri: https://localhost:8443/oauth/token_key

security.oauth2.resource.jwt.keyUri 属性指向授权服务器用于签署 JWT 的私钥对应的公钥端点。资源服务器(即订单处理微服务)将使用该公钥来验证请求中包含的 JWT 的签名。

6. JWT 保护微服务流程
graph LR
    A[客户端] -->|请求 JWT 令牌| B(授权服务器)
    B -->|颁发 JWT 令牌| A
    A -->|携带 JWT 令牌请求资源| C(资源服务器)
    C -->|使用公钥验证 JWT 签名| B
    B -->|返回验证结果| C
    C -->|根据结果响应请求| A
7. 不同授权方式总结
授权方式 适用场景 特点
客户端凭证授权 无用户交互,客户端以自身身份请求资源 无需用户参与,使用客户端 ID 和密钥获取令牌,无刷新令牌
密码授权 用户直接参与,提供用户名和密码 需要用户提供敏感信息,有刷新令牌
JWT 授权 增强安全性,减少对授权服务器的依赖 自包含令牌,包含信息多,验证依赖公钥
8. 总结

通过本文,我们详细介绍了如何使用 OAuth 2.0 和 JWT 来保护微服务。从搭建 OAuth 2.0 授权服务器,到使用 OAuth 2.0 保护微服务,再到引入 JWT 进一步增强安全性,我们逐步深入地探讨了微服务安全的各个方面。

在实际应用中,我们可以根据具体的业务需求和安全要求选择合适的授权方式。客户端凭证授权适用于无用户交互的场景,密码授权适用于需要用户直接参与的场景,而 JWT 授权则可以在保证安全性的同时,减少对授权服务器的依赖。

希望本文能够帮助你更好地理解和应用 OAuth 2.0 和 JWT 来保护微服务,提升系统的安全性和可靠性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值