SpringSecurity(六)短信验证码登录

由 SpringSecurity(四)认证流程 我们已经知道了Spring Security用户名和密码的登录流程。仿照用户名和密码登录编写一个短信验证码登录

手机验证码登录流程图

短信验证码

新建一个SmsCode类,里面有三个属性:String code(验证码字符串)、LocalDateTime expireTime(过期时间)、String mobile(手机号)。省略短信验证码的发送过程。大概步骤如下:

1. 用户点击发送验证码按钮

2. 随机生成一个code字符串,新建SmsCode对象,设置验证码字符串code、手机号mobile和过期时间expireTime

3. 将SmsCode对象存入session

实现手机验证码登录

1. 新建 SmsCodeAuthenticationToken,对应用户名密码登录的UsernamePasswordAuthenticationToken

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 500L;
    private final Object principal;

    public SmsCodeAuthenticationToken(Object mobile) {
        super((Collection)null);
        this.principal = mobile;
        this.setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return null;
    }

    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");
        } else {
            super.setAuthenticated(false);
        }
    }

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

2. 新建  SmsCodeAuthenticationFilter,对应用户名密码登录的UsernamePasswordAuthenticationFilter

public class SmsCodeAuthenticationFilter  extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String mobile = this.obtainMobile(request);
            if (mobile == null) {
                mobile = "";
            }
            mobile = mobile.trim();
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.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 this.mobileParameter;
    }
}

3. 新建 SmsCodeAuthenticationProvider,对应用户名密码登录的DaoAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private MyUserService myUserService;

    public MyUserService getMyUserService() {
        return myUserService;
    }

    public void setMyUserService(MyUserService myUserService) {
        this.myUserService = myUserService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken)authentication;
        UserDetails user = myUserService.loadUserByMobile((String)smsCodeAuthenticationToken.getPrincipal());
        if (user == null) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }
        SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(user, user.getAuthorities());
        result.setDetails(smsCodeAuthenticationToken.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
    }
}

4. 修改 MyUserService

@Component
public class MyUserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        
        List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
        LoginUser loginUser = new LoginUser(s,new BCryptPasswordEncoder().encode("123456"),authorityLists);
        loginUser.setNickName("成");
        return loginUser;
    }

    public UserDetails loadUserByMobile(String mobile) throws UsernameNotFoundException {
        //  通过手机号mobile去数据库里查找用户以及用户权限
        List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
        LoginUser loginUser = new LoginUser(mobile,new BCryptPasswordEncoder().encode("123456"),authorityLists);
        loginUser.setNickName("成");
        return loginUser;
    }
}

5. 新建 SmsCodeAuthenticationSecurityConfig

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private MyAuthenticationFailHandler myAuthenticationFailHandler;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyUserService MyUserService;


    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setMyUserService(MyUserService);

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

6.  新建SmsCodeFilter过滤器,用于验证短信验证码是否正确。使用过滤器来验证短信验证码的好处是,可以任意设置哪些地址需要短信验证码验证之后才能访问,不仅仅只使用于登录。

@Component
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private Set<String> urls = new HashSet<>();

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        // 这里配置需要拦截的地址 ......
        urls.add("/authentication/mobile"); //

    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        boolean action = false;
        for (String url : urls) {
            if (antPathMatcher.match(url, httpServletRequest.getRequestURI())) {
                action = true;
                break;
            }
        }
        if (action) {
            try {
                validate(httpServletRequest);
            } catch (SmsCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validate(HttpServletRequest request) {
        SmsCode smsCode = (SmsCode)request.getSession().getAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
        String smsCodeRequest = request.getParameter("smsCode");
        if (smsCodeRequest == null || smsCodeRequest.isEmpty()) {
            throw new SmsCodeException("短信验证码不能为空");
        }
        if (smsCode == null) {
            throw new SmsCodeException("短信验证码不存在");
        }
        if (smsCode.isExpired()) {
            request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
            throw new SmsCodeException("短信验证码已过期");
        }
        if(!smsCodeRequest.equalsIgnoreCase(smsCode.getCode())) {
            throw new SmsCodeException("短信验证码错误");
        }
        if(!smsCode.getMobile().equals(request.getParameter("mobile"))) {
            throw new SmsCodeException("输入的手机号与发送短信验证码的手机号不一致");
        }
        request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
    }
}

7.  SpringSecurityConfig配置文件,将SmsCodeAuthenticationSecurityConfig和SmsCodeFilter注入

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserService myUserService;

    @Autowired
    private MyAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailHandler authenticationFailHandler;

    @Autowired
    private ImageCodeFilter imageCodeFilter;

    @Autowired
    private SmsCodeFilter smsCodeFilter;

    // 注入短信登录的相关配置
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class) // 将ImageCodeFilter过滤器设置在UsernamePasswordAuthenticationFilter之前
                .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/authentication/*","/login/*","/code/*") // 不需要登录就可以访问
                .permitAll()
                .antMatchers("/user/**").hasAnyRole("USER") // 需要具有ROLE_USER角色才能访问
                .antMatchers("/admin/**").hasAnyRole("ADMIN") // 需要具有ROLE_ADMIN角色才能访问
                .anyRequest().authenticated()
                .and()
                    .formLogin()
                    .loginPage("/authentication/login") // 访问需要登录才能访问的页面,如果未登录,会跳转到该地址来
                    .loginProcessingUrl("/authentication/form")
                    .successHandler(authenticationSuccessHandler)
                    .failureHandler(authenticationFailHandler)
                ;
        http.apply(smsCodeAuthenticationSecurityConfig);
    }

    // 密码加密方式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    // 重写方法,自定义用户
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth.inMemoryAuthentication().withUser("lzc").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN","USER");
//        auth.inMemoryAuthentication().withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
        auth.userDetailsService(myUserService); // 注入MyUserService,这样SpringSecurity会调用里面的loadUserByUsername(String s)
    }
}

代码地址    https://github.com/923226145/SpringSecurity/tree/master/chapter5

猜你喜欢

转载自blog.csdn.net/lizc_lizc/article/details/84073498