博客 / 詳情

返回

若依添加手機驗證碼登錄

SpringSecurity登錄流程

在若依平台上的賬號密碼登錄流程如下:
SysLoginController.login()--->loginService.login()--->authenticationManager.authenticate(){
--->daoAuthenticationProvider.authenticate()--->userDetailsService.loadUserByUsername()
} ---> 根據用户信息創建token返回

其中:

  • AuthenticationManager、DaoAuthenticationProvider、UserDetailsService的綁定關係在SecurityConfig中實現
  • AuthenticationManager具體實現是ProvideManager,其裏面有一個List<AuthenticationProvider>,在調用authenticate方法時,會for循環調用 AuthenticationProvider的authenticate方法,如果ProvideManager能處理,那麼for循環終止,如果不能處理變回繼續調用下一個。AuthenticationProvider通過supports(...)判端是支持某種方式認證
  • DaoAuthenticationProvider是SpringSecurity實現的一個根據賬號密碼認證的Provider,其會調用UserDetailsService的loadUserByUsername去查詢用户的詳細信息,包括權限,因此我們一般需要實現該接口,根據用户和密碼實現查詢用户的功能。
  • authenticationManager.authenticate(Authentication authentication),其中UsernamePasswordAuthenticationToken是SpringSecurity實現的通過賬號和密碼登錄的Authentication

手機號和驗證碼登錄流程

我們需要仿照賬號密碼登錄流程,實現短信密碼登錄流程,因此我們需要自定義實現:

  • SysLoginController.smsLogin() 手機驗證碼登錄接口入口
  • loginService.smsLogin() 手機號登錄業務實現
  • SmsAuthenticationToken,自定義,繼承AbstractAuthenticationToken,AuthenticationManager認證時的參數
  • SmsAuthProvider,自定義,繼承AuthenticationProvider,實現認證功能,主要是檢查手機驗證碼是否正確
  • UserDetailsServiceImpl新增一個方法,根據手機號查詢用户的詳細信息
  • 當然發送驗證碼、從redis中獲取驗證碼判斷是否正確這裏不再詳細描述

手機號驗證碼登錄實現

1.接口入口

@PostMapping("/smsLogin")
public AjaxResult loginWithSmsCode(@RequestBody @Valid SmsLoginParam loginParam) {
    AjaxResult ajax = AjaxResult.success();
    // 生成令牌
    String token = loginService.smsLogin(loginParam.getPhone(),loginParam.getCode());
    ajax.put(Constants.TOKEN, token);
    return ajax;
}

2.loginService.smsLogin()業務實現

/**
 * 短信登錄
 *
 * @param phone 手機號
 * @param smsCode 手機驗證碼
 * @return 結果
 */
public String smsLogin(String phone,String smsCode) {
    // 驗證碼校驗
    Authentication authentication = null;
    try {
        //通過手機號查詢出密碼
        SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(phone, smsCode);
        // 該方法會去調用UserDetailsServiceImpl.loadUserByPhoneNumber
        authentication = authenticationManager.authenticate(smsAuthenticationToken);
    } catch (Exception e) {
        if (e instanceof BadCredentialsException) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, ServletUtils.getTenantId(),Constants.LOGIN_FAIL, e.getMessage()));
            throw new ServiceException(e.getMessage());
        } else {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone,ServletUtils.getTenantId(), Constants.LOGIN_FAIL, e.getMessage()));
            throw new ServiceException(e.getMessage());
        }
    } finally {
        AuthenticationContextHolder.clearContext();
    }
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, ServletUtils.getTenantId(),Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    recordLoginInfo(loginUser.getUserId());
    // 生成token
    return tokenService.createToken(loginUser);
}

3.SmsAuthenticationToken實現

/**
 * @author ljq
 * @date 2024/11/12
 * @description 實現短信驗證碼的憑證,類似UsernamePasswordAuthenticationToken
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 手機號
     */
    private final Object principal;

    /**
     * 驗證碼
     */
    private Object credentials;

    public SmsAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    public SmsAuthenticationToken(Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return credentials;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

4.SmsAuthProvider實現

/**
 * @author ljq
 * @date 2024/11/12
 * @description TODO
 */
public class SmsAuthProvider implements AuthenticationProvider {

    private final SmsUserDetailsService smsUserDetailsService;

    private final SmsCaptchaRedisCache smsCaptchaRedisCache;

    public SmsAuthProvider(SmsUserDetailsService smsUserDetailsService, SmsCaptchaRedisCache smsCaptchaRedisCache) {
        this.smsUserDetailsService = smsUserDetailsService;
        this.smsCaptchaRedisCache = smsCaptchaRedisCache;
    }


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String phone = (String) authentication.getPrincipal();
        String smsCode = (String) authentication.getCredentials();

        if (!smsCaptchaRedisCache.isExistCaptcha(phone)) {
            throw new BadCredentialsException("驗證碼已過期");
        }

        if (!smsCaptchaRedisCache.checkCaptcha(phone, smsCode)) {
            throw new BadCredentialsException("驗證碼錯誤");
        }

        UserDetails userDetails = smsUserDetailsService.loadUserByPhoneNumber(phone);

        return new SmsAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

5.UserDetailsServiceImpl實現根據手機號查詢用户詳情

@Service
public class UserDetailsServiceImpl implements UserDetailsService, SmsUserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPasswordService passwordService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ... ...
        return createLoginUser(user);
    }

    @Override
    public UserDetails loadUserByPhoneNumber(String phoneNumber) throws UsernameNotFoundException {

        String tenantId = ServletUtils.getHeader(HEADER_KEY_TENANT);
        SysUser user = userService.selectUserByPhoneNumber(phoneNumber,tenantId);
        if (StringUtils.isNull(user)) {
            log.info("登錄用户電話:{} 不存在.", phoneNumber);
            //throw new ServiceException(MessageUtils.message("user.not.exists"));
            throw new ServiceException("電話不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            log.info("登錄用户電話:{} 已被刪除.", phoneNumber);
            throw new ServiceException(MessageUtils.message("user.password.delete"));
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登錄用户電話:{} 已被停用.", phoneNumber);
            throw new ServiceException(MessageUtils.message("user.blocked"));
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

其中SmsUserDetailsService為新定義的一個接口,裏面聲明瞭loadUserByPhoneNumber方法

6.在SecurityConfig中AuthenticationManager、SmsAuthProvider、SmsUserDetailsService關係綁定

... ...

@Bean
public AuthenticationManager authenticationManager() {

    //賬號密碼登錄校驗
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(userDetailsService);
    daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());

    //短信登錄校驗,UserDetailsServiceImpl 也實現了SmsUserDetailsService,所以可以直接強轉
    SmsAuthProvider smsAuthProvider = new SmsAuthProvider((SmsUserDetailsService) userDetailsService, smsCaptchaRedisCache);

    return new ProviderManager(daoAuthenticationProvider,smsAuthProvider);
}

... ...
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.