带有JWT的Spring Security:OAuth 2资源服务器

Since version 5.2, Spring has introduced a new library, OAuth 2.0 Resource 小号ever, handling JWT so that we no longer need to manually add a Filter to extract claims from JWT token and verify the token.

What is a Resource server?

Resource server provides protected resources. It communicates with its Authorization server to validate a request to access a protected resource. Typically the endpoints of a resource server are protected based on the Oauth2 scopes and user roles.
Please refer to this for more details.

Example Token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU

When you decode it from jwt.io, you find that the JWT structure consists of 3 parts: Header, Payload, Signature.

Header

它通常包含两个字段:

  1. 一种令牌,类型,智威汤逊签名算法阿尔格, HS256
{
  "typ": "JWT",
  "alg": "HS256"
}

Payload

The payload contains a set of claims. e.g. iss (issuer), exp (expiration time), sub (subject)

{
  "iss": "http://my.microservice.com/",
  "sub": "subject",
  "scope": [
    "read"
  ],
  "exp": 4740547387,
  "jti": "c8aa2f77-6666-47f7-b56e-424e1c1e18cb",
  "iat": 1586947387
}

用来授权我们的端点的主张是范围:读。

According to this, Spring OAuth 2 Resource Server, by default, looks for the clam names: scope and scp, as they are well-known claims for authorisation. If you are going use a custom claim name, you can see the example at the end of this post.

Example Project

We're going to use Spring Initializr to generate Spring Boot project from scratch.

带有JWT的Spring Security:OAuth 2资源服务器_第1张图片

这是build.gradle文件中的依赖项:

plugins {
  id 'org.springframework.boot' version '2.2.6.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
  mavenCentral()
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}
test {
  useJUnitPlatform()
}

如您所见,我们使用Spring Boot版本2.2.6.RELEASE。 spring-boot-starter-oauth2-resource-server包含spring-security-oauth2-jose版本5.2.5.RELEASE,其中包含nimbus-jose-jwt库以支持JWT解码。


Controller

我们创建了2个端点:

  1. “ /”终结点-接受HTTP GET方法并期望带有“ Authorization:Bearer(JWT Token)”的HTTP标头"/message" endpoints accepts 2 HTTP methods: GET and POST
import org.springframework.security.core.annotation.AuthenticationPrincipal;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RestController;  

@RestController  
public class Controller {  

  @GetMapping("/")  
  public String index(@AuthenticationPrincipal Jwt jwt) {  
    return String.format("Hello, %s!", jwt.getSubject());  
  }  

  @GetMapping("/message")  
  public String message() {  
    return "secret message";  
  }  

  @PostMapping("/message")  
  public String createMessage(@RequestBody String message) {  
    return String.format("Message was created. Content: %s", message);  
  }  
}

Configuration

  1. 我们为/ message端点定义安全规则。 消息端点将检查是否请求具有权限读用于GET方法请求具有权限写对于POST方法我们还告诉Spring我们将使用带有JSON Web令牌(JWT)的OAuth2资源服务器。我们禁用会话管理–这将阻止创建会话cookieHTTP基本认证默认的Spring登录页面CSRF.
import org.springframework.http.HttpMethod;  
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.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;  


@EnableWebSecurity  
public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {  

  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasAuthority("SCOPE_read")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasAuthority("SCOPE_write")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  
}

Note that this configuration file is expressed as DSL. Unlike the traditional approach with builder chaining, we can use Java 8 lamda to express the configurations.

application.yml

spring:  
  security:  
    oauth2:  
      resourceserver:  
        jwt:  
          jwk-set-uri: https://login.domain.com/xxx/keys # JSON Web Key URI to use to verify the JWT token.

Expected Results

当您调用带有无效声明的安全终结点时,您会收到HTTP 403消息。 例如,您发送了一个读为范围但端点期望写 范围.

当JWT授权失败时,您会收到HTTP 401消息。 例如,

  1. 令牌未被发行者识别。令牌已过期。令牌是无效的结构。等等

Testing

使用HTTP请求消息端点得到。 令牌包含读范围

GET http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Date: Wed, 15 Apr 2020 16:12:03 GMT

secret message

Response code: 200; Time: 1261ms; Content length: 337bytes

使用HTTP请求消息端点开机自检。 令牌包含读范围。 但它期望写范围。

POST http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
HTTP/1.1 403 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 15 Apr 2020 20:12:00 GMT

