javaWeb学习框架建设收集安全收集

spring boot 之 security(六) 短信验证登陆

2019-10-20  本文已影响0人  _大叔_

账号密码登陆 和 短信验证登陆 是不一样的 写一起不利于维护,所以我们需要写自己的filter来验证登陆。

原理及流程

其实在 SmsAuthenticationFilter 之前还有一层Filter,它是用来检验验证码是否正确的,放在 SmsAuthenticationFilter 之前也是因为可重用,当其他需要短信验证的时候我们则可以直接让该filter替我们校验,无需再写一个。

一、实现自己的 SmsAuthenticationFilter,用于 类似于 UsernamePasswordAuthenticationFilter 的功能

package com.wt.cloud.sms;

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 SmsCodeAuthticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;

    /**
     * 是否只处理 post请求
     */
    private boolean postOnly = true;

    /**
     * 拦截的手机验证码登陆路径
     */
    public SmsCodeAuthticationFilter() {
        super(new AntPathRequestMatcher("/authentication/mobile", "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);

        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 void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Username parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

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

    public final String getMobileParameter() {
        return mobileParameter;
    }
}

很简单就是把 UsernamePasswordAuthenticationFilter 的内容copy过来就行,把 UsernamePasswordAuthenticationFilter 里的username 改成我们的 mobile(页面上手机的参数名),把password去掉,因为手机登陆不需要密码。然后登陆路径修改即可。

二、实现 SmsCodeAuthenticationToken

这个是用来 给 provider 准备的,provider 要用这个来区分我得认证类型,并得到传递的数据。实现方式可以查看 UsernamePasswordAuthenticationToken

package com.wt.cloud.sms;

import lombok.Data;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

@Data
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
    /**
     *  没有认证之前是手机号 认证之后是用户信息
     */
    private final Object principal;
    
    /**
     * 没登陆的时候放的是手机号
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }
    
    @Override
    public Object getCredentials() {
        return null;
    }

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

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

三、实现 SmsCodeAuthenticationProvider

provide 需要先重新定义 校验器,这个校验器是用来判断你的 你所传递的认证的类型。如果是 SmsCodeAuthenticationToken 那就走我得 authenticate方法,authenticate方法就是需获取 UserDetails(UserDetails 是从 userDetailsService.loadUserByUsername获取得到,这里获取了密码,并校验了是否有效等 ),并得到 一个完整的 Authentication。

package com.wt.cloud.sms;

import lombok.Data;
import org.springframework.security.authentication.AuthenticationProvider;
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.security.core.userdetails.UsernameNotFoundException;

@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    /**
     * 获取用户信息 组装Authentication
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
        UserDetails user = userDetailsService.loadUserByUsername((String) smsCodeAuthenticationToken.getPrincipal());
        if(user == null){
            throw new UsernameNotFoundException("无法获取用户信息");
        }
        SmsCodeAuthenticationToken smsAuthentication = new SmsCodeAuthenticationToken(user,user.getAuthorities());
        smsCodeAuthenticationToken.setDetails(smsAuthentication.getDetails());
        return smsAuthentication;
    }

    /**
     * 校验器
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        // 校验类型是否是 SmsCodeAuthenticationToken
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

四、整合所写的provider 和 filter

package com.wt.cloud.sms;


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.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;


@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        SmsCodeAuthticationFilter smsCodeAuthticationFilter = new SmsCodeAuthticationFilter();
        smsCodeAuthticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        smsCodeAuthticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsCodeAuthticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        builder.authenticationProvider(smsCodeAuthenticationProvider)
        .addFilterAfter(smsCodeAuthticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

五、写一个手机验证的过滤校验器

package com.wt.cloud.filter;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.wt.cloud.ValidateException;
import com.wt.cloud.sms.SmsCode;
import com.wt.cloud.validate.ImageCode;
import lombok.Data;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * 功能描述: OncePerRequestFilter spring提供的工具类,保证我们的过滤器始终只会被调一次
 * @author : big uncle
 * @date : 2019/10/14 15:13
 */
@Data
public class ValidateSmsCodeFilter extends OncePerRequestFilter {

    /**
     * 失败处理器
     */
    private AuthenticationFailureHandler authenticationFailureHandler;

    /**
     * spring 的session
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 拦截登陆uri,不是登陆则全部放过
        if(StrUtil.equalsIgnoreCase("/authentication/mobile",httpServletRequest.getRequestURI()) && StrUtil.equalsIgnoreCase(httpServletRequest.getMethod(),"post")){
            try{
                validate(new ServletWebRequest(httpServletRequest));
            }catch(ValidateException v){
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,v);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }

    // 校验
    private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        String mobile = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(),"mobile");
        SmsCode smsCode = (SmsCode) sessionStrategy.getAttribute(servletWebRequest, mobile);
        String code = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(),"mobileCode");
        if(StrUtil.isEmpty(code)){
            throw new ValidateException("验证码为空或不存在");
        }
        if(ObjectUtil.isNull(smsCode)){
            throw new ValidateException("验证码错误");
        }
        if(!StrUtil.equalsIgnoreCase(smsCode.getCode(),code)){
            throw new ValidateException("验证码不匹配");
        }
        if(LocalDateTime.now().isAfter(smsCode.getExpireTime())){
            sessionStrategy.removeAttribute(servletWebRequest, mobile);
            throw new ValidateException("验证码已过期");
        }
        sessionStrategy.removeAttribute(servletWebRequest, mobile);
    }
}

SmsCode

package com.wt.cloud.sms;

import lombok.Data;

import java.time.LocalDateTime;
import java.util.UUID;

@Data
public class SmsCode {

    private String code;

    private LocalDateTime expireTime;

    public SmsCode(String code,int expireIn){
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

    public static SmsCode createCode(){

        String content = UUID.randomUUID().toString().replace("-","").substring(0,4);
        //释放资源
       return new SmsCode(content,60);

    }
}

六、把 SmsCodeAuthenticationSecurityConfig 注册到 WebSecurityConfig

// 校验smsCode的过滤器
ValidateSmsCodeFilter validateSmsCodeFilter = new ValidateSmsCodeFilter(); validateSmsCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

.apply(smsCodeAuthenticationSecurityConfig);

其实就类似于把 SmsCodeAuthenticationSecurityConfig 里的配置,在 WebSecurityConfig 里配置。

package com.wt.cloud.config;

import com.wt.cloud.filter.ValidateCodeFilter;
import com.wt.cloud.filter.ValidateSmsCodeFilter;
import com.wt.cloud.properties.SecurityProperties;
import com.wt.cloud.sms.SmsCodeAuthenticationSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;


/**
 * 功能描述: WebSecurityConfigurerAdapter web安全应用的适配器
 * @author : big uncle
 * @date : 2019/10/10 10:26
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private MyAuthenticationSuccessHandle myAuthenticationSuccessHandle;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        // 设置数据源
        tokenRepository.setDataSource(dataSource);
        // 启动的时候创建存储token的表
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭默认的basic认证拦截
//        http
//        .authorizeRequests()
//        .anyRequest()
//        .permitAll().and()
//        .logout()
//        .permitAll();
        // 让使用form表单认证
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        // 校验smsCode的过滤器
        ValidateSmsCodeFilter validateSmsCodeFilter = new ValidateSmsCodeFilter();
        validateSmsCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

        http
                // 在 UsernamePasswordAuthenticationFilter 之前添加手机验证码过滤器
                .addFilterBefore(validateSmsCodeFilter,UsernamePasswordAuthenticationFilter.class)
                // 在 UsernamePasswordAuthenticationFilter 之前添加图形验证码过滤器
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                // 自定义登陆页面 或 controller
                .loginPage("/web/authentication")
                // 覆盖spring security默认登陆地址。默认是login
                .loginProcessingUrl("/authentication/login")
                // 配置成功处理类
                .successHandler(myAuthenticationSuccessHandle)
                // 配置失败处理类
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                // 记住我
                .rememberMe()
                // 设置存储库
                .tokenRepository(persistentTokenRepository())
                 // 设置token 失效时间
                .tokenValiditySeconds(3600)
                // 获取 userDetailsService 做登陆
                .userDetailsService(userDetailsService)
                .and()
                // 以下都是授权的配置
                .authorizeRequests()
                // 剔除登陆页面的认证拦截,否则会在进登陆页面一直跳转;permitAll 指任何人都可以访问这个url
                .antMatchers(
                        "/web/authentication",
                        "/code/*",
                        "/mobile.html",
                        securityProperties.getWebProperties().getLoginPage()
                ).permitAll()
                // 任何请求
                .anyRequest()
                // 都需要身份认证
                .authenticated()
                .and()
                // 关闭跨站请求伪造拦截
                .csrf().disable()
                // 把 apply 类的配置,加到整体的配置里面
                .apply(smsCodeAuthenticationSecurityConfig);

    }
}

Controller

package com.wt.cloud.validate;

import com.wt.cloud.sms.SmsCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@RestController
@RequestMapping("/code")
@Slf4j
public class ValiDateWeb {

    /**
     * spring 的session
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();


    @GetMapping("/smsCode")
    public void smsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
        SmsCode smsCode = SmsCode.createCode();
        String mobile = ServletRequestUtils.getStringParameter(request,"mobile");
        sessionStrategy.setAttribute(new ServletWebRequest(request),mobile,smsCode);
        // 调用发送手机的方法
        log.info("手机号:{} 验证码:{}",mobile,smsCode.getCode());
    }

}

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/authentication/mobile" method="post">
        <input name="mobile" value="15771720555"> 手机号
        <br>
        <input type="text" name="mobileCode" > 验证码
        <br>
        <a href="/code/smsCode?mobile=15771720555">发送验证码</a>
        <input type="submit" value="登陆" >
    </form>
</body>
</html>
上一篇 下一篇

猜你喜欢

热点阅读