Introduction: This paper is the second part of the series “Design and Implementation of Authentication and API Permission Control in micro-service Architecture”, and focuses on the specific implementation of user identity authentication and token issuance. This article is a lengthy analysis of most of the code involved, and you can save it for reading at your leisure. Please subscribe to this series.

1. System Overview

In the last article, the design and implementation of authentication and API permission control in microservice architecture (I) introduced the background, technical research and final selection of the project, and presented the execution results of the endpoint finally realized. The system architecture is mentioned, but detailed flow charts are not listed. In the author’s application scenario, Auth system is combined with gateway. Configure endpoint information on the outbound gateway, such as logging in to the system to apply for token authorization and verifying check_token.

The following figure shows the flowchart of the combination of gateway and Auth system. The specific implementation details of gateway system will be introduced in another article later. (In the drawing of the flow chart here, the author uses the minimalist language description, the students lightly spray 😆!)

login

The figure above shows the simple process of system login. The details are omitted. The validity check of user information is actually calling user system. The general process is as follows: after the client request reaches the gateway, it logs in to the endpoint according to the request identified by the gateway and forwards it to the Auth system to verify the user’s information.

Another aspect is validation for general requests. Some public interfaces that do not require permissions are configured at the gateway. After a request reaches the gateway, the gateway will directly pass the request if it matches the path. If the request needs to be verified, the relevant verification information of the request will be intercepted, as well as the context information required for API permission verification (the author’s project carries out permission pre-verification for some operations, which will be described in the next chapter), and the Auth system will be called. After successful verification, routing and forwarding will be carried out.

gw

This article focuses on the user identity authentication and token issuance mentioned in the first article. There are two main aspects to this:

  • Authentication of user validity
  • The authorization token is obtained. Procedure

2. Configure and class diagram

2.1 Main Configuration of AuthorizationServer

Configurations for AuthorizationServer and ResourceServer were listed in the previous article. AuthorizationServer mainly inherited AuthorizationServerConfigurerAdapter, overwrite the three methods of implementing an interface:

    / / corresponds to the configuration AuthorizationServer security authentication information, create ClientCredentialsTokenEndpointFilter core filters
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}// Configure client information for OAuth2
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}TokenStore, TokenGranter, OAuth2RequestFactory
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}Copy the code

2.2 the mainAuthenticationClass class diagram

auth

Main Authentication methods Authenticate (Authentication Authentication) In the interface AuthenticationManager, its implementation class is ProviderManager, As you can see from the figure above, ProviderManager in turn relies on the AuthenticationProvider interface, which defines a List

global variable. The author here implements this interface implementation class CustomAuthenticationProvider. A custom provider, and configured in the GlobalAuthenticationConfigurerAdapter change custom validation provider, overwrite the configure () method.

@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {

    @Autowired
    CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider);// Use custom AuthenticationProvider}}Copy the code

AuthenticationManagerBuilder is used to create the AuthenticationManager, allowing custom AuthenticationProvider offers a variety of ways, such as LDAP, based on JDBC and so on.

3. Authenticate and authorize the token

The following describes the main classes and interfaces for authentication and authorization tokens.

3.1 Built-in EndpointsTokenEndpoint

Spring-security-oauth2 provides a jar package with built-in base endpoints associated with tokens. In this article, authentication and authorization tokens are related to /oauth/token, which deals with the interface class TokenEndpoint. Let’s take a look at the specific processing of the authentication and authorization token process.

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {...@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map
       
         parameters)
       ,> throws HttpRequestMethodNotSupportedException {
    // Verify client information first
        if(! (principalinstanceof Authentication)) {
            throw new InsufficientAuthenticationException(
                    "There is no client authentication. Try adding an appropriate authentication filter.");
        }

        String clientId = getClientId(principal);
        // Load the client information according to the clientId in the requestClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); .// Verify the scope of the field
        if(authenticatedClient ! =null) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }
        // Authorization mode cannot be empty
        if(! StringUtils.hasText(tokenRequest.getGrantType())) {throw new InvalidRequestException("Missing grant type");
        }
        // Token endpoint does not support Implicit mode
        if (tokenRequest.getGrantType().equals("implicit")) {
            throw new InvalidGrantException("Implicit grant type not supported from token endpoint"); }...// Enter the CompositeTokenGranter, match the authorization mode, and then perform password mode authentication and token provisioning
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }

        returngetResponse(token); }... }Copy the code

client

