Spring Cloud OAuth2 enables user authentication and single sign on

OAuth 2 has four authorization modes: authorization code mode, implicit mode, resource owner password credentials mode and client credentials mode. Please refer to this chapter for specific OAuth 2. ( http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)

In this paper, we will use authorization code mode and password mode to realize user authentication and authorization management.

OAuth2 is actually a network standard about authorization. It formulates the design idea and operation process. Using this standard, we can actually realize the authentication process of OAuth2 by ourselves. Spring Cloud starter OAuth2 to be introduced today is actually a concrete implementation encapsulated by Spring Cloud according to OAuth2 standard and spring security.

Under what circumstances does OAuth2 need to be used

First of all, what we are most familiar with is what almost everyone has used, such as logging in with wechat, QQ, microblog, Google account, github authorization, etc. These are typical oauth2 use scenarios. Suppose we build our own service platform. If we do not use oauth2 login, we need the user to complete the registration first, and then log in with the account password of the registration number or the mobile phone verification code. After using OAuth2, I believe many people have used or even developed official account service and small program. When we enter the web page and small program interface, we need not register for the first time. We can login directly with WeChat, which greatly improves the efficiency of using. Because everyone has wechat, you can use third-party services immediately with wechat. This experience is not too good. For our service, we do not need to store the user's password, but only the unique ID and user information returned by the authentication platform.

The above uses the authorization code mode of OAuth2 and uses the authoritative platform of a third party to realize user identity authentication. Of course, if there are many services in your company, you can specially extract a certification center, which acts as the authoritative certification platform mentioned above. All services should be certified in this certification center.

In this way, did you find that this is actually a single sign on function. This is another use scenario. For multi service platforms, OAuth2 can be used to realize single sign on of services. After only one login, you can walk freely through multiple services. Of course, it is only limited to the services and interfaces within the scope of authorization.

Realize unified authentication function

This article first introduces the single sign on realized by password mode, and then continues to talk about authorization code mode in the next article.

Today, when microservices are rampant, who dares to say that they don't have many microservices. Microservices not only reduce the coupling between services, but also increase the complexity of the system in some aspects, such as user authentication. Suppose we implement an e-commerce platform here. What users see is an APP or a web site. In fact, it is composed of multiple independent services, such as user services, order services, product services, etc. As long as the user enters the user name and password for the first time, after logging in, the user can access various pages arbitrarily for a period of time, such as product list page, my order page, my attention page, etc.

We can imagine that we can naturally think of what credentials we must carry when requesting each service and interface, and then each service knows which user is requesting the interface. Otherwise, there must be a problem. In fact, the credentials in it are simply a Token, a Token identifying the user's identity.

System architecture description

Authentication center: OAuth2 auth server. OAuth2 mainly implements the client. The generation, refresh and verification of tokens are completed in the authentication center.

Order service: oauth2 client order server, one of the microservices, will go to the Certification Center for verification after receiving the request.

User service: oauth2 client user server, the second of micro services, will go to the authentication center for authentication after receiving the request.

Client: for example, APP terminal, web terminal, etc

The above figure describes the request process between the client using OAuth2 and the microservice. The general process is that the client exchanges the user name and password to the authentication server for a token and returns it to the client. The client takes the token to each micro service request data interface. Generally, this token is placed in the header. When the microservice receives the request, it must first take the token to authenticate the server and check the legitimacy of the token. If it is legal, it will dynamically return data according to the user's role and authority.

Create and configure authentication server

The most configured is the authentication server. Verifying the account and password, storing the token, checking the token and refreshing the token are all the work of the authentication server.

1. Introduce the required maven package

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

spring-cloud-starter-oauth2 includes spring-cloud-starter-security, so there is no need to introduce it separately. Redis package is introduced because a method of storing token s with redis will be introduced below.

2. Configure the application yml

Set the basic project configuration and add the redis configuration, which will be used later.

spring:
  application:
    name: auth-server
  redis:
    database: 2
    host: localhost
    port: 32768
    password: 1qaz@WSX
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 100ms

server:
  port: 6001

management:
  endpoint:
    health:
      enabled: true

