1. 概述

在本教程中,我们将讨论如何让 Spring Security OAuth2 实现使用 JSON Web 令牌。

我们还将继续在此 OAuth 系列中的Spring REST API + OAuth2 + Angular文章的基础上进行构建。

2.OAuth2授权服务器

以前,Spring Security OAuth 堆栈提供了将授权服务器设置为 Spring 应用程序的可能性。然后,我们必须将其配置为使用 JwtTokenStore ,以便我们可以使用 JWT 令牌。

然而,OAuth 堆栈已被 Spring 弃用,现在我们将使用 Keycloak 作为我们的授权服务器。

所以这一次,我们将在 Spring Boot 应用程序中将授权服务器设置为嵌入式 Keycloak 服务器 。它默认发行 JWT 令牌,因此无需进行任何其他配置。

3.资源服务器

现在让我们看看如何配置资源服务器以使用 JWT。

我们将在 application.yml 文件中执行此操作:

server: 
  port: 8081
  servlet: 
    context-path: /resource-server

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung
          jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

JWT包含Token中的所有信息,因此资源服务器需要验证Token的签名以确保数据没有被修改。 jwk-set-uri 属性 包含 服务器可用于此目的的 公钥

Issuer-uri 属性指向基本授权服务器 URI,它也可用于验证 iss 声明,作为附加的安全措施。

此外,如果未设置 jwk-set-uri 属性,资源服务器将尝试使用 颁发者 uri授权服务器元数据端点确定此密钥的位置。

值得注意的是,添加 Issuer-uri 属性要求 我们必须先运行授权服务器,然后才能启动资源服务器应用程序

现在让我们看看如何使用 Java 配置来配置 JWT 支持:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
              .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
                  .hasAuthority("SCOPE_read")
                .antMatchers(HttpMethod.POST, "/api/foos")
                  .hasAuthority("SCOPE_write")
                .anyRequest()
                  .authenticated()
            .and()
              .oauth2ResourceServer()
                .jwt();
    }
}

这里我们覆盖默认的 Http Security 配置;我们需要明确指定 * 我们希望它充当资源服务器,并且我们将分别使用 oauth2ResourceServer()jwt() 方法来使用 JWT 格式的访问令牌。

上面的 JWT 配置是默认的 Spring Boot 实例为我们提供的。这也可以定制,我们很快就会看到。

4. 代币中的自定义声明

现在让我们设置一些基础设施,以便能够 在授权服务器返回的访问令牌中添加一些自定义声明 。框架提供的标准声明都很好,但大多数时候我们需要令牌中的一些额外信息才能在客户端使用。

让我们以自定义声明 organization 为例,它将包含给定用户组织的名称。

4.1.授权服务器配置

为此,我们需要向领域定义文件 baeldung-realm.json 添加一些配置:

  • 为我们的用户添加属性 组织 [email protected]
    "attributes" : {
      "organization" : "baeldung"
    },
    
  • 将名为 organizationprotocolMapper 添加到 jwtClient 配置中:
    "protocolMappers": [{
      "id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1",
      "name": "organization",
      "protocol": "openid-connect",
      "protocolMapper": "oidc-usermodel-attribute-mapper",
      "consentRequired": false,
      "config": {
        "userinfo.token.claim": "true",
        "user.attribute": "organization",
        "id.token.claim": "true",
        "access.token.claim": "true",
        "claim.name": "organization",
        "jsonType.label": "String"
      }
    }],
    

对于独立的 Keycloak 设置,也可以使用管理控制台来完成。

请务必记住, 上面的 JSON 配置特定于 Keycloak,并且对于其他 OAuth 服务器可能有所不同

随着这个新配置的启动和运行,我们将在 [email protected] 的令牌有效负载中获得一个额外的属性, organization = baeldung

{
  jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e"
  exp: 1585242462
  nbf: 0
  iat: 1585242162
  iss: "http://localhost:8083/auth/realms/baeldung"
  sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f"
  typ: "Bearer"
  azp: "jwtClient"
  auth_time: 1585242162
  session_state: "384ca5cc-8342-429a-879c-c15329820006"
  acr: "1"
  scope: "profile write read"
  organization: "baeldung"
  preferred_username: "[email protected]"
}

4.2.在 Angular 客户端中使用访问令牌

接下来,我们将要在 Angular 客户端应用程序中使用令牌信息。我们将使用angular2-jwt库来实现这一点。

我们将在 AppService 中使用 组织 声明,并添加一个函数 getOrganization

getOrganization(){
  var token = Cookie.get("access_token");
  var payload = this.jwtHelper.decodeToken(token);
  this.organization = payload.organization; 
  return this.organization;
}

该函数利用 angular2-jwt 库中的 JwtHelperService 来解码访问令牌并获取我们的自定义声明。现在我们需要做的就是在 AppComponent 中显示它:

@Component({
  selector: 'app-root',
  template: `<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" href="/">Spring Security Oauth - Authorization Code</a>
    </div>
  </div>
  <div class="navbar-brand">
    <p>{{organization}}</p>
  </div>
</nav>
<router-outlet></router-outlet>`
})

