preface

In the last article, I described the login authentication process of SpringSecurity in detail. It also makes preparations for the extension of customized login authentication such as mobile phone number + SMS verification code, email address + email verification code and third-party login authentication. In this paper, the author will take you hand in hand to realize how to achieve another login authentication in the Integration of Spring Security in the SpringBoot project, that is, mobile phone number + SMS verification code login authentication.

In order to save the time cost of building the project, the function of this paper is implemented on the basis of the open source project BlogServer, which the author modified before. The author will provide the code address of the project at the end of the article, and hope that readers can spend about 5 minutes to see the end of the article.

1 Customize AuthenticationToken

Our custom MobilePhoneAuthenticationToken classes inherit from AbstractAuthenticationToken class, It mainly provides a parameterized constructor and overwrites methods such as getCredentials, getPrincipal, setAuthenticated, eraseCredentials, and getName

public class MobilePhoneAuthenticationToken extends AbstractAuthenticationToken {
    // Login id, here is the mobile phone number
    private Object principal;
    
    // Login credentials, here is the SMS verification code
    private Object credentials;
    
    /** * constructor *@paramAuthorities *@paramPrincipal Indicates the login identity *@paramCredentials Login credentials */
    public MobilePhoneAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
    
    @Override
    public Object getCredentials(a) {
        return credentials;
    }

    @Override
    public Object getPrincipal(a) {
        return principal;
    }
    // The set method cannot be used to set the authentication identifier
    @Override
    public void setAuthenticated(boolean authenticated) {
        if (authenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }
        super.setAuthenticated(false);
    }
    // Erases login credentials
    @Override
    public void eraseCredentials(a) {
        super.eraseCredentials();
        credentials = null;
    }
    
    // Get the authentication token name
    @Override
    public String getName(a) {
        return "mobilePhoneAuthenticationToken"; }}Copy the code

2 Customize the AuthenticationProvider class

We custom MobilePhoneAuthenticationProvider classes We refer to the AbstractUserDetailsAuthenticationProvider class source code, At the same time, three interfaces such as AuthenticationProvider, InitializingBean and MessageSourceAware are implemented

At the same time in order to realize the function of the cell phone number + SMS verification code login authentication, we add the UserService in this class and RedisTemplate two class attribute, as MobilePhoneAuthenticationProvider two structure parameters of a class

The source code of this class is as follows:

public class MobilePhoneAuthenticationProvider implements AuthenticationProvider.InitializingBean.MessageSourceAware {

    private UserService userService;

    private RedisTemplate redisTemplate;

    private boolean forcePrincipalAsString = false;

    private static final Logger logger = LoggerFactory.getLogger(MobilePhoneAuthenticationProvider.class);

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    
    public MobilePhoneAuthenticationProvider(UserService userService, RedisTemplate redisTemplate) {
        this.userService = userService;
        this.redisTemplate = redisTemplate;
    }
    