3. spring security basic configuration

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    /**
     * Allow anonymous access to all interfaces, mainly oauth interface
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**").permitAll();
    }
}

Decorated with the @ EnableWebSecurity annotation and inherited from the WebSecurityConfigurerAdapter class.

The focus of this class is to declare two beans, PasswordEncoder and AuthenticationManager. I'll use it later. BCryptPasswordEncoder is a password encryption tool class, which can realize irreversible encryption. AuthenticationManager is an authorization management Bean that must be specified to realize the password mode of OAuth2.

4. Implement UserDetailsService

If you have used Security before, you must be familiar with this class. It is not only a way to realize user authentication, but also the simplest and most convenient one. In addition, there is a way to combine Security with authentication provider. Let's talk about it when we have the opportunity to talk about Security.

The core of UserDetailsService is the {loadUserByUsername method, which receives a string parameter, that is, the passed user name, and returns a} UserDetails object.

@Slf4j
@Component(value = "kiteUserDetailsService")
public class KiteUserDetailsService implements UserDetailsService {


    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("usernameis:" + username);
        // Query database operation
        if(!username.equals("admin")){
            throw new UsernameNotFoundException("the user is not found");
        }else{
            // User roles should also be obtained in the database
            String role = "ROLE_ADMIN";
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(role));
            // The online environment should query the database through the user name to obtain the encrypted password
            String password = passwordEncoder.encode("123456");
            return new org.springframework.security.core.userdetails.User(username,password, authorities);
        }
    }
}

In order to demonstrate, the user name, password and role are written in the code. In the formal environment, the encrypted password and role should be found from the database or other places according to the user name. The account admin and password 123456 will be used later in exchange for a token. And set the "ROLE_ADMIN" role for this user.

5. OAuth2 configuration file

Create a configuration file that inherits from authorization server configureradapter

@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    public PasswordEncoder passwordEncoder;

    @Autowired
    public UserDetailsService kiteUserDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore redisTokenStore;

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        /**
         * redis token mode
         */
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(kiteUserDetailsService)
                .tokenStore(redisTokenStore);

    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("order-client")
                .secret(passwordEncoder.encode("order-secret-8888"))
                .authorizedGrantTypes("refresh_token", "authorization_code", "password")
                .accessTokenValiditySeconds(3600)
                .scopes("all")
                .and()
                .withClient("user-client")
                .secret(passwordEncoder.encode("user-secret-8888"))
                .authorizedGrantTypes("refresh_token", "authorization_code", "password")
                .accessTokenValiditySeconds(3600)
                .scopes("all");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
        security.checkTokenAccess("isAuthenticated()");
        security.tokenKeyAccess("isAuthenticated()");
    }
}

There are three overrides to the configure method.

Overriding the AuthorizationServerEndpointsConfigurer parameter

endpoints.authenticationManager(authenticationManager)
                .userDetailsService(kiteUserDetailsService)
                .tokenStore(redisTokenStore);

authenticationManage() calls this method to support password mode.

userDetailsService() sets the user authentication service.

tokenStore() specifies how token s are stored.

redisTokenStore Bean is defined as follows:

@Configuration
public class RedisTokenStoreConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore (){
        return new RedisTokenStore(redisConnectionFactory);
    }
}

Overriding the ClientDetailsServiceConfigurer parameter, where the constraints of each end are defined. include

ClientId and client secret: these two parameters correspond to the cleint ID and client secret defined by the requester

Authorized grant types can include one or more of the following settings:

  • authorization_code: authorization code type.
  • Implicit: implicit authorization type.
  • Password: the password type of the resource owner (i.e. user).
  • client_credentials: type of client credentials (client ID and Key).
  • refresh_token: obtain a new token through the refresh token obtained by the above authorization.

accessTokenValiditySeconds: the validity period of the token

scopes: used to restrict the access permission of the client. The scope parameter will be brought when exchanging token s. Tokens can only be exchanged normally if they are within the scope definition.

The above code is stored in inMemory. Saving the configuration in memory is equivalent to hard coding. In the formal environment, the method is to persist to the database, such as mysql.

The specific methods are as follows:

  1. Add tables to the database and insert data
create table oauth_client_details (
    client_id VARCHAR(256) PRIMARY KEY,
    resource_ids VARCHAR(256),
    client_secret VARCHAR(256),
    scope VARCHAR(256),
    authorized_grant_types VARCHAR(256),
    web_server_redirect_uri VARCHAR(256),
    authorities VARCHAR(256),
    access_token_validity INTEGER,
    refresh_token_validity INTEGER,
    additional_information VARCHAR(4096),
    autoapprove VARCHAR(256)
);
INSERT INTO oauth_client_details
    (client_id, client_secret, scope, authorized_grant_types,
    web_server_redirect_uri, authorities, access_token_validity,
    refresh_token_validity, additional_information, autoapprove)
