[Spring Security + OAuth2 + JWT start to practice] 25. JWT replace default token

brief introduction

JWT is a stateless way. User's data can be directly parsed in token to get user's data. There are two problems in stateless: 1. Lease renewal, 2. Exit. There is no good solution to these two problems. If it is a common web project, JWT is not recommended

Replace the default token with JWT

(the default token is generated by UUID) you only need to specify the TokenStore as JwtTokenStore.

Modify TokenStoreConfig configuration class:

package com.spring.security;

import com.spring.security.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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.redis.RedisTokenStore;

@Configuration
public class TokenStoreConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    @ConditionalOnProperty(prefix = "hk.security.oauth2", name = "tokenStore", havingValue = "redis")
    public TokenStore redisTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    /**
     * Configuration when using jwt, effective by default
     *
     * @author zhailiang
     *
     */
    @Configuration
    @ConditionalOnProperty(prefix = "hk.security.oauth2", name = "tokenStore", havingValue = "jwt", matchIfMissing = true)
    public static class JwtConfig {

        @Autowired
        private SecurityProperties securityProperties;

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

        /**
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(){
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
           // Signature key
            converter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey());
            return converter;
        }

    }
}

The key has been configured in the system and transformed by itself

After TokenStoreConfig is configured in the configuration class, we specify it in the authentication server:

    //jwt mode only
    @Autowired(required = false)
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailService);
        if (jwtAccessTokenConverter != null) {
            endpoints.accessTokenConverter(jwtAccessTokenConverter);
        }
    }

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

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODQ4ODcxNzAsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNjU4MmYxOGUtMTYwZi00ZjAyLTgyNWItZTE5OTA4YjNjMjBkIiwiY2xpZW50X2lkIjoibXloazEiLCJzY29wZSI6WyJhbGwiLCJyZWFkIiwid3JpdGUiXX0.sOJkysiLDNVEh7fKgGSd_ksR3bEPSYqfOyeTnC9G718",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCIsInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI2NTgyZjE4ZS0xNjBmLTRmMDItODI1Yi1lMTk5MDhiM2MyMGQiLCJleHAiOjE1ODQ4ODcxNzAsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjJiYWRiNDRhLWMyNjMtNDcxZi04ZThkLTkzMzU4MmQxODNiNSIsImNsaWVudF9pZCI6Im15aGsxIn0.RMPMiIlU4UW-fG-5sd2k5mXMmKis9WuCzGD5WJhQ5nY",
    "expires_in": 3599,
    "scope": "all read write",
    "jti": "6582f18e-160f-4f02-825b-e19908b3c20d"
}

Copy the contents of access ABCD token to https://www.jsonwebtoken.io Website analysis:

Get resources:

I found that there is nothing back to see the / user/me method:

    @GetMapping("/user/me")
    private Object getCurrentUser(@AuthenticationPrincipal UserDetails user) {
        return user;
    }

The current mode is not UserDetails:

    @GetMapping("/user/me")
    private Object getCurrentUser(Authentication user) {
        return user;
    }

Restart the project test. As long as the jwt token does not expire, it can be used:

{
    "authorities": [
        {
            "authority": "admin"
        }
    ],
    "details": {
        "remoteAddress": "127.0.0.1",
        "sessionId": null,
        "tokenValue": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODQ4ODgxOTksInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiODNmMzEwMjUtMGVlZC00YmYxLWJkNDktZmNiN2YyZjVkMjIyIiwiY2xpZW50X2lkIjoibXloazEiLCJzY29wZSI6WyJhbGwiLCJyZWFkIiwid3JpdGUiXX0._NWlXGYavY7ntdR0vTSxVTwrYZrtHZZidsU0v8F8kow",
        "tokenType": "bearer",
        "decodedDetails": null
    },
    "authenticated": true,
    "userAuthentication": {
        "authorities": [
            {
                "authority": "admin"
            }
        ],
        "details": null,
        "authenticated": true,
        "principal": "admin",
        "credentials": "N/A",
        "name": "admin"
    },
    "principal": "admin",
    "oauth2Request": {
        "clientId": "myhk1",
        "scope": [
            "all",
            "read",
            "write"
        ],
        "requestParameters": {
            "client_id": "myhk1"
        },
        "resourceIds": [],
        "authorities": [],
        "approved": true,
        "refresh": false,
        "redirectUri": null,
        "responseTypes": [],
        "extensions": {},
        "grantType": null,
        "refreshTokenRequest": null
    },
    "credentials": "",
    "clientOnly": false,
    "name": "admin"
}

Expanding JWT

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

package com.spring.security.jwt;

import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

import java.util.HashMap;
import java.util.Map;

/**
 * JWT Enhancer
 */
public class HkJWTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        //Enhanced content
        info.put("company", "baidu.com");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

We added "company" and "Baidu. Com" information to Token. Then register the Bean in TokenStoreConfig:

        /**
         * @return
         */
        @Bean
        @ConditionalOnBean(TokenEnhancer.class)
        public TokenEnhancer jwtTokenEnhancer(){
            return new HkJWTokenEnhancer();
        }

Finally, configure the enhancer in the authentication server:

    //jwt mode only
    @Autowired(required = false)
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired(required = false)
    private TokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailService);
        if (jwtAccessTokenConverter != null && jwtTokenEnhancer!=null) {
            //Enhancer
            TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> enhancers = new ArrayList<>();
            enhancers.add(jwtTokenEnhancer);
            enhancers.add(jwtAccessTokenConverter);
            enhancerChain.setTokenEnhancers(enhancers);

            //
            endpoints.tokenEnhancer(enhancerChain)
                    .accessTokenConverter(jwtAccessTokenConverter);
        }
    }

Restart the project, get the token again, and the system returns:

It can be seen that there are more company information we added in the returned JSON content. In addition, copy the access_token to https://www.jsonwebtoken.io website for parsing. The content is as follows:

{
 "user_name": "admin",
 "scope": [
  "all",
  "read",
  "write"
 ],
 "company": "baidu.com",
 "exp": 1584889256,
 "authorities": [
  "admin"
 ],
 "jti": "0703a99a-482f-41ea-b0a9-ac4853bfe0c4",
 "client_id": "myhk1"
}

The parsed JWT also contains the company 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>

Modify / user/me:

    @GetMapping("/user/me")
    private Object getCurrentUser(Authentication user, HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        String token = StringUtils.substringAfter(header, "bearer ");

        return Jwts.parser().setSigningKey(securityProperties.getOauth2().getJwtSigningKey().getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
    }

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

refresh token

After the token expires, we can use refresh_token to exchange a new available token from the system. For the system to return the refresh_token, you need to add the following configuration to the user-defined configuration of the authentication server:

//Authorization mode
.authorizedGrantTypes("password", "authorization_code", "refresh_token");

Let's say that access_token is expired now. We use refresh_token in exchange for a new token. Use postman to send the following request:

Note: JWT token has no exit function.

Tags: Programming Java Spring Redis JSON

Posted on Sun, 22 Mar 2020 09:58:59 -0700 by pmt2k