   /** * Authentication method *@paramAuthentication Authentication token *@return successAuthenticationToken
     * @throwsAuthenticationException AuthenticationException */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        / / first determines the authentication parameter must be a MobilePhoneAuthenticationToken type object
        Assert.isInstanceOf(MobilePhoneAuthenticationToken.class, authentication,
                ()-> this.messages.getMessage("MobilePhoneAuthenticationProvider.onlySupports"."Only MobilePhoneAuthenticationToken is supported"));
        // Get the principal attribute of the authentication parameter as the mobile number
        String phoneNo = authentication.getPrincipal().toString();
        if (StringUtils.isEmpty(phoneNo)) {
            logger.error("phoneNo cannot be null");
            throw new BadCredentialsException("phoneNo cannot be null");
        }
        // Obtains the credentials attribute of the authentication parameter as the SMS verification code
        String phoneCode = authentication.getCredentials().toString();
        if (StringUtils.isEmpty(phoneCode)) {
            logger.error("phoneCode cannot be null");
            throw new BadCredentialsException("phoneCode cannot be null");
        }
        try {
            // Call userService to query user information based on mobile phone number
            CustomUser user = (CustomUser) userService.loadUserByPhoneNum(Long.parseLong(phoneNo));
            // Verify whether the user account is expired, locked, and valid
            userDetailsChecker.check(user);
            // Query the verification code stored when sending SMS verification codes in redis cache according to the key value composed of mobile phone numbers
            String storedPhoneCode = (String) redisTemplate.opsForValue().get("loginVerifyCode:"+phoneNo);
            if (storedPhoneCode==null) {
                logger.error("phoneCode is expired");
                throw new BadCredentialsException("phoneCode is expired");
            }
            // If the SMS verification code carried by the user is inconsistent with that queried based on the mobile phone number in Redis, a verification code error is thrown
            if(! phoneCode.equals(storedPhoneCode)) { logger.error("the phoneCode is not correct");
                throw new BadCredentialsException("the phoneCode is not correct");
            }
            // Assign the completed user information to the principal property value in the component returned authentication token
            Object principalToReturn = user;
            // If the user information is forced into a string, only the user's mobile phone number is returned
            if(isForcePrincipalAsString()) {
                principalToReturn = user.getPhoneNum();
            }
            / / certification successfully returns a MobilePhoneAuthenticationToken instance objects, principal properties for more complete user information
            MobilePhoneAuthenticationToken successAuthenticationToken = new MobilePhoneAuthenticationToken(user.getAuthorities(), principalToReturn, phoneCode);
            return successAuthenticationToken;
        } catch (UsernameNotFoundException e) {
            // The user's mobile phone number does not exist. If the user has registered, the user is prompted to go to the personal information page to add mobile phone number information first. Otherwise, the user is prompted to register as a user with mobile phone number and then log in
            logger.error("user " + phoneNo + "not found, if you have been register as a user, please goto the page of edit user information to add you phone number, " +
                    "else you must register as a user use you phone number");
            throw new BadCredentialsException("user " + phoneNo + "not found, if you have been register as a user, please goto the page of edit user information to add you phone number, " +
                    "else you must register as a user use you phone number");
        } catch (NumberFormatException e) {
            logger.error("invalid phoneNo, due it is not a number");
            throw new BadCredentialsException("invalid phoneNo, due do phoneNo is not a number"); }}/ * * * only supports custom MobilePhoneAuthenticationToken class certification * /
    @Override
    public boolean supports(Class
        aClass) {
        return aClass.isAssignableFrom(MobilePhoneAuthenticationToken.class);
    }

    @Override
    public void afterPropertiesSet(a) throws Exception {
        Assert.notNull(this.messages, "A message source must be set");
        Assert.notNull(this.redisTemplate, "A RedisTemplate must be set");
        Assert.notNull(this.userService, "A UserDetailsService must be set");
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
        this.forcePrincipalAsString = forcePrincipalAsString;
    }

    public boolean isForcePrincipalAsString(a) {
        returnforcePrincipalAsString; }}Copy the code

In this custom authenticator class, custom authentication logic is mainly completed in the Authenticate method, and a new one is returned after successful authentication

After MobilePhoneAuthenticationToken object, principal properties for certification through the user details.

3 Customize the AuthenticationFilter class

We custom MobilePhoneAuthenticationFilter follow UsernamePasswordAuthenticationFilter class source code to achieve a dedicated phone number + login authentication code certification authentication filter, its source code is as follows, AttemptAuthentication method is used to extract request parameters such as cell phone number and SMS verification code from HttpServletRequest type request parameters. And then assembled into a MobilePhoneAuthenticationToken object, used to invoke the enclosing getAuthenticationManager () passed as a parameter to authenticate method.

Rewrite attemptAuthentication method after MobilePhoneAuthenticationFilter class source code is as follows:

/** * Custom mobile login authentication filter */
public class MobilePhoneAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String SPRING_SECURITY_PHONE_NO_KEY = "phoneNo";

    public static final String SPRING_SECURITY_PHONE_CODE_KEY = "phoneCode";

    private String phoneNoParameter = SPRING_SECURITY_PHONE_NO_KEY;

    private String phoneCodeParameter = SPRING_SECURITY_PHONE_CODE_KEY;

    private boolean postOnly = true;

    public MobilePhoneAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    public MobilePhoneAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        super(requiresAuthenticationRequestMatcher);
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        if(postOnly && ! httpServletRequest.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + httpServletRequest.getMethod());
        }
        String phoneNo = obtainPhoneNo(httpServletRequest);
        if (phoneNo==null) {
            phoneNo = "";
        } else {
            phoneNo = phoneNo.trim();
        }
        String phoneCode = obtainPhoneCode(httpServletRequest);
        if (phoneCode==null) {
            phoneCode = "";
        } else {
            phoneCode = phoneCode.trim();
        }
        MobilePhoneAuthenticationToken authRequest = new MobilePhoneAuthenticationToken(new ArrayList<>(), phoneNo, phoneCode);
        this.setDetails(httpServletRequest, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Nullable
    protected String obtainPhoneNo(HttpServletRequest request) {
        return request.getParameter(phoneNoParameter);
    }

    @Nullable
    protected String obtainPhoneCode(HttpServletRequest request) {
        return request.getParameter(phoneCodeParameter);
    }

    protected void setDetails(HttpServletRequest request, MobilePhoneAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); }}Copy the code

