SpringSecurity OAuth2 custom token configuration (JWT)

catalog:

  • Custom token configuration
  • Replace default token with JWT
  • Extended JWT
  • Parsing JWT in JAVA
  • refresh token

Spring Security allows us to customize the token configuration, such as different clients_ The ID corresponds to different tokens, the effective time of the token, the storage strategy of the token, etc; We can also use JWT to replace the default token.

Custom token configuration

Let's let the authentication server AuthorizationServerConfig inherit the AuthorizationServerConfigurerAdapter and rewrite its configure(ClientDetailsServiceConfigurer clients) method:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenEnhancer tokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("test1")
                .secret(new BCryptPasswordEncoder().encode("test1111"))
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(864000)
                .scopes("all", "a", "b", "c")
                .authorizedGrantTypes("password")
                .and()
                .withClient("test2")
                .secret(new BCryptPasswordEncoder().encode("test2222"))
                .accessTokenValiditySeconds(7200);
    }
}

After the authentication server inherits the authorization server configureradapter adapter, it needs to override the configure (authorization server endpoints configurer endpoints) method to specify the AuthenticationManager and UserDetailService.

Create a new configuration class SecurityConfig and register the AuthenticationManagerBean we need in it:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

In addition, overriding the configure(ClientDetailsServiceConfigurer clients) method mainly configures:

  1. Define two clients_ ID, and the client can use different clients_ ID to obtain different tokens;
  2. client_ The valid time of the token with ID test1 is 3600 seconds, client_ The valid time of the token with ID test2 is 7200 seconds;
  3. client_ Refresh with ID test1_ The effective time of the token (described below) is 864000 seconds, that is, 10 days, that is, it can be refreshed within these 10 days_ Token in exchange for a new token;
  4. Getting client_ When the token ID is test1, the scope can only be specified as a value in all, a, b or c, otherwise the acquisition will fail;
  5. The client can only be obtained through password mode_ Token with ID test1, while test2 is unlimited.

After starting the project, use the password mode to obtain the token of test1:

image.png

As described earlier, the header needs to pass in the value of test1:test1111 encrypted by base64:

image.png


Return result:

{
    "access_token": "3f9864cc-dab5-420a-b129-560f409922d6",
    "token_type": "bearer",
    "expires_in": 3599,
    "scope": "all"
}

Replace default token with JWT

To replace the default token with JWT (the default token is generated by UUID), you only need to specify the TokenStore as JwtTokenStore.

Create a JWTokenConfig configuration class:

@Configuration
public class JWTokenConfig {

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("test_key"); // Signature key
        return accessTokenConverter;
    }
}

The signature key is test_key. After configuring JwtTokenStore in the configuration class, we specify it in the authentication server:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailService userDetailService;
    @Autowired
    private TokenStore jwtTokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .userDetailsService(userDetailService);
    }

...
}

Restart the service to obtain the token, and the system will return the token in the following format:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Njg3ODkzNTIsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2NzhlZTc5NC1lYzhhLTQxOTYtYmE2NS0xM2QwNjRiOGEyZWUiLCJjbGllbnRfaWQiOiJ0ZXN0MSIsInNjb3BlIjpbImFsbCJdfQ.ojuPewlJ_3hWRwOrlCwcHFZaNIX_78FQwj86Inw79_w",
    "token_type": "bearer",
    "expires_in": 3599,
    "scope": "all",
    "jti": "678ee794-ec8a-4196-ba65-13d064b8a2ee"
}

Set access_ Copy the contents of the token to https://jwt.io/ Website analysis:

image.png

Expand JWT

The PAYLOAD content obtained from the Token parsing above is:

{
  "exp": 1568789352,
  "user_name": "user",
  "authorities": [
    "admin"
  ],
  "jti": "678ee794-ec8a-4196-ba65-13d064b8a2ee",
  "client_id": "test1",
  "scope": [
    "all"
  ]
}

If we want to add some additional information to JWT, we need to implement TokenEnhancer (Token enhancer):

public class JWTTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("message", "hello world");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

We added the message: hello world message to the Token. Then register the Bean in JWTokenConfig:

@Configuration
public class JWTokenConfig {
    ......

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new JWTokenEnhancer();
    }
}

Finally, configure the intensifier in the authentication server:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailService userDetailService;
    @Autowired
    private TokenStore jwtTokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private TokenEnhancer tokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancers = new ArrayList<>();
        enhancers.add(tokenEnhancer);
        enhancers.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(enhancers);

        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore)
                .tokenEnhancer(enhancerChain)
                .accessTokenConverter(jwtAccessTokenConverter)
                .userDetailsService(userDetailService);
    }
...
}