VALUES
    ('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all',
    'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);

INSERT INTO oauth_client_details
    (client_id, client_secret, scope, authorized_grant_types,
    web_server_redirect_uri, authorities, access_token_validity,
    refresh_token_validity, additional_information, autoapprove)
VALUES
    ('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all',
    'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);

Note: client_ The secret field cannot be the original value of secret directly. It needs to be encrypted. Because BCryptPasswordEncoder is used, the final inserted value should be BCryptPasswordEncoder Value after encode().

  1. Then in the configuration file application Add configuration about database in YML
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_cloud?characterEncoding=UTF-8&useSSL=false
    username: root
    password: password
    hikari:
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      maximum-pool-size: 9   

After Spring Boot 2.0, hikari is used as the database connection pool by default. If you use other connection pools, you need to import related packages and add configurations accordingly.

  1. Add DataSource injection in oauth2config configuration class
@Autowired
private DataSource dataSource;
  1. Modify the public void configure(ClientDetailsServiceConfigurer clients) override method as follows:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
	jcsb.passwordEncoder(passwordEncoder);
}

There is also an overridden method, {public void configure(AuthorizationServerSecurityConfigurer security), which restricts the client's access to the authentication interface.

security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
security.tokenKeyAccess("isAuthenticated()");

The first line of code allows the client to access the OAuth2 authorization interface, otherwise the request token will return 401.

The second and third lines allow authorized users to access the checkToken interface and obtain the token interface respectively.

After completion, start the project. If you use IDEA, you will see oauth2 relevant RESTful interfaces in the Mapping window below.

There are mainly the following:

POST /oauth/authorize  Authorization code mode authentication authorization interface
GET/POST /oauth/token  obtain token Interface
POST  /oauth/check_token  inspect token Legitimacy interface

Create user client project

The authentication server is created above, and now we start to create a client corresponding to the business-related microservices in our system. We assume that this microservice project manages user related data, so it is called user client.

1. Reference related maven packages

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2,application.yml profile

spring:
  application:
    name: client-user
  redis:
    database: 2
    host: localhost
    port: 32768
    password: 1qaz@WSX
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 100ms
server:
  port: 6101
  servlet:
    context-path: /client-user

security:
  oauth2:
    client:
      client-id: user-client
      client-secret: user-secret-8888
      user-authorization-uri: http://localhost:6001/oauth/authorize
      access-token-uri: http://localhost:6001/oauth/token
    resource:
      id: user-client
      user-info-uri: user-info
    authorization:
      check-token-access: http://localhost:6001/oauth/check_token

The above is the general configuration information and redis configuration, focusing on the security configuration below. If you don't pay attention to the configuration here, 401 or other problems will occur.

Client ID and client secret should be consistent with the configuration in the authentication service. If inMemory or jdbc is used.

User authorization URI is required for authorization code authentication, which will be discussed in the next article.

Access token URI is the interface to obtain the token required by the password mode.

authorization. Check token access is also key information. When the server receives a request from the client, it needs to take the token in the request to the authentication server for token verification, which is the interface of the request

3. Resource profile

In the concept of OAuth2, all interfaces are called resources, and the permissions of interfaces are the permissions of resources. Therefore, Spring Security OAuth2 provides the annotation @ EnableResourceServer on resources, which is similar to @ EnableWebSecurity.

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Value("${security.oauth2.client.client-secret}")
    private String secret;

    @Value("${security.oauth2.authorization.check-token-access}")
    private String checkTokenEndpointUrl;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore (){
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Bean
    public RemoteTokenServices tokenService() {
        RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setClientId(clientId);
        tokenService.setClientSecret(secret);
        tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
        return tokenService;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenServices(tokenService());
    }
}

Because redis is used as token storage, a Bean called tokenService needs to be specially configured to verify tokens.

4. Finally, add a RESTful interface

@Slf4j
@RestController
public class UserController {

    @GetMapping(value = "get")
    //@PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public Object get(Authentication authentication){
        //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        authentication.getCredentials();
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
        String token = details.getTokenValue();
        return token;
    }
}

A RESTful method, only when the access user has a role_ Can only be accessed with admin permission, otherwise 401 unauthorized is returned.

Through the Authentication parameter or securitycontextholder getContext(). Getauthentication () can get the authorization information for viewing.

Test certification function

1. Start the authentication server, and the start port is 6001

2. Start the user service client. The start port is 6101

3. Request authentication server to obtain token

I use the REST Client to make an access request. The request format is as follows:

POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

Suppose we use grant on a web side_ Type is password, indicating that this is a password mode using OAuth2.

username=admin and password=123456 are equivalent to the user name and password entered in the web login interface. We have fixed the user name as admin and password as 123456 in the authentication server configuration, and in the online environment, it should be obtained by querying the database.

scope=all is permission related. The scope is specified as all in OAuthConfig of the authentication service.

Authorization should be added to the request header in the format of Basic space base64(clientId:clientSecret). The client ID of the microservice client is user client, and the client secret is user-secret-8888. Connect the two values through a colon, and use base64 encoding (user client: user secret-8888). The value after it is dxnlci1jbgllbnq6dxnlci1zwnyzxqtodg4oa = =, which can be https://www.sojson.com/base64.html Online code acquisition.

After running the request, if the parameters are correct, the returned content obtained is as follows, which is in json format

{
  "access_token": "9f958300-5005-46ea-9061-323c9e6c7a4d",
  "token_type": "bearer",
  "refresh_token": "0f5871f5-98f1-405e-848e-80f641bab72e",
  "expires_in": 3599,
  "scope": "all"
}

access_token: it is the token that needs to be carried in the subsequent request, and it is also the main purpose of this request
token_type: bearer, which is the most commonly used form of access token
refresh_token: you can then use this value to exchange for a new token without entering the account password
expires_in: expiration time of token (seconds)

4. Request the resource interface with the obtained token

We define an interface in the user client http://localhost:6101/client-user/get, now take the information obtained in the previous step token to request this interface.

GET http://localhost:6101/client-user/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer ce334918-e666-455a-8ecd-8bd680415d84

Similarly, request header Authorization is required. The format is bear + space + token. Normally, the token will be returned as it is according to the logic of the interface.

5. After the token expires, use refresh_token for access_token

Access is usually set_ The expiration time of token is less than refresh_ The expiration time of the token in order to access_ After the token expires, you can obtain new access without logging in again_ token.

### Exchange access_token
POST http://localhost:6001/oauth/token?grant_type=refresh_token&refresh_token=706dac10-d48e-4795-8379-efe8307a2282
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

grant_type is set to refresh_token.

refresh_ The token is set to the refresh returned when the token is requested_ The value of the token.

Authorization is added to the request header, and the format is still Basic + space + Base64 (client ID: client secret)

After the request is successful, the same data format as the request token will be returned.

Replace redisToken with JWT

The above token storage uses the redis scheme. Spring Security OAuth2 also provides jdbc and JWT support. jdbc will not be considered for the time being. Now let's introduce how to use JWT to store tokens.

With JWT, you don't need to store the token to the server. JWT has its own special encryption method, which can effectively prevent data from being tampered with. As long as you don't put key information such as user password into JWT, you can ensure security.

Authentication server transformation

Remove the redis configuration first.

Add JwtConfig configuration class

@Configuration
public class JwtTokenConfig {

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("dev");
        return accessTokenConverter;
    }
}