The code is commented above for readers who are interested in it. The main process of interface processing is to check whether the authentication information is valid, throw an exception directly, and then process the GrantType of the request. According to the GrantType, authentication in password mode and token provisioning are carried out. Here’s the code for getTokenGranter() :

public class CompositeTokenGranter implements TokenGranter {

    // Set of granttypes
    private final List<TokenGranter> tokenGranters;

    public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
        this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
    }

    // Go through the list and process the matching grantType
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if(grant! =null) {
                returngrant; }}return null; }... }Copy the code

This request is in password mode, and then enters its GrantType processing flow. Below is the grant() method.

    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        if (!this.grantType.equals(grantType)) {
            return null;
        }

        String clientId = tokenRequest.getClientId();
        // Load ClientDetails corresponding to clientId for further verification
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        // Verify that clientId has the grantType mode again, safe
        validateGrantType(grantType, client);
        / / access token
        return getAccessToken(client, tokenRequest);

    }

    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    // Perform authentication before entering the creation of the token
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
    // Authentication
        OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, null);
    }Copy the code

The above code is the implementation details of the Grant () method. After GrantType matches its corresponding grant(), basic authentication is performed to ensure security, and then the main process, authentication and token issuance, is described in the following section.

3.2 User-defined Authentication classesCustomAuthenticationProvider

The implementation of defined CustomAuthenticationProvider verified method. The concrete implementation is shown below.

    // The main custom validation method
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        Map data = (Map) authentication.getDetails();
        String clientId = (String) data.get("client");
        Assert.hasText(clientId,"clientId must have value" );
        String type = (String) data.get("type");
        // Verify user information by invoking the user service
        Map map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
            // Check the returned information. If the information is incorrect, an exception will be thrown, and authorization fails
        String userId = (String) map.get("userId");
        if (StringUtils.isBlank(userId)) {
            String errorCode = (String) map.get("code");
            throw new BadCredentialsException(errorCode);
        }
        CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId, clientId);
        return new CustomAuthenticationToken(customUserDetails);
    }
    // Construct a CustomUserDetails, simple, omitted
    private CustomUserDetails buildCustomUserDetails(String username, String password, String userId, String clientId) {}// Create a map requesting userService
    private Map<String, String> getUserServicePostObject(String username, String password, String type) {}Copy the code

Authenticate () finally returned to construct custom CustomAuthenticationToken, in CustomAuthenticationToken, the Boolean authenticated is set to true, the user information to verify success. The CustomUserDetails parameter is passed in as the payload, which is related to token generation.

/ / AbstractAuthenticationToken inherited abstract class
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
    private CustomUserDetails userDetails;
    public CustomAuthenticationToken(CustomUserDetails userDetails) {
        super(null);
        this.userDetails = userDetails;
        super.setAuthenticated(true); }... }Copy the code

The Authentication and CredentialsContainer AbstractAuthenticationToken implements the interface, the inside of the specific information the reader can see the source code.

3.3 about the JWT

After verifying the user information, the next step is to authorize the user. Before talking about specific authorization, I would like to add some knowledge points about JWT Token.

Json Web Token (JWT) is an open jSON-based standard (RFC 7519) implemented for the transfer of declarations between network application environments. The token is designed to be compact and secure, especially suitable for single sign-on (SSO) scenarios in distributed sites. The JWT declaration is generally used to pass authenticated user identity information between the identity provider and the service provider to obtain resources from the resource server, and to add some additional declaration information necessary for other business logic. The token can also be used directly for authentication or can be encrypted.

From the above description, you can see the definition of JWT. Here, you can compare the difference between token authentication and traditional session authentication. JWT — JSON WEB TOKEN JWT — JSON WEB TOKEN JWT — JSON WEB TOKEN

The JWT consists of the header header, payload information, and signature signature. The following is an example of generating a good Access_token.

  • header

    The header of the JWT carries two pieces of information. One is the declaration type, in this case JWT. Second, the algorithm for declaring encryption usually uses HMAC SHA256 directly. The first part is generally fixed as:
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9Copy the code
  • Playload stores valid information, which contains three parts: declarations registered in the standard, public declarations, and private declarations. The additional information added by the author is X-Keets -UserId and X-Keets -ClientId. Readers can customize according to actual project needs. Finally, the result of playload after base64 encoding is:

    eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2Vl dHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQCopy the code
  • The third part of the signature JWT is a visa information, which consists of three parts: header, payload, secret. As for Secret, the observant reader may have noticed that there were specific Settings in the previous configuration. The first two parts are joined together to form the third part of the JWT, which is encrypted with salt secret using the encryption method declared in the header. The third part is as follows:

    5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdoCopy the code

