Spring Cloud Security: Oauth2 used in conjunction with JWT

Keywords: Java Redis Spring SpringBoot github

SpringBoot e-commerce project mall (20k+star) address: https://github.com/macrozheng/mall

abstract

Spring Cloud Security provides a series of solutions for building secure SpringBoot applications, and with Oauth2, more functions can be achieved, such as using JWT tokens to store information and refreshing tokens. This article will give a detailed description of how it works with JWT.

Introduction to JWT

JWT is the abbreviation of JSON WEB TOKEN. It is a JSON object that can be safely transferred based on RFC 7519 standard. Because of the use of digital signatures, it is trusted and secure.

Composition of JWT

  • Format of JWT token: header.payload.signature;
  • Generation algorithm used in header to store signatures;
{
  "alg": "HS256",
  "typ": "JWT"
}
  • payload is used to store data such as expiration time, user name, user rights, etc.
{
  "exp": 1572682831,
  "user_name": "macro",
  "authorities": [
    "admin"
  ],
  "jti": "c1a0645a-28b5-4468-b4c7-9623131853af",
  "client_id": "admin",
  "scope": [
    "all"
  ]
}
  • Signatures are signature s generated with headers and payloads, and once the headers and payloads are tampered with, validation will fail.

JWT instance

  • This is a JWT string:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzI2ODI4MzEsInVzZXJfbmFtZSI6Im1hY3JvIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiYzFhMDY0NWEtMjhiNS00NDY4LWI0YzctOTYyMzEzMTg1M2FmIiwiY2xpZW50X2lkIjoiYWRtaW4iLCJzY29wZSI6WyJhbGwiXX0.x4i6sRN49R6JSjd5hd1Fr2DdEMBsYdC4KB6Uw1huXPg

Create oauth2-jwt-server module

This module is just an extension of the oauth2-server module, which can be copied directly under the extension.

How tokens are stored in oauth2

In the previous section, we stored tokens in memory, so deploying multiple services would cause the problem of not being able to use tokens.
There are two ways to store tokens in Spring Cloud Security to solve this problem, one using Redis and the other using JWT.

Store tokens using Redis

  • Add Redis-related dependencies to pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • Add redis-related configurations in application.yml:
spring:
  redis: #redis related configuration
    password: 123456 #Set with Password
  • Add a configuration to store tokens in Redis:
/**
 * Store token configuration using redis
 * Created by macro on 2019/10/8.
 */
@Configuration
public class RedisTokenStoreConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore (){
        return new RedisTokenStore(redisConnectionFactory);
    }
}
  • Specify the storage policy for tokens in the authentication server configuration as Redis:
/**
 * Authentication Server Configuration
 * Created by macro on 2019/9/30.
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserService userService;

    @Autowired
    @Qualifier("redisTokenStore")
    private TokenStore tokenStore;

    /**
     * Configuration required to use password mode
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                .tokenStore(tokenStore);//Configure Token Storage Policy
    }
    
    //Omit Code...
}
  • Use password mode to get tokens after running the project, accessing the following addresses: http://localhost:9401/oauth/token

  • A token acquisition operation reveals that the token has been stored in Redis.

Store tokens using JWT

  • Add a configuration that uses JWT to store tokens:
/**
 * Store configuration of token using Jwt
 * Created by macro on 2019/10/8.
 */
@Configuration
public class JwtTokenStoreConfig {

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("test_key");//Configure the secret key used by JWT
        return accessTokenConverter;
    }
}
  • Specify the storage policy for tokens in the authentication server configuration as JWT:
/**
 * Authentication Server Configuration
 * Created by macro on 2019/9/30.
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserService userService;

    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;

    /**
     * Configuration required to use password mode
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                .tokenStore(tokenStore) //Configure Token Storage Policy
                .accessTokenConverter(jwtAccessTokenConverter);
    }
    
    //Omit Code...
}
  • Use password mode to get tokens after running the project, accessing the following addresses: http://localhost:9401/oauth/token

  • Discover that the acquired token has become a JWT token and get access_token https://jwt.io/ You can get the content by parsing it on the website.
{
  "exp": 1572682831,
  "user_name": "macro",
  "authorities": [
    "admin"
  ],
  "jti": "c1a0645a-28b5-4468-b4c7-9623131853af",
  "client_id": "admin",
  "scope": [
    "all"
  ]
}

Extend content stored in JWT

Sometimes we need to extend what's stored in JWT, here we'll extend the data in JWT with a key of enhance and a value of enhance info.

  • Inherit TokenEnhancer to implement a JWT content enhancer:
/**
 * Jwt Content Enforcer
 * Created by macro on 2019/10/8.
 */
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("enhance", "enhance info");
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}
  • Create a JwtTokenEnhancer instance:
/**
 * Store configuration of token using Jwt
 * Created by macro on 2019/10/8.
 */
@Configuration
public class JwtTokenStoreConfig {
    
    //Omit Code...

    @Bean
    public JwtTokenEnhancer jwtTokenEnhancer() {
        return new JwtTokenEnhancer();
    }
}
  • Configure the JWT content enhancer in the authentication server configuration:
/**
 * Authentication Server Configuration
 * Created by macro on 2019/9/30.
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserService userService;

    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;

    /**
     * Configuration required to use password mode
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer); //Configure JWT's Content Enhancer
        delegates.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(delegates);
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                .tokenStore(tokenStore) //Configure Token Storage Policy
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(enhancerChain);
    }

    //Omit Code...
}
  • After running the project, use password mode to get the token, then parse the token and find that it already contains the extended content.
{
  "user_name": "macro",
  "scope": [
    "all"
  ],
  "exp": 1572683821,
  "authorities": [
    "admin"
  ],
  "jti": "1ed1b0d8-f4ea-45a7-8375-211001a51a9e",
  "client_id": "admin",
  "enhance": "enhance info"
}

Parsing content in JWT in Java

If we need to get information in JWT, we can use a toolkit called jjwt.

  • Add dependencies to pom.xml:
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
  • Modify the UserController class to use the jjwt tool class to parse the JWT content stored in the Authorization header.
/**
 * Created by macro on 2019/9/30.
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        String token = StrUtil.subAfter(header, "bearer ", false);
        return Jwts.parser()
                .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody();
    }

}
  • Place the token in the Authorization header and access the following address for information: http://localhost:9401/user/getCurrentUser

refresh token

When using oauth2 in Spring Cloud Security, if the token fails, you can use the refresh token to get access_token again through refresh_token's authorization mode.

  • Simply modify the authentication server's configuration and add refresh_token's authorization mode.
/**
 * Authentication Server Configuration
 * Created by macro on 2019/9/30.
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("admin")
                .secret(passwordEncoder.encode("admin123456"))
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(864000)
                .redirectUris("http://www.baidu.com")
                .autoApprove(true) //Automatic Authorization Configuration
                .scopes("all")
                .authorizedGrantTypes("authorization_code","password","refresh_token"); //Add Authorization Mode
    }
}
  • Use the refresh token mode to get a new token and access the following addresses: http://localhost:9401/oauth/token

Modules used

springcloud-learning
└── oauth2-jwt-server -- Use jwt Of oauth2 Certification Test Service

Project Source Address

https://github.com/macrozheng/springcloud-learning

Public Number

mall project In the full series of learning tutorials, focus on the first time public number is available.

Posted by adrianuk29 on Thu, 21 Nov 2019 18:23:31 -0800