SpringSecurity自定义实现手机短信登录

SpringSecurity自定义登录验证-手机验证码登录

其实实现原理上跟账号密码登录一样的

1、自定义短信验证Token

定义一个仅使用手机号验证权限的鉴权Token,SpringSecurity原生的UsernamePasswordAuthenticationToken是使用username和password,如下图

SpringSecurity自定义实现手机短信登录_第1张图片

principal相当于username,credentials相当于password,所以我们仿照他的写一个跟据手机号鉴权的Token即可:

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;

/**
 * 短信登录令牌
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object telephone;

    /**
     * SmsCodeAuthenticationFilter中构建的未认证的Authentication
     *
     * @param telephone
     */
    public SmsAuthenticationToken(Object telephone) {
        super(null);
        this.telephone = telephone;
        this.setAuthenticated(false);
    }

    /**
     * SmsCodeAuthenticationProvider中构建已认证的Authentication
     *
     * @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、实现UserDetailsService接口

这一步就是封装权限的操作,实现类似于SpringSecurity的账号密码封装权限,只不过这里调用userService传入的参数是手机号,如果用户存在就返回一个带有权限的 UserDetails 实现类对象(我这里实现类是LoginUser

/**
 * 查询短信登录信息并封装为 UserDetails 这里可以抽取一个抽象类,权限加载和校验租户等逻辑交给父类处理
 */
@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);
            throw new InternalAuthenticationServiceException("手机号:" + phone + " 不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            log.info("登录用户:{} 已被删除.", phone);
            throw new ServiceException("对不起,您的账号:" + phone + " 已被删除");
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户:{} 已被停用.", phone);
            throw new DisabledException("对不起,您的账号:" + phone + " 已停用");
        }
        return createLoginUser(user);
    }

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

}

3、自定义认证authenticate

第二步返回了带有权限的 LoginUser 对象,在这里需要重写authenticate()方法,调用loadUserByUsername()方法实现身份认证逻辑返回验证Token

/**
 * 短信登录校验器
 */
//@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public SmsAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * 重写 authenticate方法,实现身份验证逻辑。
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        String telephone = (String) authenticationToken.getPrincipal();// 获取凭证也就是用户的手机号
        // 根据手机号查询用户信息UserDetails
        UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);
        if (StringUtil.isEmpty(userDetails)) {
            throw new InternalAuthenticationServiceException("用户不存在");
        }
        // 鉴权成功,返回一个拥有鉴权的 AbstractAuthenticationToken
        SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
        smsAuthenticationToken.setDetails(authenticationToken.getDetails());
        return smsAuthenticationToken;
    }

    /**
     * 重写supports方法,指定此 AuthenticationProvider 仅支持短信验证码身份验证。
     *
     * @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

这里既使用用户名密码登录也使用手机短信登录,所以UserDetailsService自定义的手机短信实现类加个@Qualifier注解防止注入失败,UserDetailsService实现类分别指定别名按别名注入:

// 分别去你的实现类里配置
// 账号密码登录
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {}
@Service("smsUserDetailsService")
// 自定义的手机短信登录
public class SmsUserDetailsService implements UserDetailsService {}

在配置文件中添加自定义的手机短信认证,并放行登录接口。其他配置已省略。

/**
 * spring security配置
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义用户认证逻辑
     */
    @Resource
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    /**
     * 自定义手机短信登录
     */
    @Resource
    @Qualifier("smsUserDetailsService")
    private UserDetailsService smsUserDetailsService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
		// 添加手机号短信登录
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/sms-login").anonymous()
                // 。。。。。。

    }

    /**
     * 身份认证接口,添加自定义的手机短信认证
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new SmsAuthenticationProvider(smsUserDetailsService));
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

5、验证接口

既然是手机短信登录,这里申请aliyun短信服务或别的企业的短信服务就忽略了,直接使用固定短信验证码+redis的方式。

写一个接口跟据手机号发送验证码,并将验证码存入redis,再响应给前端:

/**
 * 短信发送
 *
 * @param phone
 * @return
 */
@GetMapping("/sendCode/{phone}")
public AjaxResult sendCode(@PathVariable("phone") String phone) {

    SmsCode smsCode = aliyunSmsService.sendCode(phone);

    return AjaxResult.success(smsCode.getCode());
}

此处aliyunSmsService.sendCode(phone)逻辑可以自行百度aliyun短信服务。

登录接口:

/**
 * 手机验证码登录方法
 *
 * @param smsLoginBody
 * @return 结果
 */
@PostMapping("/sms-login")
public AjaxResult smsLogin(@RequestBody SmsLoginBody smsLoginBody) {
    // 生成令牌
    log.info("手机验证码登录:{}",smsLoginBody.getTelephone());
    String token = loginService.smsLogin(smsLoginBody.getTelephone(), smsLoginBody.getCode());
    return AjaxResult.success().put(Constants.TOKEN, token);
}

方法实现:

/**
 * 手机验证码登录
 *
 * @param telephone
 * @param code
 * @return
 */
public String smsLogin(String telephone, String code) {
    // 未携带手机号或验证码
    if (StringUtil.isEmpty(telephone)) {
        throw new TelePhoneException();
    }
    if (StringUtil.isEmpty(code)) {
        throw new CaptchaException();
    }
    // 获取手机验证码
    String verifyKey = CacheConstants.ALIYUN_SMS_KEY + telephone;
    String phoneCode = redisTemplate.opsForValue().get(verifyKey);
    if (StringUtil.isEmpty(phoneCode)) {
        throw new SmsException("验证码已失效");
    }
    if (!phoneCode.equals(code)) {
        throw new SmsException("验证码错误");
    }
    // 删除key
      redisTemplate.delete(verifyKey);
    // 通过手机号获取用户
    SysUser userByTelephone = userService.getUserByTelephone(telephone);
    if (StringUtil.isEmpty(userByTelephone)) {
        throw new TelePhoneException();
    }
    // 用户验证
    Authentication authentication = null;
    String username = userByTelephone.getUserName();
    try {
        SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(telephone);
        AuthenticationContextHolder.setContext(authenticationToken);
        // 该方法会去调用 SmsUserDetailsService.loadUserByUsername
        authentication = authenticationManager.authenticate(authenticationToken);
    } catch (Exception e) {
        if (e instanceof BadCredentialsException) {
            // 异步记录日志
            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();
    // 记录登录信息,修改用户表,添加登录IP、登录时间
    recordLoginInfo(loginUser.getUserId());
    // 生成token
    return tokenService.createToken(loginUser);
}

如果是若依系统,记得所有用户要有role角色。

你可能感兴趣的:(Java,java,spring,spring,boot)