4 Modify the UserService class

The UserService class is mainly used to query user-defined information of users. We add a method to query user information by mobile phone number in this class. Notice If the user table does not contain a field for mobile phone numbers, add a field for storing mobile phone numbers. The column type is BigINT, and the field type is Long in the entity class

The implementation code of UserService class to query user information according to the user’s mobile phone number is as follows:

@Service
@Transactional
public class UserService implements CustomUserDetailsService {
    @Resource
    UserMapper userMapper;
    
    @Resource
    RolesMapper rolesMapper;
    
    @Resource
    PasswordEncoder passwordEncoder;
   
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    
    /** * Query user details based on the user's mobile phone number *@paramPhoneNum Mobile phone number *@return customUser
     * @throws UsernameNotFoundException
     */
     @Override
    public UserDetails loadUserByPhoneNum(Long phoneNum) throws UsernameNotFoundException {
        logger.info("User Login authentication, phoneNum={}", phoneNum);
        UserDTO userDTO = userMapper.loadUserByPhoneNum(phoneNum);
        if (userDTO == null) {
            / / UsernameNotFoundException anomalies
            throw  new UsernameNotFoundException("user " + phoneNum + " not exist!");
        }
        CustomUser customUser = convertUserDTO2CustomUser(userDTO);
        return customUser;
    }
    
    /** * UserDTO to the CustomUser object *@param userDTO
     * @return user
     */
    private CustomUser convertUserDTO2CustomUser(UserDTO userDTO) {
        // Query the role information of the user and return it to the user
        List<Role> roles = rolesMapper.getRolesByUid(userDTO.getId());
        // Roles with large permissions are ranked first
        roles.sort(Comparator.comparing(Role::getId));
        CustomUser user = new CustomUser(userDTO.getUsername(), userDTO.getPassword(),
                userDTO.getEnabled()==1.true.true.true.new ArrayList<>());
        user.setId(userDTO.getId());
        user.setNickname(userDTO.getNickname());
        user.setPhoneNum(userDTO.getPhoneNum());
        user.setEmail(userDTO.getEmail());
        user.setUserface(userDTO.getUserface());
        user.setRegTime(userDTO.getRegTime());
        user.setUpdateTime(userDTO.getUpdateTime());
        user.setRoles(roles);
        user.setCurrentRole(roles.get(0));
        returnuser; }}Copy the code

The UserDTO and CustomUser entities are listed below:

public class UserDTO implements Serializable {

    private Long id;

    private String username;

    private String password;

    private String nickname;

    private Long phoneNum;

    // Valid identifier: 0- invalid; 1 - effective
    private int enabled;

    private String email;

    private String userface;

    private Timestamp regTime;

    private Timestamp updateTime;
    / /... Omit the set and get methods for individual attributes
}
Copy the code
public class CustomUser extends User {
    private Long id;
    private String nickname;
    private Long phoneNum;
    private List<Role> roles;
    // Current role
    private Role currentRole;
    private String email;
    private String userface;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date regTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date updateTime;

    public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public CustomUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }
    
    @Override
    @JsonIgnore
    public List<GrantedAuthority> getAuthorities(a) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode()));
        }
        return authorities;
    }
    / /... Omit the set and get methods for the other attributes
    
}
Copy the code

Mapper layer according to the mobile phone number query user details code is as follows:

UserMapper.java

@Repository
public interface UserMapper {

    UserDTO loadUserByPhoneNum(@Param("phoneNum") Long phoneNum);
    / /... Omit other abstract methods
}
Copy the code

UserMapper.xml