JwtAccessTokenConverter is used for JWT data conversion because JWT has its own unique data format. If you haven't learned about JWT, you can search and learn about it first.

Change OAuthConfig configuration class

@Autowired
private TokenStore jwtTokenStore;

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        /**
         * Normal jwt mode
         */
         endpoints.tokenStore(jwtTokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .userDetailsService(kiteUserDetailsService)
                /**
                 * Support password mode
                 */
                .authenticationManager(authenticationManager);
}

Inject JWT related beans, and then modify the configure (final authorizationserverendpoints configurer endpoints) method to JWT storage mode.

Transform user client

Modify application YML profile

security:
  oauth2:
    client:
      client-id: user-client
      client-secret: user-secret-8888
      user-authorization-uri: http://localhost:6001/oauth/authorize
      access-token-uri: http://localhost:6001/oauth/token
    resource:
      jwt:
        key-uri: http://localhost:6001/oauth/token_key
        key-value: dev

Note that the SigningKey set by the authentication server JwtAccessTokenConverter must be the same as the key value in the configuration file, otherwise JWT cannot be decoded normally, resulting in failure of verification.

Configuration of ResourceServerConfig class

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();

        accessTokenConverter.setSigningKey("dev");
        accessTokenConverter.setVerifierKey("dev");
        return accessTokenConverter;
    }

    @Autowired
    private TokenStore jwtTokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(jwtTokenStore);
    }
}

