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);
}

... ...

liumang
343 声望36 粉丝

一直在思考怎么结合自己擅长的知识做些什么。现在有了好主意坚持一年,看看会有什么改变,有什么美好的事情发生。


引用和评论

0 条评论