<select id="loadUserByPhoneNum" resultType="org.sang.pojo.dto.UserDTO">
        SELECT id, username, nickname,password, phoneNum, enabled, email, userface, regTime, updateTime
        FROM `user`
        WHERE phoneNum = #{phoneNum,jdbcType=BIGINT}
</select>
Copy the code

5 Modify the sendLoginVeryCodeMessage method of the SMS service

How to integrate Tencent cloud SMS service to achieve SMS verification code function in the SpringBoot project, you can refer to my article published in the public number before SpringBoot project quickly integrate Tencent cloud SMS SDK to achieve mobile verification code function

However, the verification code needs to be slightly modified, because the domestic mobile phone number must be prefixed with +86 followed by the user’s 11-digit mobile phone number. The 11-digit mobile phone number is stored in our database, and the 11-digit mobile phone number is also used when we log in using mobile phone number + SMS verification code. Therefore, the +86 prefix of the mobile phone number should be removed when the SMS verification code is stored in the Redis cache. If this is not changed, then the user’s mobile phone number field in the database should be designed as a string, and the phone number parameter passed by the front-end user when logging in should also be prefixed with +86. In order to avoid any more changes, we’ll do it here.

SmsService.java

public SendSmsResponse sendLoginVeryCodeMessage(String phoneNum) {
        SendSmsRequest req = new SendSmsRequest();
        req.setSenderId(null);
        req.setSessionContext(null);
        req.setSign("Alfred on the Java Technology Stack");
        req.setSmsSdkAppid(smsProperty.getAppid());
        req.setTemplateID(SmsEnum.PHONE_CODE_LOGIN.getTemplateId());
        req.setPhoneNumberSet(new String[]{phoneNum});
        String verifyCode = getCode();
        String[] params = new String[]{verifyCode, "10"};
        req.setTemplateParamSet(params);
        logger.info("req={}", JSON.toJSONString(req));
        try {
            SendSmsResponse res = smsClient.SendSms(req);
            if ("Ok".equals(res.getSendStatusSet()[0].getCode())) {
                // If the SMS verification code is successfully sent, the verification code will be saved in redis cache (currently only for domestic SMS services).
                phoneNum = phoneNum.substring(3);
                redisTemplate.opsForValue().set("loginVerifyCode:"+phoneNum, verifyCode, 10, TimeUnit.MINUTES);
            }
            return res;
        } catch (TencentCloudSDKException e) {
            logger.error("send message failed", e);
            throw new RuntimeException("send message failed, caused by " + e.getMessage());
        }
    
    // Other code omitted

Copy the code

6 Modify the WebSecurityConfig configuration class

Finally, we need to modify the WebSecurityConfig configuration class, define MobilePhoneAuthenticationProvider and the AuthenticationManager two classes of bean method, Add new logic handling to both configure methods at the same time.

The final complete code for the WebSecurityConfig configuration class is as follows:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserService userService;
    @Resource
    RedisTemplate<String, Object> redisTemplate;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
        MobilePhoneAuthenticationProvider mobilePhoneAuthenticationProvider = this.mobilePhoneAuthenticationProvider();
        auth.authenticationProvider(mobilePhoneAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Add a mobile login authentication filter and set the intercepting authentication request path in the constructor
        MobilePhoneAuthenticationFilter mobilePhoneAuthenticationFilter = new MobilePhoneAuthenticationFilter("/mobile/login");
        mobilePhoneAuthenticationFilter.setAuthenticationSuccessHandler(new FormLoginSuccessHandler());
        mobilePhoneAuthenticationFilter.setAuthenticationFailureHandler(new FormLoginFailedHandler());
        / / the following the authenticationManager must be set, otherwise the MobilePhoneAuthenticationFilter# attemptAuthentication
        / / method calls this. GetAuthenticationManager () authenticate will quote NullPointException (authRequest) method
        mobilePhoneAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
        mobilePhoneAuthenticationFilter.setAllowSessionCreation(true);
        http.addFilterAfter(mobilePhoneAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        // Configure cross-domain
        http.cors().configurationSource(corsConfigurationSource());
        // Disable spring Security framework logout and use custom logout
        http.logout().disable();
        http.authorizeRequests()
                .antMatchers("/user/reg").anonymous()
                .antMatchers("/sendLoginVerifyCode").anonymous()
                .antMatchers("/doc.html").hasAnyRole("user"."admin")
                .antMatchers("/admin/**").hasRole("admin")
                /// the admin/** URL must have the super administrator role. If you use the.hasauthority () method to configure the URL, add ROLE_ to the parameter as follows :hasAuthority("ROLE_ super administrator ")
                .anyRequest().authenticated()// All other paths are accessible after login
                .and().formLogin().loginPage("http://localhost:3000/#/login")
                .successHandler(new FormLoginSuccessHandler())
                .failureHandler(new FormLoginFailedHandler()).loginProcessingUrl("/user/login")
                .usernameParameter("username").passwordParameter("password").permitAll()
                .and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(getAccessDeniedHandler());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/blogimg/**"."/index.html"."/static/**");
    }

    @Bean
    AccessDeniedHandler getAccessDeniedHandler(a) {
        return new AuthenticationAccessDeniedHandler();
    }

    // Configure cross-domain access to resources
    private CorsConfigurationSource corsConfigurationSource(a) {
        UrlBasedCorsConfigurationSource source =   new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");	* indicates that all requests are regarded as the same source. If you need to specify IP address and port number, you can change the IP address and port number to localhost: 8080, which are separated by commas (,).
        corsConfiguration.addAllowedHeader("*");//header, which headers are allowed? In this case, the token is used.
        corsConfiguration.addAllowedMethod("*");	// Allowed request methods, PSOT, GET etc
        corsConfiguration.setAllowCredentials(true);
        // Register the cross-domain configuration
        source.registerCorsConfiguration("/ * *",corsConfiguration); // Configure the URL to allow cross-domain access
        return source;
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean(a) throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Bean
    public MobilePhoneAuthenticationProvider mobilePhoneAuthenticationProvider(a) {
        MobilePhoneAuthenticationProvider mobilePhoneAuthenticationProvider = new MobilePhoneAuthenticationProvider(userService, redisTemplate);
        returnmobilePhoneAuthenticationProvider; }}Copy the code

7 Effect Experience

After coding, we started our SpringBoot project after starting Mysql server and Redis server

Call the interface for sending SMS verification codes in Postman

Note: The phoneNmber parameter above can be entered into any user’s valid domestic mobile phone number, starting with +86, and then click the blue Send button in the upper right corner of Postman to send the request.

If the verification code is successfully sent, the following response message is displayed:

{
    "status": 200."msg": "success"."data": {
        "code": "Ok"."phoneNumber": "+ 8618682244076"."fee": 1."message": "send success"}}Copy the code

The phone will also receive a six-digit SMS verification code, valid for 10 minutes

Then we call the login interface using our mobile phone number + the 6-digit SMS verification code received

After successful login, the following response information is displayed:

{
    "msg": "login success"."userInfo": {
        "accountNonExpired": true."accountNonLocked": true."authorities": [{"authority": "ROLE_admin"
            },
            {
                "authority": "ROLE_user"
            },
            {
                "authority": "ROLE_test1"}]."credentialsNonExpired": true."currentRole": {
            "id": 1."roleCode": "admin"."roleName": "Administrator"
        },
        "email": "[email protected]"."enabled": true."id": 3."nickname": "Alfred the Programmer."."phoneNum": 18682244076."regTime": 1624204813000."roles": [{"$ref": "$.userInfo.currentRole"
            },
            {
                "id": 2."roleCode": "user"."roleName": "Ordinary user"
            },
            {
                "id": 3."roleCode": "test1"."roleName": "Test Role 1"}]."username": "heshengfu"
    },
    "status": "success"
}
Copy the code

Write in the last

Add the mobile phone number + SMS code to the SpringBoot application for login authentication. If you feel that the article is helpful to you, you are welcome to give me this article in view and forward to the programmer colleagues and friends around, thank you! Later, when I have time, I will call the background interface of this implementation to realize the function of mobile phone number + SMS verification code on the front-end user login interface.

The following is the source address of this article in my Gitee warehouse, need to study the complete code of friends can be cloned to their own local.

Gitee clone of BlogServer project: gitee.com/heshengfu12…

This article first personal wechat public number [Fu on Web programming], think my article is helpful to you, welcome to add a wechat public number to pay attention to. There is my contact information in the [contact author] menu of the public account, and welcome to add my wechat to exchange technical problems, so that we will not be alone on the road of technological advancement!