export class AppComponent implements OnInit {
  public organization = "";
  constructor(private service: AppService) { }  
   
  ngOnInit() {  
    this.organization = this.service.getOrganization();
  }  
}

5. 在资源服务器中访问额外声明

但是我们如何在资源服务器端访问这些信息呢?

5.1.访问身份验证服务器声明

这非常简单,我们只需要从 org.springframework.security.oauth2.jwt.Jwt AuthenticationPrincipal 中提取它, 就像我们对 UserInfoController 中的任何其他属性所做的那样:

@GetMapping("/user/info")
public Map<String, Object> getUserInfo(@AuthenticationPrincipal Jwt principal) {
    Map<String, String> map = new Hashtable<String, String>();
    map.put("user_name", principal.getClaimAsString("preferred_username"));
    map.put("organization", principal.getClaimAsString("organization"));
    return Collections.unmodifiableMap(map);
}

5.2.添加/删除/重命名声明的配置

现在,如果我们想在资源服务器端添加更多声明怎么办?或者删除或重命名一些?

假设我们要修改来自身份验证服务器的 组织 声明以获取大写的值。但是,如果用户不存在该声明,我们需要将其值设置为 unknown

为了实现这一点,我们必须 添加一个实现 Converter 接口的类并使用 MappedJwtClaimSetConverter 来转换声明

public class OrganizationSubClaimAdapter implements 
  Converter<Map<String, Object>, Map<String, Object>> {
    
    private final MappedJwtClaimSetConverter delegate = 
      MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);
        String organization = convertedClaims.get("organization") != null ? 
          (String) convertedClaims.get("organization") : "unknown";
        
        convertedClaims.put("organization", organization.toUpperCase());

        return convertedClaims;
    }
}

然后,在我们的 SecurityConfig 类中,我们需要 添加我们自己的 JwtDecoder 实例 来覆盖 Spring Boot 提供的实例 ,并将 OrganizationSubClaimAdapter 设置为其声明转换器

@Bean
public JwtDecoder customDecoder(OAuth2ResourceServerProperties properties) {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(
      properties.getJwt().getJwkSetUri()).build();
    
    jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter());
    return jwtDecoder;
}

现在,当我们点击用户 [email protected]/user/info API 时,我们将获得该 组织 UNKNOWN

请注意,应谨慎覆盖 Spring Boot 配置的默认 JwtDecoder bean,以确保仍包含所有必要的配置。

6. 从 Java 密钥库加载密钥

在之前的配置中,我们使用授权服务器的默认公钥来验证令牌的完整性。

我们还可以使用存储在 Java 密钥库文件中的密钥对和证书来执行签名过程。

6.1.生成 JKS Java 密钥库文件

让我们首先使用命令行工具 keytool 生成密钥,更具体地说是 .jks 文件:

keytool -genkeypair -alias mytest 
                    -keyalg RSA 
                    -keypass mypass 
                    -keystore mytest.jks 
                    -storepass mypass

该命令将生成一个名为 mytest.jks 的文件,其中包含我们的密钥、公钥和私钥。

还要确保 keypassstorepass 相同。

6.2.导出公钥

接下来我们需要从生成的 JKS 中导出公钥。我们可以使用以下命令来执行此操作:

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

响应示例如下所示:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1
czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2
MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV
BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj
Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM
urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
lLFCUGhA7hxn2xf3x1JW
-----END CERTIFICATE-----

6.3. Maven 配置

我们不希望 Maven 过滤过程拾取 JKS 文件,因此我们将确保将其排除在 pom.xml 中:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <excludes>
                <exclude>*.jks</exclude>
            </excludes>
        </resource>
    </resources>
</build>

如果我们使用 Spring Boot,我们需要确保通过 Spring Boot Maven 插件 addResources 将 JKS 文件添加到应用程序类路径中:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <addResources>true</addResources>
            </configuration>
        </plugin>
    </plugins>
</build>

6.4.授权服务器

现在,我们将配置 Keycloak 以使用 mytest.jks 中的密钥对,方法是将其添加到领域定义 JSON 文件的 KeyProvider 部分,如下所示:

{
  "id": "59412b8d-aad8-4ab8-84ec-e546900fc124",
  "name": "java-keystore",
  "providerId": "java-keystore",
  "subComponents": {},
  "config": {
    "keystorePassword": [ "mypass" ],
    "keyAlias": [ "mytest" ],
    "keyPassword": [ "mypass" ],
    "active": [ "true" ],
    "keystore": [
            "src/main/resources/mytest.jks"
          ],
    "priority": [ "101" ],
    "enabled": [ "true" ],
    "algorithm": [ "RS256" ]
  }
},

在这里,我们将 优先级 设置为 101 ,大于授权服务器的任何其他密钥对,并将 active 设置为 true 。这样做是为了确保我们的资源服务器将从我们之前指定的 jwk-set-uri 属性中选择这个特定的密钥对。

同样,此配置特定于 Keycloak,对于其他 OAuth 服务器实现可能有所不同。

七、结论

在这篇简短的文章中,我们重点介绍了如何设置 Spring Security OAuth2 项目以使用 JSON Web 令牌。

本文的完整实现可以在 GitHub 上找到。