{
  "timestamp": "2019-04-15T12:27:25.020+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/message"
}

Response code: 403; Time: 28ms; Content length: 125 byte

Custom claim

如果我们的JWT不包含众所周知的声明怎么办?范围,scp)进行授权?

我们将使用索赔名称:角色例如。

Token with claim: roles

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY

Payload

{
  "iss": "http://my.microservice.com/",
  "sub": "subject",
  "roles": [
    "student"
  ],
  "exp": 4740570123,
  "jti": "379ea761-3e50-4362-8e12-d072346a7be1",
  "iat": 1586970123
}

This section is going to illustrate on how to modify a the Default JWŤ Converter.

我们将修改现有的配置文件,如下所示:

package com.example.resourcesever;  

import org.springframework.core.convert.converter.Converter;  
import org.springframework.http.HttpMethod;  
import org.springframework.security.authentication.AbstractAuthenticationToken;  
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.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;  

import java.util.Collection;  


@EnableWebSecurity  
public class OAuth2ResourceServerSecurityCustomConfiguration extends WebSecurityConfigurerAdapter {  

  private static final String AUTHORITY_PREFIX = "ROLE_";  
 private static final String CLAIM_ROLES = "roles";  

  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasAuthority("ROLE_student")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasAuthority("ROLE_admin")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(oauth2ResourceServer ->  
            oauth2ResourceServer  
                .jwt(jwt ->  
                    jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter()))  
        )  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  

  private Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {  
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();  
  jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());  
 return jwtAuthenticationConverter;  
  }  

  private Converter<Jwt, Collection<GrantedAuthority>> getJwtGrantedAuthoritiesConverter() {  
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();  
  converter.setAuthorityPrefix(AUTHORITY_PREFIX);  
  converter.setAuthoritiesClaimName(CLAIM_ROLES);  
 return converter;  
  }  
}

从上面开始,首先,我们告诉Spring我们要使用声明名称角色代替范围要么scp。

  • 只要学生角色将通过GET方法授权。只要管理员角色将通过POST方法授权。

其次,我们要设置权限前缀角色_代替范围_

我们可以进一步修改。。。我们可以使用hasRole代替hasAuthority。

package com.example.resourcesever;  

import org.springframework.core.convert.converter.Converter;  
import org.springframework.http.HttpMethod;  
import org.springframework.security.authentication.AbstractAuthenticationToken;  
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.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;  

import java.util.Collection;  


@EnableWebSecurity  
public class OAuth2ResourceServerSecurityCustomConfiguration extends WebSecurityConfigurerAdapter {  

  private static final String AUTHORITY_PREFIX = "ROLE_";  
 private static final String CLAIM_ROLES = "roles";  

  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasRole("student")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasRole("admin")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(oauth2ResourceServer ->  
            oauth2ResourceServer  
                .jwt(jwt ->  
                    jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter()))  
        )  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  

  private Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {  
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();  
  jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());  
 return jwtAuthenticationConverter;  
  }  

  private Converter<Jwt, Collection<GrantedAuthority>> getJwtGrantedAuthoritiesConverter() {  
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();  
  converter.setAuthorityPrefix(AUTHORITY_PREFIX);  
  converter.setAuthoritiesClaimName(CLAIM_ROLES);  
 return converter;  
  }  
}

Notice that I provide only the role name(admin, student) without the prefix ROLE_ to the hasRole() method, because the implementation of hasRole() does not expect us to put the prefix ROLE_, it will do for us.

Testing with Custom claim: roles

使用HTTP请求消息端点得到。 令牌包含学生角色

GET http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Date: Wed, 15 Apr 2020 17:11:01 GMT

secret message

Response code: 200; Time: 1261ms; Content length: 337bytes

使用HTTP请求消息端点开机自检。 令牌包含学生角色。 但它期望管理员角色。

POST http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY
HTTP/1.1 403 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 15 Apr 2020 22:10:05 GMT

{
  "timestamp": "2019-04-15T12:27:25.020+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/message"
}

Response code: 403; Time: 28ms; Content length: 125 byte

Conclusion

OAuth 2资源服务器库为我们提供了最低限度的配置。 我们不再需要编写过滤器了。

from: https://dev.to//toojannarong/spring-security-with-jwt-the-easiest-way-2i43

你可能感兴趣的:(java,json)