For specific application methods, see the /logout endpoint built in the first article.

3.3 CustomAuthorizationTokenServices

Now to create a token for the user, mainly related to custom interface AuthorizationServerTokenServices here. AuthorizationServerTokenServices mainly has the following three methods:

    / / create a token
    OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
    / / refresh token
    OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
            throws AuthenticationException;
    / / access token
    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);Copy the code

Due to space limitations, the author only analyzes the implementation method of createAccessToken(), and the other methods are implemented. Readers can follow the author’s GitHub project.

public class CustomAuthorizationTokenServices implements AuthorizationServerTokenServices.ConsumerTokenServices {...public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        // Access existing AccessToken via TokenStore
        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken;
        // Remove existing AccessToken and refreshToken
        if(existingAccessToken ! =null) {
            if(existingAccessToken.getRefreshToken() ! =null) {
                refreshToken = existingAccessToken.getRefreshToken();
                // The token store could remove the refresh token when the
                    // access token is removed, but we want to be sure
                tokenStore.removeRefreshToken(refreshToken);
            }
            tokenStore.removeAccessToken(existingAccessToken);
        }
        //recreate a refreshToken
        refreshToken = createRefreshToken(authentication);

        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        if(accessToken ! =null) {
            tokenStore.storeAccessToken(accessToken, authentication);
        }
        refreshToken = accessToken.getRefreshToken();
        if(refreshToken ! =null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        returnaccessToken; }... }Copy the code

This side of the specific implementation in the above notes, basically did not rewrite much, the reader can see the source code here. CreateAccessToken () also calls two private methods to createAccessToken and refreshToken, respectively. Create accessToken based on refreshToken. Here you can customize the aging length of the token. AccessToken is created as follows:

     private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

    private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.

    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
    // Corresponding tokenId, the identity of the storage
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
        }
        token.setRefreshToken(refreshToken);
        //scope indicates the scope
        token.setScope(authentication.getOAuth2Request().getScope());
        // The custom TokenEnhancer described in the previous section is used here
        returnaccessTokenEnhancer ! =null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }Copy the code

Now that WE’re talking about TokenEnhancer, here’s a quick post of code.

public class CustomTokenEnhancer extends JwtAccessTokenConverter {

    private static final String TOKEN_SEG_USER_ID = "X-KEETS-UserId";
    private static final String TOKEN_SEG_CLIENT = "X-KEETS-ClientId";

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

        Map<String, Object> info = new HashMap<>();
        // Retrieve the UserId from the custom userDetails
        info.put(TOKEN_SEG_USER_ID, userDetails.getUserId());

        DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
        customAccessToken.setAdditionalInformation(info);

        OAuth2AccessToken enhancedToken = super.enhance(customAccessToken, authentication);
        / / set the ClientId
        enhancedToken.getAdditionalInformation().put(TOKEN_SEG_CLIENT, userDetails.getClientId());

        returnenhancedToken; }}Copy the code

After this, user identity verification and authorization token provisioning are complete. The final result returned successfully is:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cC I6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOi Jmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ.5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo"."token_type": "bearer"."refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZX JfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZhLTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5Nj U1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoiZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb 4tTux6OQWouQJ2nn1DeE"."expires_in": 43195,
    "scope": "all"."X-KEETS-UserId": "d6448c24-3c4c-4b80-8372-c2d61868f8c6"."jti": "bad72b19-d9f3-4902-affa-0430e7db79ed"."X-KEETS-ClientId": "frontend"
}Copy the code

4. To summarize

At the beginning of this paper, an overview of Auth system is given, and a brief flow chart of login and verification is drawn to facilitate readers to have a general understanding of the implementation of the system. Then it mainly explains the realization of user identity authentication and token issuance. The main classes and interfaces are analyzed and explained. The next article will focus on token authentication and CONTEXT permissions verification at the API level.

This article source address:

Making:Github.com/keets2012/A…

Code:Gitee.com/keets/Auth-…

Subscribe to the latest articles, welcome to follow my official account

Wechat official account


reference

  1. What is JWT — JSON WEB TOKEN
  2. Re: Spring Security OAuth2 from scratch

reading

Design and Implementation of Authentication and API Authority Control in Microservices Architecture (PART 1)