springsecurity整合短信登录

逻辑图
在这里插入图片描述
逻辑
原有用户密码的逻辑
1.先进入 UsernamePasswordAuthenticationFilter 中,根据输入的用户名和密码信息,构造出一个暂时没有鉴权的 UsernamePasswordAuthenticationToken,并将 UsernamePasswordAuthenticationToken 交给 AuthenticationManager 处理。
2.AuthenticationManager 本身并不做验证处理,他通过 for-each 遍历找到符合当前登录方式的一个 AuthenticationProvider,并交给它进行验证处理,对于用户名密码登录方式,这个 Provider 就是 DaoAuthenticationProvider。
3.在这个 Provider 中进行一系列的验证处理,如果验证通过,就会重新构造一个添加了鉴权的 UsernamePasswordAuthenticationToken,并将这个 token 传回到 UsernamePasswordAuthenticationFilter 中。
4.在该 Filter 的父类 AbstractAuthenticationProcessingFilter 中,会根据上一步验证的结果,跳转到 successHandler 或者是 failureHandler

短信验证码的逻辑
1.用户名密码登录有个 UsernamePasswordAuthenticationFilter ,我们搞一个 SmsAuthenticationFilter,代码粘过来改一改。
2.用户名密码登录需要 UsernamePasswordAuthenticationToken,我们搞一个 SmsAuthenticationToken,代码粘过来改一改。
3.用户名密码登录需要 DaoAuthenticationProvider,我们模仿它也 implenments AuthenticationProvider,叫做 SmsAuthenticationProvider。
4.先经过 SmsAuthenticationFilter,构造一个没有鉴权的 SmsAuthenticationToken,然后交给 AuthenticationManager 处理。
5.AuthenticationManager 通过 for-each 挑选出一个合适的 provider 进行处理,当然我们希望这个 provider 要是 SmsAuthenticationProvider。
6.验证通过后,重新构造一个有鉴权的 SmsAuthenticationToken,并返回给 SmsAuthenticationFilter。
filter 根据上一步的验证结果,跳转到成功或者失败的处理逻辑。

代码实例
1.创建验证码生成器

package com.lc.gansu.security.smsValidate;

import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Component;

/**
 * TODO 创建验证码生成器
 *
 * @author songtianxiong
 * @version 1.0
 * @date 2021/12/17 16:33
 */
@Component
public class SmsCodeGenerator {
    public String generate() {
        return RandomStringUtils.randomNumeric(4);
    }
}

2.验证码发送器

package com.lc.gansu.security.smsValidate;

import org.springframework.stereotype.Component;

/**
 * TODO 验证码发送器
 *
 * @author songtianxiong
 * @version 1.0
 * @date 2021/12/20 14:22
 */
@Component
public class SmsCodeSender {
    public void send(String mobile, String code) {
        System.out.println("向手机" + mobile + "发送短信验证码" + code);
    }
}

3.发送短信接口

package com.lc.gansu.security.smsValidate;

import com.lc.gansu.security.component.SecurityConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * TODO 发送短信接口
 *
 * @author songtianxiong
 * @version 1.0
 * @date 2021/12/17 16:34
 */
@RestController
public class ValidateCodeController {
    @Autowired
    private SmsCodeGenerator smsCodeGenerator;
    @Resource
    private SmsCodeSender smsCodeSender;
    @Resource
    private RedisTemplate redisTemplate;

    @GetMapping("/code/sms")
    public String createSmsCode(@RequestParam String mobile) throws IOException {
        //获取验证码
        String smsCode = smsCodeGenerator.generate();
        //把验证码设置到redis
        redisTemplate.boundValueOps(SecurityConstants.getValidCodeKey(mobile)).set(smsCode, 300, TimeUnit.SECONDS);
        smsCodeSender.send("18360903475", "登录验证码为:" + smsCode + ",五分钟过期");
        return "验证码是 : " + smsCode;
    }
}

4.token:两个实现方法:未鉴权的和已鉴权的

