栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring Security05--手机验证码登录

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring Security05--手机验证码登录

上一篇:https://blog.csdn.net/fengxianaa/article/details/124717243

1. 手机验证码登录

账号密码是最常见的登录方式,但是现在的登录多种多样:手机验证码、二维码、第三方授权等等

下面模仿账号密码登录,新增一下手机验证码登录

1. 获取手机验证码

修改 SecController 增加:

@GetMapping("/phone/code")
public String phoneCode(HttpSession session) throws IOException {
    //验证码配置
    Properties properties = new Properties();
    properties.setProperty("kaptcha.image.width", "150");
    properties.setProperty("kaptcha.image.height", "50");
    properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
    properties.setProperty("kaptcha.textproducer.char.length", "4");
    Config config = new Config(properties);
    DefaultKaptcha kaptcha = new DefaultKaptcha();
    kaptcha.setConfig(config);

    //生成验证码
    String code = kaptcha.createText();
    session.setAttribute("phoneNum", code);
    return code;
}

2. PhoneNumAuthenticationToken

用户名密码登录用的是 UsernamePasswordAuthenticationToken,继承 AbstractAuthenticationToken

我们新建 PhoneNumAuthenticationToken 继承 AbstractAuthenticationToken

public class PhoneNumAuthenticationToken extends AbstractAuthenticationToken {

    private final Object phone;//手机号

    private Object num;//验证码


    public PhoneNumAuthenticationToken(Object phone, Object num) {
        super(null);
        this.phone = phone;
        this.num = num;
        setAuthenticated(false);
    }

    public PhoneNumAuthenticationToken(Object phone, Object num, Collection authorities) {
        super(authorities);
        this.phone = phone;
        this.num = num;
        super.setAuthenticated(true); // must use super, as we override
    }

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

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

3. PhoneNumAuthenticationFilter

参考 UsernamePasswordAuthenticationFilter 写一个过滤器,拦截短信登录接口/phone/login

新建 PhoneNumAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter

public class PhoneNumAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 表示这个 Filter 拦截 /phone/login 接口
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER =
            new AntPathRequestMatcher("/phone/login", "POST");

    // 参数名
    private String phoneParameter = "phone";
    private String numParameter = "num";


    public PhoneNumAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (!"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("请求方式有误: " + request.getMethod());
        }
        //如果请求的参数格式不是json,直接异常
        if (!request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
            throw new AuthenticationServiceException("参数不是json:" + request.getMethod());
        }
        // 用户以json的形式传参的情况下
        String phone = null;
        String num = null;
        try {
            Map map = JSONObject.parseObject(request.getInputStream(),Map.class);
            phone = map.get(phoneParameter);
            num = map.get(numParameter);
        } catch (IOException e) {
            throw new AuthenticationServiceException("参数不对:" + request.getMethod());
        }

        if (phone == null) {
            phone = "";
        }
        if (num == null) {
            num = "";
        }

        phone = phone.trim();
        // 封装手机号、验证码,后面框架会从中拿到 手机号, 调用我们的 LoginPhoneService 获取用户
        PhoneNumAuthenticationToken authRequest
                = new PhoneNumAuthenticationToken(phone, num);
        //设置ip、sessionId信息
        setDetails(request,authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

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

4. LoginPhoneService

新建 LoginPhoneService

@Component
public class LoginPhoneService implements UserDetailsService {

    @Autowired
    private UserService userService;

    
    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        // 从数据库查询用户
        User user = userService.getByPhone(phone);
        if(user == null){
            return null;
        }

        // 把用户信息封装到一个 userdetails 对象中,UserDetails是一个接口,LoginUser实现了这个接口
        LoginUser loginUser = new LoginUser();
        loginUser.setUser(user);
        return loginUser;
    }
}

注意,这里需要修改数据库的user表,增加 phone 字段

5. PhoneAuthenticationProvider

新建 PhoneAuthenticationProvider 实现 AuthenticationProvider 接口,主要实现 authenticate 方法,写我们自己的认证逻辑

@Component
public class PhoneAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private LoginPhoneService loginPhoneService;

    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        PhoneNumAuthenticationToken token = (PhoneNumAuthenticationToken) authentication;
        String phone = (String) authentication.getPrincipal();// 获取手机号
        String num = (String) authentication.getCredentials(); // 获取输入的验证码
        // 1. 从 session 中获取验证码
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String phoneNum = (String) req.getSession().getAttribute("phoneNum");
        if (!StringUtils.hasText(phoneNum)) {
            throw new BadCredentialsException("验证码已经过期,请重新发送验证码");
        }
        if (!phoneNum.equals(num)) {
            throw new BadCredentialsException("验证码不正确");
        }
        // 2. 根据手机号查询用户信息
        LoginUser loginUser = (LoginUser) loginPhoneService.loadUserByUsername(phone);
        if (userDetails == null) {
            throw new BadCredentialsException("用户不存在,请注册");
        }
        // 3. 把用户封装到 PhoneNumAuthenticationToken 中,
        // 后面就可以使用 SecurityContextHolder.getContext().getAuthentication(); 获取当前登陆用户信息
        PhoneNumAuthenticationToken authenticationResult = new PhoneNumAuthenticationToken(phone, num, userDetails.getAuthorities());
        authenticationResult.setDetails(token.getDetails());
        return authenticationResult;
    }

    
    @Override
    public boolean supports(Class authentication) {
        // 如果参数是 PhoneNumAuthenticationToken 类型,返回true
        return (PhoneNumAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

6. 配置 SecurityConfig

下面最重要的,把上面的东西配置到 SecurityConfig 中,让其生效

@Bean
public PhoneNumAuthenticationFilter phoneNumAuthenticationFilter() throws Exception {
    PhoneNumAuthenticationFilter filter = new PhoneNumAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());//认证使用
    //设置登陆成功返回值是json
    filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write(JSONObject.toJSONString(authentication));
        }
    });
    //设置登陆失败返回值是json
    filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            Map map = new HashMap<>();
            map.put("errMsg", "手机登陆失败:"+ exception.getMessage());
            out.write(JSONObject.toJSONString(map));
            out.flush();
            out.close();
        }
    });
    filter.setFilterProcessesUrl("/phone/login");//其实这里不用设置,在 PhoneNumAuthenticationFilter 我们已经定义了一个静态变量
    return filter;
}

@Autowired
private LoginUserService loginUserService;

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
    //对默认的UserDetailsService进行覆盖
    authenticationProvider.setUserDetailsService(loginUserService);
    authenticationProvider.setPasswordEncoder(passwordEncoder());
    return authenticationProvider;
}
@Autowired
private PhoneAuthenticationProvider phoneAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        // /phone/code 请求不用登陆
        .antMatchers("/code","/phone/code").permitAll()
        .anyRequest().authenticated()
        .and().csrf().disable();

    http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        .authenticationProvider(daoAuthenticationProvider());//把账户密码验证加进去

    //把 手机号认证过滤器 加到拦截器链中
    http.addFilterAfter(phoneNumAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        .authenticationProvider(phoneAuthenticationProvider);//把验证逻辑加进去
}

7. 测试

不登录访问 localhost:8080/sec

获取手机验证码

输入错误的验证码

输入正确的,登录成功

8. 优化

其实上面的代码可以优化,根据上面的代码逻辑,我们是先根据手机号拿到用户后,再比较验证码是否正确

根据我们之前账户密码登录的经验,比较验证码是否正确的代码完全可以放到 PhoneNumAuthenticationFilter 中

但是为了模仿账号密码登录的这个过程,我并没有那样做

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/879947.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号