[JAVA] Spring Security OAuth2 Integrated SMS authentication code login and user-defined login are used in microservices

Hey girl pieced together the original article. If there is any ambiguity, you can leave a message. If you need to reprint, you need to indicate the source.

hey said: the login authentication process has been introduced in detail in the previous article. It is suggested that brothers who are confused can take a sight first. Looking down. auth2 is integrated in the project, which gives us four built-in authorization modes + a token refresh mode, using grant_type distinction.
But think about it carefully. These four are certainly not suitable for all kinds of beautiful needs. It is not allowed to log in with the whole foreign style mobile phone, verification code or third-party wechat.

Adopt custom grant_type to add authorization mode

Note: if you just look at the implementation, you will be confused. You can also learn about the built-in authorization mode process and see the customized one. Our cerebellar bag melons can be full. To understand the principle, please see my previous article.

In addition, this is a new addition to the original four modes, rather than adding this one to cover the original. If it coexists

After the nonsense, start the whole work.

  1. General idea analysis.
    Are the five built-in implementation classes of TokenGranter managed together through tokenGranters in the CompositeTokenGranter class. Then loop through the contrast grant_type, find the corresponding implementation class to handle authorization
    Each authorization method corresponds to an AuthenticationProvider implementation class. All AuthenticationProvider implementation classes are saved through the providers collection in the ProviderManager
    , the implementation class of TokenGranter will give a whole AuthenticationToken implementation class to the manager (authentication management class), and many providers (authentication specific implementation classes) are also managed in the authentication management class. You don't have to traverse again. Use the supports method to see who the AuthenticationToken matches. Find the specific AuthenticationProvider to work.
    After a general description, do you know what we want to do when customizing.

  2. Let's start from the beginning and clear it step by step. Do a mobile phone number + password login mode practice
    First, send a post request with the old actor postman. client_id and client_secret can be placed in the parameter or in the header, just as you like.

    Use our brains. Our authorization model grant_type:phone. This pattern is defined by ourselves. There is no management in CompositeTokenGranter. We want programs to recognize new patterns. Do you have to arrange an implementation class for me. Leave it to CompositeTokenGranter to manage.

If you have a little cute, you'll be confused. How do we write implementation classes.
Let's see how other models work. We can't surpass them, but we can imitate them first.

Then let's pick a common one and write it according to resourceowner passwordtokengranter.

  1. Create a PhoneCustomTokenGranter to inherit AbstractTokenGranter
    Attribute GRANT_TYPE equals phone
package com.hey.girl.auth.grant;
/**
 * @Description: Custom grant_type issue token
 * @author: heihei
 * @date: 2021 December 3, 2011 11:39
 */
public class PhoneCustomTokenGranter extends AbstractTokenGranter {
    private static final String GRANT_TYPE = "phone";
    private final AuthenticationManager authenticationManager;

    public PhoneCustomTokenGranter(AuthenticationManager authenticationManager,
                                   AuthorizationServerTokenServices tokenServices,
                                   ClientDetailsService clientDetailsService,
                                   OAuth2RequestFactory requestFactory
                                  ) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, "phone");
    }

    protected PhoneCustomTokenGranter(AuthenticationManager authenticationManager,
                                      AuthorizationServerTokenServices tokenServices,
                                      ClientDetailsService clientDetailsService,
                                      OAuth2RequestFactory requestFactory,
                                      String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }
    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        String phone = (String)parameters.get("phone");
        String password = (String)parameters.get("password");
        parameters.remove("password");
        Authentication userAuth = new PhoneAuthenticationToken(phone, password);
        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);
        try{
            userAuth = authenticationManager.authenticate(userAuth);
        }catch (Exception e){
            throw new InvalidGrantException("Could not authenticate mobile: " + phone);
        }
        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + phone);
        }
    }
}

Q1 code parsing kankan
A1- GRANT_TYPE: authorization mode; authenticationManager authentication management class

Q2- getOAuth2Authentication method parsing
A2 - this method is called when generating a token. OAuth2Request (equal to the integration of TokenRequests+ClientDetails) and authorization (currently authorized user) are combined to generate an oauthauthentication.
In this method, it is mainly to obtain the request parameters. Login with mobile number and password. Then a PhoneAuthenticationToken is generated and passed to the manager management class through the supports method of the provider. According to different types of tokens, find the corresponding provider for specific Authentication. Get Authentication after Authentication. You can create the oauthauthentication object.

  1. In the above logic, a PhoneAuthenticationToken needs to be generated. So can we copy the UsernamePasswordAuthenticationToken we often use.
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 1L;
    /**
     * identity
     */
    private final Object principal;
    /**
     * voucher
     */
    private Object credentials;
    public PhoneAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }
    public PhoneAuthenticationToken( Object principal,Object credentials,Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public void setAuthenticated(boolean authenticated) throws IllegalArgumentException{
        Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }
    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
    @Override
    public Object getPrincipal() {
        return this.principal;
    }
}
  1. The above logic needs a Provider, so follow the DaoAuthenticationProvider
    PhoneAuthenticationProvider
/**
 1. @Description: Authentication specific implementation class
 2. @author: heihei
 3. @date: 2021 At 10:34 on December 3
 */