package com.lc.gansu.security.smsValidate;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * TODO 两个实现方法:未鉴权的和已鉴权的
 * 步骤:
 *
 * principal 原本代表用户名,这里保留,只是代表了手机号码。
 * credentials 原本代码密码,短信登录用不到,直接删掉。
 * SmsCodeAuthenticationToken() 两个构造方法一个是构造没有鉴权的,一个是构造有鉴权的。
 * 剩下的几个方法去除无用属性即可。
 *
 * @author songtianxiong
 * @version 1.0
 * @date 2021/12/17 16:39
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    /**
     * UsernamePasswordAuthenticationToken类里代表用户名
     * 现在代表手机号
     */
    private final Object principal;

    /**
     * 通过手机号构造未鉴权的token
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 通过手机号构造已鉴权的token
     *
     * @param principal
     * @param authorities
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

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

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

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

}

5.短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter

package com.lc.gansu.security.smsValidate;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /**
     * form表单中手机号码的字段name
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    /**
     * 是否仅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        // 短信登录的请求 post 方式的 /sms/login
        super(new AntPathRequestMatcher("/sms/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);

        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public String getMobileParameter() {
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}

6.SmsUserDetailsService 用户信息

package com.lc.gansu.security.smsValidate;

import com.lc.gansu.module.sys.domain.Org;
import com.lc.gansu.module.sys.domain.Permission;
import com.lc.gansu.module.sys.domain.Role;
import com.lc.gansu.security.SecurityService;
import com.lc.gansu.security.domain.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * TODO 参考用户密码的逻辑提供给provider构建上下文用户
 *
 * @author songtianxiong
 * @version 1.0
 * @date 2021/12/20 14:22
 */
@Slf4j
public class SmsUserDetailsService implements UserDetailsService {


    private final SecurityService securityService;

    public SmsUserDetailsService(SecurityService securityService) {
        this.securityService = securityService;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.info("开始加载用户 : {}", s);
        User sysUser = securityService.retrieveSysUserByPhone(s);
        //这里只有BadCredentialsException可以被捕获到,所以给前端的异常信息必须抛出BadCredentialsException
        if (Objects.isNull(sysUser)) throw new BadCredentialsException("帐号不存在,请重新输入!");
        if (sysUser.getState() != 1) throw new BadCredentialsException("账号已停用!");
        if (!sysUser.isEnabled()) {
            throw new DisabledException("账号禁用");
        } else if (!sysUser.isAccountNonLocked()) {
            throw new LockedException("账号锁定");
        } else if (!sysUser.isAccountNonExpired()) {
            throw new AccountExpiredException("账号过期");
        } else if (!sysUser.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException("密码过期");
        }

        Map<Long, List<Role>> sysOrgIdMapSysRoleList = securityService.getOrgIdWithRolesByUserId(sysUser.getId());
        if (sysOrgIdMapSysRoleList.size() > 0) {
            Long orgId = sysOrgIdMapSysRoleList.keySet().iterator().next();
            //加载用户当前的组织
            if (orgId == 0) sysUser.setCurrentOrg(new Org().setId(0L).setName("").setState(1));
            else sysUser.setCurrentOrg(securityService.getOrgById(orgId));
            //加载用户当前组织上的菜单列表
            // List<Permission> privileges = securityService.getMenu(sysUser.getId(), 0L,orgId);
            List<Permission> privileges = securityService.getMenuTree(sysUser.getId(), orgId);
            if (Objects.isNull(privileges) || privileges.size() < 1) throw new UsernameNotFoundException("用户在组织上没有权限");
            sysUser.setCurrentMenuList(privileges);
            //加载用户组织ID及对应的角色
            sysUser.setOrgIdMapRoleList(sysOrgIdMapSysRoleList);
            //加载用户的组织对象列表
            sysUser.setOrgList(sysOrgIdMapSysRoleList.keySet().stream().map(securityService::getOrgById).collect(Collectors.toSet()));
        }
        return sysUser;
    }
}

    //通过手机号码查找用户
    public User retrieveSysUserByPhone(String phone) {
        return securityDao.selectUserByPhone(phone);
    }
    User selectUserByPhone(String phone) {
        String sql = "select * from general_sys_user where phone = ? and state=1";
        Object[] objects = new Object[]{phone};
        try {
            return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), objects);
        } catch (EmptyResultDataAccessException e) {
            return null;
        }
    }
  1. 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
    方法首先能够在使用短信验证码登陆时候被 AuthenticationManager 挑中,其次要在这个类中处理验证逻辑。
