Spring Security实现手机号和验证码认证

概要

Spring Security 是一个很常用的安全框架,当然老外写的框架很多时候还是不会适应咱们的国情,比如现在的登录,手机号加验证码才是主流,毕竟密码太多,谁都会忘。而其默认的认证方式还是username 加 password的方式:UsernamePasswordAuthenticationToken。本文讲述了如何使用解决手机号和验证码的方式完成认证。

参考资料

这里我参考几篇文章,不过大部分的文章还在用WebSecurityConfigurerAdapter ,这个已经不适用于最新的SpringSecurity了,还有就是一般会让你写token,provider,和filter三个文件。
SpringSecurity自定义实现手机短信登录
Spring Security学习(六)——配置多个Provider(存在两种认证规则)

整体架构流程

本文会相对来说比较简单,同时本文建立在第四节 springsecurity结合jwt实现前后端分离开发的基础上进行开发的。

  1. 创建一个SmsAuthenticationProvider
  2. `ProviderManager``中配置两个provider

SmsAuthenticationProvider

首先我们参考DaoAuthenticationProvider写一个针对手机号验证码的认证提供者。我们需要继承AbstractUserDetailsAuthenticationProvider ,然后实现retrieveUser和additionalAuthenticationChecks,第一个方法其实基本不用动,核心就是在第二个方法里面对验证码进行判断

package com.sbvadmin.config;

import com.jayway.jsonpath.Criteria;
import com.sbvadmin.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
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;
import org.springframework.stereotype.Component;

@Component
public class SmsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    private UserServiceImpl userService;

    @Autowired
    RedisTemplate redisTemplate;

    private UserDetailsService userDetailsService;
    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        String presentedCode = authentication.getCredentials().toString(); // 待匹配的验证码
        Object codeInCache = redisTemplate.opsForValue().get(userDetails.getUsername()); // 短信生成的验证码
        if (codeInCache == null){
            this.logger.info("验证码已经失效");
            throw new BadCredentialsException("验证码已经失效");
        }
        if (!codeInCache.toString().equals(presentedCode)){
            this.logger.info("验证码错误");
            throw new BadCredentialsException("验证码错误");
        }
    }
}

ProviderManager中配置两个provider

     @Bean
     DaoAuthenticationProvider daoAuthenticationProvider(){
         DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
         daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
         daoAuthenticationProvider.setUserDetailsService(userServiceDetails);
         return daoAuthenticationProvider;
     }
    @Bean
     protected AuthenticationManager authenticationManager() throws Exception {
        smsAuthenticationProvider.setUserDetailsService(userServiceDetails);
        // 加入两个provider
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(daoAuthenticationProvider(),smsAuthenticationProvider));
        authenticationManager.setEraseCredentialsAfterAuthentication(false);
        return authenticationManager;
     }

小结

其实这里的核心就是看懂getAuthenticationManager().authenticate执行认证时候的代码,里面对provider进行了循环认证,默认的只有daoAuthenticationProvider一个,我们再加一个smsAuthenticationProvider的provider就能很好的解决这个问题,然后登录接口地址也不变,十分方便。唯一不足的就是提示方面弱一些,统一会提示用户名或密码或验证码错误。
代码地址:https://github.com/billyshen26/sbvadmin

  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Security可以很方便地实现手机验证码登录,步骤如下: 1. 添加spring-security-web和spring-security-config依赖。 ```xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring-security.version}</version> </dependency> ``` 2. 创建一个实现UserDetailsService接口的类,该类用于根据不同的用户名加载用户信息。 ```java @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户名不存在"); } return new UserPrincipal(user); } } ``` 3. 创建一个实现AuthenticationProvider接口的类,该类用于验证用户的手机号验证码是否正确。 ```java @Service public class SmsCodeAuthenticationProvider implements AuthenticationProvider { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; String mobile = authenticationToken.getPrincipal().toString(); String code = authenticationToken.getCredentials().toString(); String redisCode = redisTemplate.opsForValue().get(SmsCodeAuthenticationFilter.REDIS_SMS_CODE_KEY_PREFIX + mobile); if (StringUtils.isBlank(redisCode)) { throw new BadCredentialsException("验证码不存在或已过期"); } if (!StringUtils.equals(code, redisCode)) { throw new BadCredentialsException("验证码不正确"); } UserDetails userDetails = new UserPrincipal(new User(mobile, "", Collections.emptyList())); return new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } } ``` 4. 创建一个实现AuthenticationFilter接口的类,该类用于处理短信验证码登录请求。 ```java public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; public static final String SPRING_SECURITY_FORM_CODE_KEY = "code"; public static final String REDIS_SMS_CODE_KEY_PREFIX = "SMS_CODE_"; private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY; private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/login/mobile", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("不支持的请求方式: " + request.getMethod()); } String mobile = obtainMobile(request); String code = obtainCode(request); if (StringUtils.isBlank(mobile)) { throw new UsernameNotFoundException("手机号不能为空"); } if (StringUtils.isBlank(code)) { throw new BadCredentialsException("验证码不能为空"); } SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, code); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected String obtainCode(HttpServletRequest request) { return request.getParameter(codeParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String mobileParameter) { this.mobileParameter = mobileParameter; } public void setCodeParameter(String codeParameter) { this.codeParameter = codeParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } public final String getCodeParameter() { return codeParameter; } } ``` 5. 在WebSecurityConfigurerAdapter的子类中进行配置。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login/mobile").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login").defaultSuccessURL("/home").permitAll() .and() .logout().logoutUrl("/logout").permitAll() .and() .addFilterBefore(smsCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(smsCodeAuthenticationProvider) .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SmsCodeAuthenticationFilter smsCodeAuthenticationFilter() { SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); filter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.write("{\"code\": 0, \"message\": \"登录成功\"}"); out.flush(); out.close(); }); filter.setAuthenticationFailureHandler((request, response, exception) -> { response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.write("{\"code\": 1, \"message\": \"" + exception.getMessage() + "\"}"); out.flush(); out.close(); }); return filter; } } ``` 6. 在前端页面中添加短信验证码登录的表单。 ```html <form action="/login/mobile" method="post"> <div> <label>手机号:</label> <input type="text" name="mobile" /> </div> <div> <label>验证码:</label> <input type="text" name="code" /> <button type="button" onclick="sendSmsCode()">发送验证码</button> </div> <div> <button type="submit">登录</button> </div> </form> ``` 7. 在后端控制器中添加发送短信验证码的接口。 ```java @RestController public class SmsCodeController { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping("/sms/code") public void sendSmsCode(String mobile) { String code = RandomStringUtils.randomNumeric(6); redisTemplate.opsForValue().set(SmsCodeAuthenticationFilter.REDIS_SMS_CODE_KEY_PREFIX + mobile, code, 5, TimeUnit.MINUTES); // 发送短信验证码 } } ``` 以上就是使用Spring Security实现手机验证码登录的全部步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

F_angT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值