SpringSecurity custom login verification – mobile phone verification code login
In fact, the implementation principle is the same as the account password login.
1. Custom SMS Verification Token
Define an authentication Token that only uses the mobile phone number to verify the authority. SpringSecurity’s native UsernamePasswordAuthenticationToken
uses username and password, as shown below
The principal is equivalent to the username, and the credentials are equivalent to the password, so we can follow his example and write a Token that is authenticated according to the mobile phone number:
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.Assert; import java.util.Collection; /** * SMS login token */ public class SmsAuthenticationToken extends AbstractAuthenticationToken {<!-- --> private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object telephone; /** * Unauthenticated Authentication built in SmsCodeAuthenticationFilter * * @param telephone */ public SmsAuthenticationToken(Object telephone) {<!-- --> super(null); this.telephone = telephone; this. setAuthenticated(false); } /** * Construct authenticated Authentication in SmsCodeAuthenticationProvider * * @param telephone * @param authorities */ public SmsAuthenticationToken(Object telephone, Collection<? extends GrantedAuthority> authorities) {<!-- --> super(authorities); this.telephone = telephone; super. setAuthenticated(true); } @Override public Object getCredentials() {<!-- --> return null; } @Override public Object getPrincipal() {<!-- --> return this.telephone; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {<!-- --> Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super. setAuthenticated(false); } @Override public void eraseCredentials() {<!-- --> super. eraseCredentials(); } }
2. Implement UserDetailsService interface
This step is the operation of encapsulating permissions, which is similar to SpringSecurity’s account password encapsulating permissions, except that the parameter passed in by calling userService here is the mobile phone number. If the user exists, it will return a UserDetails
implementation with permissions. Class object (my implementation class here is LoginUser
)
/** * Query SMS login information and encapsulate it as UserDetails Here, an abstract class can be extracted, and logics such as permission loading and tenant verification can be handed over to the parent class for processing */ @Service("smsUserDetailsService") public class SmsUserDetailsService implements UserDetailsService {<!-- --> private static final Logger log = LoggerFactory. getLogger(SmsUserDetailsService. class); @Resource private ISysUserService userService; @Resource private SysPermissionService permissionService; /** * loadUserByUsername * * @param phone * @return LoginUser * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {<!-- --> SysUser user = userService. getUserByTelephone(phone); if (StringUtils.isNull(user)) {<!-- --> log.info("Phone number: {} does not exist.", phone); throw new InternalAuthenticationServiceException("Mobile phone number: " + phone + " does not exist"); } else if (UserStatus. DELETED. getCode(). equals(user. getDelFlag())) {<!-- --> log.info("Login user: {} has been deleted.", phone); throw new ServiceException("Sorry, your account: " + phone + " has been deleted"); } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {<!-- --> log.info("Login user: {} has been disabled.", phone); throw new DisabledException("Sorry, your account: " + phone + " has been disabled"); } return createLoginUser(user); } public UserDetails createLoginUser(SysUser user) {<!-- --> return new LoginUser(user. getUserId(), user. getDeptId(), user, permissionService. getMenuPermission(user)); } }
3. Custom authentication authenticate
The second step returns the LoginUser object with permissions, here you need to rewrite the authenticate() method, call the loadUserByUsername() method to implement the identity authentication logic and return the verification Token
/** * SMS login validator */ //@Component public class SmsAuthenticationProvider implements AuthenticationProvider {<!-- --> private UserDetailsService userDetailsService; public SmsAuthenticationProvider(UserDetailsService userDetailsService) {<!-- --> this. userDetailsService = userDetailsService; } /** * Override the authenticate method to implement authentication logic. * * @param authentication * @return * @throws AuthenticationException */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException {<!-- --> SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication; String telephone = (String) authenticationToken.getPrincipal();// Obtain the certificate, which is the user's mobile phone number // Query user information UserDetails according to mobile phone number UserDetails userDetails = userDetailsService.loadUserByUsername(telephone); if (StringUtil.isEmpty(userDetails)) {<!-- --> throw new InternalAuthenticationServiceException("User does not exist"); } // Authentication is successful, return an AbstractAuthenticationToken with authentication SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities()); smsAuthenticationToken.setDetails(authenticationToken.getDetails()); return smsAuthenticationToken; } /** * Override the supports method to specify that this AuthenticationProvider only supports SMS verification code authentication. * * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) {<!-- --> return SmsAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() {<!-- --> return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) {<!-- --> this. userDetailsService = userDetailsService; } }
4. SecurityConfig
Here, both the username and password are used to log in and the SMS is used to log in, so the UserDetailsService custom SMS implementation class adds a @Qualifier
annotation to prevent injection failure, and the UserDetailsService implementation class specifies aliases to inject according to the aliases:
// Go to your implementation class to configure // account password login @Service("userDetailsServiceImpl") public class UserDetailsServiceImpl implements UserDetailsService {<!-- -->} @Service("smsUserDetailsService") // Customized SMS login public class SmsUserDetailsService implements UserDetailsService {<!-- -->}
Add a custom SMS authentication in the configuration file and allow the login interface. Other configurations have been omitted.
/** * spring security configuration */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {<!-- --> /** * Custom user authentication logic */ @Resource @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; /** * Customize SMS login */ @Resource @Qualifier("smsUserDetailsService") private UserDetailsService smsUserDetailsService; @Override protected void configure(HttpSecurity httpSecurity) throws Exception {<!-- --> // Add mobile phone number SMS login httpSecurity // CSRF is disabled because sessions are not used .csrf().disable() // filter requests .authorizeRequests() // For login login registration register verification code captchaImage allows anonymous access .antMatchers("/sms-login").anonymous() // . . . . . . } /** * Identity authentication interface, add custom SMS authentication */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {<!-- --> auth.authenticationProvider(new SmsAuthenticationProvider(smsUserDetailsService)); auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } }
5. Verification interface
Since it is a mobile phone SMS login, the application for aliyun SMS service or SMS service of other companies is ignored here, and the fixed SMS verification code + redis method is used directly.
Write an interface to send the verification code according to the mobile phone number, store the verification code in redis, and then respond to the front end:
/** * send a text message * * @param phone * @return */ @GetMapping("/sendCode/{phone}") public AjaxResult sendCode(@PathVariable("phone") String phone) {<!-- --> SmsCode smsCode = aliyunSmsService. sendCode(phone); return AjaxResult.success(smsCode.getCode()); }
Here aliyunSmsService.sendCode(phone)
logic can be used for Baidu aliyun SMS service.
Login interface:
/** * Mobile phone verification code login method * * @param smsLoginBody * @return result */ @PostMapping("/sms-login") public AjaxResult smsLogin(@RequestBody SmsLoginBody smsLoginBody) {<!-- --> // generate token log.info("Mobile phone verification code login: {}", smsLoginBody.getTelephone()); String token = loginService.smsLogin(smsLoginBody.getTelephone(), smsLoginBody.getCode()); return AjaxResult.success().put(Constants.TOKEN, token); }
Method implementation:
/** * Mobile phone verification code login * * @param telephone * @param code * @return */ public String smsLogin(String telephone, String code) {<!-- --> // No mobile phone number or verification code is carried if (StringUtil.isEmpty(telephone)) {<!-- --> throw new TelePhoneException(); } if (StringUtil.isEmpty(code)) {<!-- --> throw new CaptchaException(); } // Get the mobile phone verification code String verifyKey = CacheConstants.ALIYUN_SMS_KEY + telephone; String phoneCode = redisTemplate.opsForValue().get(verifyKey); if (StringUtil.isEmpty(phoneCode)) {<!-- --> throw new SmsException("Verification code has expired"); } if (!phoneCode.equals(code)) {<!-- --> throw new SmsException("Verification code error"); } // delete key redisTemplate.delete(verifyKey); // Get user by phone number SysUser userByTelephone = userService. getUserByTelephone(telephone); if (StringUtil.isEmpty(userByTelephone)) {<!-- --> throw new TelePhoneException(); } // User Authentication Authentication authentication = null; String username = userByTelephone. getUserName(); try {<!-- --> SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(telephone); AuthenticationContextHolder.setContext(authenticationToken); // This method will call SmsUserDetailsService.loadUserByUsername authentication = authenticationManager.authenticate(authenticationToken); } catch (Exception e) {<!-- --> if (e instanceof BadCredentialsException) {<!-- --> // asynchronous logging AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } else {<!-- --> AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_FAIL, e.getMessage())); throw new ServiceException(e. getMessage()); } } finally {<!-- --> AuthenticationContextHolder. clearContext(); } AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); LoginUser loginUser = (LoginUser) authentication. getPrincipal(); // Record login information, modify user table, add login IP, login time recordLoginInfo(loginUser. getUserId()); // generate token return tokenService.createToken(loginUser); }
If it is the Ruoyi system, remember that all users must have a role role.