Restart the project and obtain the token again. The system returns:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc4OTkzNSwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiZDlmM2M4NGItMWNlNy00YWJmLWE1ZjUtODlkMTVmMTA2YTRhIiwiY2xpZW50X2lkIjoidGVzdDEifQ.WjIvH4dpaF8oKXh1qWuSP5o4slgpG9fAWzGTBUrwlA4",
    "token_type": "bearer",
    "expires_in": 3599,
    "scope": "all",
    "message": "hello world",
    "jti": "d9f3c84b-1ce7-4abf-a5f5-89d15f106a4a"
}

You can see that the returned JSON content has more message information we added, and access will be added_ The token is copied to the jwt.io website for parsing. The contents are as follows:

{
  "user_name": "user",
  "scope": [
    "all"
  ],
  "exp": 1568789935,
  "message": "hello world",
  "authorities": [
    "admin"
  ],
  "jti": "d9f3c84b-1ce7-4abf-a5f5-89d15f106a4a",
  "client_id": "test1"
}

The parsed JWT also contains the message information we added.

Parsing JWT in Java

To parse JWT in Java code, you need to add the following dependencies:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Write interface index

    @GetMapping("/index")
    public Object index(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        String token = StringUtils.substringAfter(header, "bearer ");

        return Jwts.parser().setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
    }

signkey needs to be consistent with the signature key specified in JwtAccessTokenConverter. Restart the project, access / index after obtaining the token, and the output is as follows:

{
    "user_name": "user",
    "scope": [
        "all"
    ],
    "exp": 1568790296,
    "message": "hello world",
    "authorities": [
        "admin"
    ],
    "jti": "4da297b1-9c12-4251-bf18-93bbc35d25bd",
    "client_id": "test1"
}

refresh token

After the token expires, we can use refresh_token in exchange for a new available token from the system. However, as can be seen from the previous example, the JSON information returned after successful authentication does not contain refresh_token to make the system return refresh_ For token, you need to add the following configuration in the user-defined configuration of the authentication server:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    ......

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("test1")
                .secret(new BCryptPasswordEncoder().encode("test1111"))
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(864000)
                .scopes("all", "a", "b", "c")
            .and()
                .withClient("test2")
                .secret(new BCryptPasswordEncoder().encode("test2222"))
                .accessTokenValiditySeconds(7200);
    }
}

Refresh should be added to the authorization method_ In addition to the four standard OAuth2 token obtaining methods, Spring Security OAuth2 internally uses refresh_token is regarded as an extended way to obtain tokens.

Through the above configuration, use test1 as the client_ Refresh will be returned when ID gets token_ token´╝îrefresh_ The valid period of a token is 10 days, that is, you can exchange it for a new available token within 10 days.

Restart the project. After successful authentication, the system returns as follows:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc5MDY4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMGMwZjg3N2ItMmNkYy00NmI2LWJkYTktNThhYmMzMmNkZDQ3IiwiY2xpZW50X2lkIjoidGVzdDEifQ.RmZExfQmbPgCo2UR8HChaTxRoUzkmKB2r2h7quSAUrw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjBjMGY4NzdiLTJjZGMtNDZiNi1iZGE5LTU4YWJjMzJjZGQ0NyIsImV4cCI6MTU2OTY1MTA4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNDJmNTQyMjQtNDJiZS00ZWE2LTg5ODktOGE3ZTFhNjkzMjA5IiwiY2xpZW50X2lkIjoidGVzdDEifQ.1MLmQYoh--ExRuuf0_glHApPnTyCBi9UbZoZM3-76Ds",
    "expires_in": 3599,
    "scope": "all",
    "message": "hello world",
    "jti": "0c0f877b-2cdc-46b6-bda9-58abc32cdd47"
}

Suppose now access_ The token has expired. We use refresh_token in exchange for a new token. Send the following request using postman:

image.png

image.png

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc5MDgxMSwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMjdkOTNkMWQtNzczNi00ODQzLThjOTQtZWI1ZjdkMzIyMWJlIiwiY2xpZW50X2lkIjoidGVzdDEifQ.733DihtA3G3GkfFT82Bu-Z3FYuWHWxX8l_A0hON3XO8",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjI3ZDkzZDFkLTc3MzYtNDg0My04Yzk0LWViNWY3ZDMyMjFiZSIsImV4cCI6MTU2OTY1MTA4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNDJmNTQyMjQtNDJiZS00ZWE2LTg5ODktOGE3ZTFhNjkzMjA5IiwiY2xpZW50X2lkIjoidGVzdDEifQ.61o51OwW7twZ3tDRzEu__ho0bwwpIgwDPytkpnF00u8",
    "expires_in": 3599,
    "scope": "all",
    "message": "hello world",
    "jti": "27d93d1d-7736-4843-8c94-eb5f7d3221be"
}

Source address: https://github.com/lbshold/springboot/tree/master/Spring-Security-OAuth2-JWT



Author: lconcise
Link: https://www.jianshu.com/p/bca733826e2e
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

Tags: Spring

Posted on Sat, 06 Nov 2021 13:15:43 -0400 by igorek