Run the request to request the token interface

POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

The returned results are as follows:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJleHAiOjE1NzE3NzU4OTQsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiZjdkMjg4NDUtMmU2ZC00ZmRjLTg1OGYtMWNiY2RlNzI1ZmMyIiwiY2xpZW50X2lkIjoidXNlci1jbGllbnQifQ.vk_msYtbrAr93h5sK4wy6EC2_wRD_cD_UBS8O6eRziw",
  "expires_in": 3599,
  "scope": "all",
  "jti": "8cca29af-ea77-4fe6-9fe1-327415dcd21d"
}

We have seen that the returned token is in JWT format. Go to the JWT online decoding website https://jwt.io/ Or http://jwt.calebb.net/ Decode the token and have a look

See, user_name,client_id and other information are included.

Request the user client interface with the returned token

GET http://localhost:6101/client-user/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM

Enhanced JWT

What if I want to add additional fields (such as other user information) to JWT? Of course. spring security oauth2 provides a token enhancer. In fact, not only JWT, but also RedisToken.

Declare an enhancer

public class JWTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("jwt-ext", "JWT Extended information");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

The user name and other information can be obtained through oaut2authentication. Through these, we can query the database or cache to obtain more information, which can be added as JWT extension information.

OAuthConfig configuration class modification

Injection intensifier

@Autowired
private TokenEnhancer jwtTokenEnhancer;

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

Modify the configure(final AuthorizationServerEndpointsConfigurer endpoints) method

@Override
public void configure( final AuthorizationServerEndpointsConfigurer endpoints ) throws Exception{
	/**
	 * jwt Enhancement mode
	 */
	TokenEnhancerChain	enhancerChain	= new TokenEnhancerChain();
	List<TokenEnhancer>	enhancerList	= new ArrayList<>();
	enhancerList.add( jwtTokenEnhancer );
	enhancerList.add( jwtAccessTokenConverter );
	enhancerChain.setTokenEnhancers( enhancerList );
	endpoints.tokenStore( jwtTokenStore )
	.userDetailsService( kiteUserDetailsService )
	/**
	 * Support password mode
	 */
	.authenticationManager( authenticationManager )
	.tokenEnhancer( enhancerChain )
	.accessTokenConverter( jwtAccessTokenConverter );
}

Request the token again, and multiple JWT ext fields just added are returned in the content

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImE0NTUxZDllLWI3ZWQtNDc1OS1iMmYxLWYwYjliMjFjYTQyYyIsImV4cCI6MTU3MTc3NzU3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJmNTI3ODJlOS0wOGRjLTQ2NGUtYmJhYy03OTMwNzYwYmZiZjciLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.UQMf140CG8U0eWh08nGlctpIye9iJ7p2i6NYHkGAwhY",
  "expires_in": 3599,
  "scope": "all",
  "jwt-ext": "JWT Extended information",
  "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c"
}

User client parses JWT data

If we add additional information to JWT, we may use it. After receiving the token in JWT format, the user client needs to parse the JWT.

Introduce JWT package

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

Add a RESTful interface to parse JWT

@GetMapping(value = "jwt")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object jwtParser(Authentication authentication){
    authentication.getCredentials();
    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
    String jwtToken = details.getTokenValue();
    Claims claims = Jwts.parser()
                .setSigningKey("dev".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(jwtToken)
                .getBody();
    return claims;
}

Also note that the signature settings should be the same as the authentication server.

Use the token in the previous step to request the above interface

### Parse jwt
GET http://localhost:6101/client-user/jwt
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ

The returned contents are as follows:

{
  "user_name": "admin",
  "jwt-ext": "JWT Extended information",
  "scope": [
    "all"
  ],
  "exp": 1571745178,
  "authorities": [
    "ROLE_ADMIN"
  ],
  "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c",
  "client_id": "user-client"
}

The above is the complete process of password mode. The source code is put on github. You can have a look if you need it.

Source address

Keywords: Spring Boot

Added by vishakh369 on Mon, 10 Jan 2022 15:57:07 +0200