spring security 短信验证码登录

短信登录过滤器  SmsAuthenticationFilter 

import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
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;


/**
 * 短信登录过滤器
 */
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 设置拦截/sms/login短信登录接口
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
    // 认证参数
    private String principalParameter = "phone"; //对应手机号
    private String credentialsParameter = "code"; //对应验证码
    private boolean postOnly = true;


    public SmsAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

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

            String phone = this.obtainPrincipal(request);
            phone = phone != null ? phone : "";
            phone = phone.trim();
            String code = this.obtainCredentials(request);
            code = code != null ? code : "";

            SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, code);
            this.setDetails(request, authRequest);
            // 认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainCredentials(HttpServletRequest request) {
        return request.getParameter(this.credentialsParameter);
    }

    @Nullable
    protected String obtainPrincipal(HttpServletRequest request) {
        return request.getParameter(this.principalParameter);
    }


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


    public void setPrincipalParameter(String principalParameter) {
        Assert.hasText(principalParameter, "principal parameter must not be empty or null");
        this.principalParameter = principalParameter;
    }

    public void setCredentialsParameter(String credentialsParameter) {
        Assert.hasText(credentialsParameter, "credentials parameter must not be empty or null");
        this.credentialsParameter = credentialsParameter;
    }

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

    public final String getPrincipalParameter() {
        return this.principalParameter;
    }

    public final String getCredentialsParameter() {
        return this.credentialsParameter;
    }

}

 短信登录令牌        SmsAuthenticationToken

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

import java.util.Collection;

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


    private static final long serialVersionUID = 1L;

    private final Object phone;
    private Object code;

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

    public SmsAuthenticationToken(Object phone, Object code, Collection authorities) {
        super(authorities);
        this.phone = phone;
        this.code = code;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.code;
    }


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

    @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();
        this.code = null;
    }
}

 短信登录校验器

import cn.hutool.core.util.StrUtil;
import com.ruoyi.common.constant.Constants;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Principal;

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

    private SmsUserDetailsService smsUserDetailsService;

    private RedisTemplate redisTemplate;

    public SmsAuthenticationProvider(@Qualifier("smsUserDetailsService") SmsUserDetailsService smsUserDetailsService, RedisTemplate redisTemplate) {
        this.smsUserDetailsService = smsUserDetailsService;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
        String mobile = "";
        if (principal instanceof UserDetails) {
            mobile = ((UserDetails) principal).getUsername();
        } else if (principal instanceof AuthenticatedPrincipal) {
            mobile = ((AuthenticatedPrincipal) principal).getName();
        } else if (principal instanceof Principal) {
            mobile = ((Principal) principal).getName();
        } else {
            mobile = principal == null ? "" : principal.toString();
        }
        String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码
        // 1. 根据手机号查询用户信息
        UserDetails userDetails = smsUserDetailsService.loadUserByUsername(mobile);
        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("用户不存在");
        }
        // 2. 检验Redis手机号的验证码
        String verifyKey = Constants.PHONE_CODE_KEY + mobile;
        String redisCode = redisTemplate.opsForValue().get(verifyKey);
        if (StrUtil.isEmpty(redisCode)) {
            throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
        }
        if (!inputCode.equals(redisCode)) {
            throw new BadCredentialsException("输入的验证码不正确,请重新输入");
        }
        redisTemplate.delete(verifyKey);//用完即删
        // 3. 重新创建已认证对象,
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(principal, inputCode, userDetails.getAuthorities());
        authenticationResult.setDetails(userDetails);
        return authenticationResult;
    }

    @Override
    public boolean supports(Class aClass) {
        return SmsAuthenticationToken.class.isAssignableFrom(aClass);
    }


}

 查询短信登录信息并封装为UserDetails

import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * 查询短信登录信息并封装为UserDetails  这里可以抽取一个抽象类,权限加载和校验租户等逻辑交给父类处理
 */
@Service("smsUserDetailsService")
public class SmsUserDetailsService implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(SmsUserDetailsService.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByPhone(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));
    }
}

 适配器 SmsSecurityConfigurerAdapter

import com.ruoyi.framework.sms.handle.FailAuthenticationHandler;
import com.ruoyi.framework.sms.handle.SuccessAuthenticationHandler;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * Author: LiuLin
 * Date: 2022/5/27 16:25
 * Description:
 */
@Component
public class SmsSecurityConfigurerAdapter extends SecurityConfigurerAdapter {
    @Autowired
    private SuccessAuthenticationHandler successHandler;
    @Autowired
    private FailAuthenticationHandler failureHandler;
    @Autowired
    private SmsAuthenticationProvider smsAuthenticationProvider;


    @Override
    public void configure(HttpSecurity builder) throws Exception {

        SmsAuthenticationFilter filter = new SmsAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(successHandler);
        filter.setAuthenticationFailureHandler(failureHandler);
        builder.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);


        AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
        filter.setAuthenticationManager(authenticationManager);

        builder.authenticationProvider(smsAuthenticationProvider);

        super.configure(builder);

    }
}

登录失败

import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Author: LiuLin
 * Date: 2022/5/30 10:19
 * Description:
 */

@Component
public class FailAuthenticationHandler extends SimpleUrlAuthenticationFailureHandler {


    @Autowired
    private ISysUserService userService;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {

        String mobile = request.getParameter("phone");
        SysUser sysUser = userService.selectUserByPhone(mobile);
        if (sysUser == null) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor("未知", Constants.LOGIN_FAIL, "手机号:" + mobile + "不存在"));
        } else {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(sysUser.getUserName(), Constants.LOGIN_FAIL, exception.getMessage()));
        }

        JSONObject res = new JSONObject();//返回前端数据
        res.put("code", com.ruoyi.common.constant.HttpStatus.ERROR);
        res.put("msg", exception.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(String.valueOf(res));

    }

}

登录成功

import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


/**
 * Author: LiuLin
 * Date: 2022/5/30 10:18
 * Description:
 */

@Component
public class SuccessAuthenticationHandler extends SavedRequestAwareAuthenticationSuccessHandler {


    @Autowired
    private TokenService tokenService;

    @Autowired
    private ISysUserService userService;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {


        LoginUser loginUser = (LoginUser) authentication.getDetails();
        recordLoginInfo(loginUser.getUserId());

        AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));

        JSONObject res = new JSONObject();//返回前端数据
        res.put("msg", "操作成功");
        res.put("code", HttpStatus.SUCCESS);
        res.put("token", tokenService.createToken(loginUser));
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(String.valueOf(res));
        //response.getWriter().write(objectMapper.writeValueAsString(res));

    }

    public void recordLoginInfo(Long userId) {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
        sysUser.setLoginDate(DateUtils.getNowDate());
        userService.updateUserProfile(sysUser);
    }

}

spring security配置

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
 
    @Autowired
    private SmsSecurityConfigurerAdapter smsSecurityConfigurerAdapter;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 基于token,所以不需要session                        
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/sms/login", "/sendCode").anonymous()              
        // 添加手机号短信登录
        httpSecurity.apply(smsSecurityConfigurerAdapter);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

}

你可能感兴趣的:(spring)