@Setter
public class PhoneAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;
    /**
     * Specific methods of certification
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // Assert that the provided object is an instance of the provided class
        Assert.isInstanceOf(PhoneAuthenticationToken.class,authentication,()->{
            return "Only PhoneAuthenticationToken is supported";
        });
        PhoneAuthenticationToken  authenticationToken = (PhoneAuthenticationToken) authentication;
        String mobile = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        UserDetails user =userDetailsService.loadUserByUsername(mobile);
        PhoneAuthenticationToken authenticationResult = new PhoneAuthenticationToken(user, password,user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     * support Method to indicate that you support Token authentication
     */
    @Override
    public boolean supports(Class<?> aClass) {
        // That is, judge whether the Class represented by the current Class object is the parent Class and super interface of the Class represented by the Class object passed in the parameter,
        // Or the same type. If yes, it returns true; otherwise, it returns false.
        return PhoneAuthenticationToken.class.isAssignableFrom(aClass);
    }
}

I wonder if the UserDetailsService needs to be written here. A public class girlouserdetailsserviceimpl implements UserDetailsService was written earlier. In this class, the loadUserByUsername method is used to call the remote service, query the database and other operations. But I've changed here. My need may be to query users through fields such as mobile phone number or email. Then I will write a specific business logic of GirlPhoneUserDetailsService, which can be changed according to the actual situation. Mainly to understand the role here

@Slf4j
@Service()
@RequiredArgsConstructor
public class GirlPhoneUserDetailsService implements UserDetailsService {
    /**
     * feign Call remote service
     */
    private final RemoteUserService remoteUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
        UserDetails userDetails = getUserDetails(result);
        return userDetails;
    }
    /**
     * Build user details
     * @param result User information
     * @return
     */
    private UserDetails getUserDetails(R<UserInfo> result) {
        if(result == null || result.getData() == null) {
            throw new UsernameNotFoundException("user does not exist");
        }
        UserInfo userInfo = result.getData();
        Set<String> dbAuthsSet = new HashSet<>();
        /* Get role
         * ArrayUtil It's a stupid tool
         */
        if (ArrayUtil.isNotEmpty(userInfo.getRoles())) {
            // Get role
            Arrays.stream(userInfo.getRoles()).forEach(role -> dbAuthsSet.add(SecurityConstants.ROLE + role));
            // Get resources
            dbAuthsSet.addAll(Arrays.asList(userInfo.getPermissions()));
        }
        // Note that toArray(new Object[0]) is functionally the same as toArray()
        Collection<? extends GrantedAuthority> authorities = AuthorityUtils
                .createAuthorityList(dbAuthsSet.toArray(new String[0]));
        SysUser user = userInfo.getSysUser();
        // Construct security user
        return new GirlUser(user.getUserId(), user.getDeptId(), user.getUsername(),
                SecurityConstants.BCRYPT + user.getPassword(),
                StrUtil.equals(user.getLockFlag(), CommonConstants.STATUS_NORMAL),
                true,true, true, authorities );
    }
}
  1. After basically rewriting, can we run. No!!! Think about it. We can't configure these new.
  2. Configure PhoneCustomTokenGranter. You said you added a new mode. How to be managed. Let's first look at how several authorization modes of the program itself are loaded
    Focus on the authorization server endpoints configurer. The getDefaultTokenGranters() method is called, and an instance object of CompositeTokenGranter is created for initialization. The program writes these modes by default.


    In other words, the inspiration has come down. I'll add my new model back and it'll be over. So just configure it in the authorization server config.
    // Paste some important codes
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancer())
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)
                .reuseRefreshTokens(false)
                .pathMapping("/oauth/confirm_access", "/token/confirm_access")
                .exceptionTranslator(new GirlWebResponseExceptionTranslator());
        List<TokenGranter>  tokenGranters =   getDefaultTokenGranters(
                endpoints.getTokenServices(),
                endpoints.getClientDetailsService(),
                endpoints.getAuthorizationCodeServices(),
                endpoints.getOAuth2RequestFactory()
                );
        endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters));
    }
    private List<TokenGranter> getDefaultTokenGranters(
            AuthorizationServerTokenServices tokenServices,
            ClientDetailsService clientDetails,
            AuthorizationCodeServices authorizationCodeServices,
            OAuth2RequestFactory requestFactory
                                                       ) {
        List<TokenGranter> tokenGranters = new ArrayList();
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, requestFactory));
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
        tokenGranters.add(implicit);
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
        if (this.authenticationManager != null) {
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(this.authenticationManager, tokenServices, clientDetails, requestFactory));
        }
        tokenGranters.add(new PhoneCustomTokenGranter(
                authenticationManager,tokenServices,
                clientDetails,
                requestFactory
        ));

        return tokenGranters;
    }

This is the problem encountered in the process. It is also a very common problem, that is, the five modes provided by my new mode overlay program. You can see what the author wrote Problems encountered in adding custom authorization mode , like the author, I also want to use the add method in CompositeTokenGranter, but I don't know where to use it.

  1. Do you still feel almost anything. That's right. Your new provider. Do you have to manage.
    This configuration is written in the WebSecurityConfigurerAdapter configuration class
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(phoneAuthenticationProvider());
        auth.authenticationProvider(daoAuthenticationProvider());
    }

    @Bean
    public PhoneAuthenticationProvider phoneAuthenticationProvider() {
        PhoneAuthenticationProvider phoneAuthenticationProvider= new PhoneAuthenticationProvider();
        phoneAuthenticationProvider.setUserDetailsService(userPhoneDetailsService);
        phoneAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return phoneAuthenticationProvider;
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }

Here is also the auth.authenticationProvider. You must add the provider you want to use, otherwise the GG before the program will be overwritten.

In the last step, you add an authorization mode, and your database table must be added with the corresponding. Or it won't match.
Test:

Summary: I don't know if there is any other better way. If there is, please leave a message.
That's the same sentence. First understand some login processes. Can be easily expanded

Keywords: Java Spring Microservices oauth2

Added by McManCSU on Tue, 07 Dec 2021 10:42:45 +0200