package com.lc.gansu.security.smsValidate;

import com.lc.gansu.security.component.SecurityConstants;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 * 方法首先能够在使用短信验证码登陆时候被 AuthenticationManager 挑中,其次要在这个类中处理验证逻辑。
 *
 * 步骤:
 *
 * 实现 AuthenticationProvider 接口,实现 authenticate() 和 supports() 方法。
 * supports() 方法决定了这个 Provider 要怎么被 AuthenticationManager 挑中,我这里通过 return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication),处理所有 SmsCodeAuthenticationToken 及其子类或子接口。
 * authenticate() 方法处理验证逻辑。
 * 首先将 authentication 强转为 SmsCodeAuthenticationToken。
 * 从中取出登录的 principal,也就是手机号。
 * 调用自己写的 checkSmsCode() 方法,进行验证码校验,如果不合法,抛出 AuthenticationException 异常。
 * 如果此时仍然没有异常,通过调用 loadUserByUsername(mobile) 读取出数据库中的用户信息。
 * 如果仍然能够成功读取,没有异常,这里验证就完成了。
 * 重新构造鉴权后的 SmsCodeAuthenticationToken,并返回给 SmsCodeAuthenticationFilter 。
 * SmsCodeAuthenticationFilter 的父类在 doFilter() 方法中处理是否有异常,是否成功,根据处理结果跳转到登录成功/失败逻辑。
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    private SmsUserDetailsService smsUserDetailsService;
    private RedisTemplate redisTemplate;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();

        checkSmsCode(mobile);

        UserDetails userDetails = smsUserDetailsService.loadUserByUsername(mobile);

        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String inputCode = request.getParameter("smsCode");

        if (!redisTemplate.hasKey(SecurityConstants.getValidCodeKey(mobile))) {
            throw new BadCredentialsException("验证码已过期");
        }

        String code = (String) redisTemplate.boundValueOps(SecurityConstants.getValidCodeKey(mobile)).get();
        if (!code.equals(inputCode)) {
            throw new BadCredentialsException("验证码错误");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return smsUserDetailsService;
    }

    public void setUserDetailsService(SmsUserDetailsService smsUserDetailsService, RedisTemplate redisTemplate) {
        this.smsUserDetailsService = smsUserDetailsService;
        this.redisTemplate = redisTemplate;
    }
}

8.配置短信鉴权过滤器琏(要加到系统的安全控件的过滤器琏里)

package com.lc.gansu.security.smsValidate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lc.gansu.security.SecurityService;
import com.lc.gansu.security.component.CustomAuthenticationFailureHandler;
import com.lc.gansu.security.component.jwt.JwtAuthenticationSuccessHandler;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * TODO 配置短信鉴权过滤器琏(要加到系统的安全控件的过滤器琏里)
 */
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Resource
    private ObjectMapper jacksonObjectMapper;
    @Resource
    private SecurityService securityService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(new JwtAuthenticationSuccessHandler(jacksonObjectMapper, redisTemplate));
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(jacksonObjectMapper));

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(new SmsUserDetailsService(securityService), redisTemplate);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

9.成功失败的逻辑还走原来的JwtAuthenticationSuccessHandler和CustomAuthenticationFailureHandler

10.加到过滤器琏里面,放在原有登录逻辑之前

.and().apply(smsCodeAuthenticationSecurityConfig)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值