在 Spring Security 中定义多种登录方式,比如邮箱、手机验证码登录

这篇具有很好参考价值的文章主要介绍了在 Spring Security 中定义多种登录方式,比如邮箱、手机验证码登录。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

现在各大网站登录的方式是越来越多。比如:传统的用户名密码登录、快捷的邮箱、手机验证码登录,还有流行的第三方登录。那本篇呢,就给大家带来如何在 Spring Security 中定义使用邮箱验证码登录方式。看完本篇,让你学会自定义认证方式,如果公司还需要使用手机验证码登录,简简单单就能集成,毕竟流程是一致的。

编码

自定义验证方式需要使用到 Spring Security 内置的几个对象,如果各位还不了解,可以先看看这篇文章:Spring Security 中重要对象汇总

用户名密码表单登录会进入到 UsernamePasswordAuthenticationFilter。在这整个类中,还会用到一个对象 UsernamePasswordAuthenticationToken

  • AbstractAuthenticationProcessingFilter
  • AbstractAuthenticationToken

EmailVerificationCodeAuthenticationFilter

参考 UsernamePasswordAuthenticationFilter(copy)类并进行删减, 定义 EmailVerificationCodeAuthenticationFilter 类,内容如下:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
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.StringUtils;

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

/**
 * @author cxyxj
 */
public class EmailVerificationCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 默认的请求参数名称
     */
    public static final String EMAIL_KEY = "email";
    public static final String EMAIL_CODE_KEY = "emailCode";

    private String emailParameter = EMAIL_KEY;
    private String emailCodeParameter = EMAIL_CODE_KEY;
    /**
     * 是否仅支持post方式
     */
    private boolean postOnly = true;


    /**
     * 对请求进行过滤,只有接口为 /emil-login,请求方式为 POST,才会进入逻辑
     */
    public EmailVerificationCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/email-login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // 需要是 POST 请求
        if (postOnly &&  !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 判断请求格式是否 JSON
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            Map<String, String> loginData = new HashMap<>(2);
            try {
                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
                throw new InternalAuthenticationServiceException("请求参数异常");
            }
            // 获得请求参数
            String email = loginData.get(emailParameter);
            String emailCode = loginData.get(emailCodeParameter);
            // 检查验证码
            checkEmailCode(emailCode);
            if(StringUtils.isEmpty(email)){
                throw new AuthenticationServiceException("邮箱不能为空");
            }
            /**
             * 使用请求参数传递的邮箱和验证码,封装为一个未认证 EmailVerificationCodeAuthenticationToken 身份认证对象,
             * 然后将该对象交给 AuthenticationManager 进行认证
             */
            EmailVerificationCodeAuthenticationToken authRequest = new EmailVerificationCodeAuthenticationToken(email);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        return null;
    }

    public void setDetails(HttpServletRequest request , EmailVerificationCodeAuthenticationToken token){
        token.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }


    private void checkEmailCode(String emailCode) {
        // 实际当中请从 Redis 中获取
        String verifyCode = "123456";
        if(StringUtils.isEmpty(verifyCode)){
            throw new AuthenticationServiceException("请重新申请验证码!");
        }
        if (!verifyCode.equalsIgnoreCase(emailCode)) {
            throw new AuthenticationServiceException("验证码错误!");
        }
    }
}
复制代码

简单理理上述代码:如下:

  • 提供默认的请求参数名称。
  • 提供无参构造方法,对请求进行过滤。
  • 继承 AbstractAuthenticationProcessingFilter,重写了 attemptAuthentication 方法,对其参数进行校验,最后调用认证管理器的 authenticate 方法。
  • setDetails 方法设置该次请求信息,比如:调用地址、sessionId。

EmailVerificationCodeAuthenticationToken

参考 UsernamePasswordAuthenticationToken(copy) 类并进行删减,定义 EmailVerificationCodeAuthenticationToken 类,内容如下:

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

import java.util.Collection;

/**
 * @author cxyxj
 */
public class EmailVerificationCodeAuthenticationToken  extends AbstractAuthenticationToken {

    private final Object principal;

    public EmailVerificationCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

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


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

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

    @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");
        } else {
            super.setAuthenticated(false);
        }
    }
}
复制代码

可以发现类中并没有 credentials 属性,因为验证码是有时效性的,存着没意义,那 eraseCredentials 方法也没必要重写了,凭证都没有,何需擦除呢! 类中还有两个构造方法,一个是设置是否已认证为 false。另外一个设置是否已认证为 true。

AuthenticationProvider

对于 Provider,我们直接看 AbstractUserDetailsAuthenticationProvider,这是来处理用户名密码认证流程的。AbstractUserDetailsAuthenticationProvider 实现了 AuthenticationProvider 类, 所以重点看看该类的重写的方法。

  • supports:验证传入的身份验证对象是否是 UsernamePasswordAuthenticationToken。如果是则返回 true。
public boolean supports(Class<?> authentication) {
   return (UsernamePasswordAuthenticationToken.class
         .isAssignableFrom(authentication));
}
复制代码
  • authenticate
public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
			() -> messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.onlySupports",
					"Only UsernamePasswordAuthenticationToken is supported"));

	//获取用户名
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();

	boolean cacheWasUsed = true;
	// 根据用户名从缓存中获得UserDetails对象
	UserDetails user = this.userCache.getUserFromCache(username);

	if (user == null) {
		cacheWasUsed = false;

		try {
		// 如果缓存中没有信息,通过子类 DaoAuthenticationProvider 实现的 retrieveUser 方法,返回一个 UserDetails 对象
			user = retrieveUser(username,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (UsernameNotFoundException notFound) {
			logger.debug("User '" + username + "' not found");

			if (hideUserNotFoundExceptions) {
				throw new BadCredentialsException(messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.badCredentials",
						"Bad credentials"));
			}
			else {
				throw notFound;
			}
		}

		Assert.notNull(user,
				"retrieveUser returned null - a violation of the interface contract");
	}

	try {
		// 检查该用户对象的各种状态,比如:账户是否未锁定、账户是否启用、账户是否未过期
		preAuthenticationChecks.check(user);
		// 使用子类 DaoAuthenticationProvider 实现的 additionalAuthenticationChecks方法,检查密码是否输入正确
		additionalAuthenticationChecks(user,
				(UsernamePasswordAuthenticationToken) authentication);
	}
	catch (AuthenticationException exception) {
		if (cacheWasUsed) {
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username,
					(UsernamePasswordAuthenticationToken) authentication);
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		else {
			throw exception;
		}
	}
	// 检查该用户对象的各种状态,比如:凭证(密码)是否未过期
	postAuthenticationChecks.check(user);
	
	// 存入缓存
	if (!cacheWasUsed) {
		this.userCache.putUserInCache(user);
	}

	Object principalToReturn = user;

	if (forcePrincipalAsString) {
		principalToReturn = user.getUsername();
	}

	 // 会调用子类方法,设置是否已认证为true,并设置权限信息
	return createSuccessAuthentication(principalToReturn, authentication, user);
}
复制代码

通过对 authenticate 方法的梳理,我们知道还有两个方法在其子类中进行实现,实现类为 DaoAuthenticationProvider

// 检查密码是否输入正确
protected void additionalAuthenticationChecks(UserDetails userDetails,
		UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	if (authentication.getCredentials() == null) {
		logger.debug("Authentication failed: no credentials provided");

		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}

	String presentedPassword = authentication.getCredentials().toString();

	if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
		logger.debug("Authentication failed: password does not match stored value");

		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}
}

// 通过调用 UserDetailsService 的 loadUserByUsername 方法加载用户信息
protected final UserDetails retrieveUser(String username,
		UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	prepareTimingAttackProtection();
	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) {
		mitigateAgainstTimingAttack(authentication);
		throw ex;
	}
	catch (InternalAuthenticationServiceException ex) {
		throw ex;
	}
	catch (Exception ex) {
		throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
	}
}
复制代码

OK,接下来就是 copy 阶段。我们就没必要学 Security 定义一个抽象类、子类,直接定义一个子类即可。EmailVerificationCodeAuthenticationProvider 实现 AuthenticationProvider

/**
 * 邮箱身份验证提供者
 * @author cxyxj
 */
public class EmailVerificationCodeAuthenticationProvider implements AuthenticationProvider {

    private EmailVerificationCodeService service;

    public EmailVerificationCodeAuthenticationProvider(EmailVerificationCodeService service) {
        this.service = service;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String email = authentication.getName();
        // 根据邮箱加载用户信息
        UserDetails userDetails = service.loadUserByEmail(email);
        if (userDetails == null) {
            // 可以自定义异常类型
            throw new InternalAuthenticationServiceException("邮箱方式登录异常");
        }
        EmailVerificationCodeAuthenticationToken result = new EmailVerificationCodeAuthenticationToken(userDetails,
                userDetails.getAuthorities());
        result.setDetails(authentication.getDetails());
        return result;
    }

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

}
复制代码

再提供一个用来根据邮箱查询用户的服务。

@Service
public class EmailVerificationCodeService {


@Autowired
private SysUserMapper sysUserMapper;

@Autowired
private SysRoleMapper sysRoleMapper;

UserDetails loadUserByEmail(String email) {

    SysUser user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
    if(Objects.isNull(user)){
        return null;
    }
    // 获得用户角色信息
    List<String> roles = sysRoleMapper.selectByRoleId(user.getRoleId());
    // 构建 SimpleGrantedAuthority 对象
    List<SimpleGrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    return new SysUserDetails(user, authorities);
}
}
复制代码

数据库脚本已经放置到项目中,位于 resources/sql 下。

配置

接下来就是将上述的重写的类,配置到 Spring Security 中。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private EmailVerificationCodeService emailVerificationCodeService;

    @Bean
    public EmailVerificationCodeAuthenticationFilter emailVerificationCodeAuthenticationFilter() throws Exception {
        EmailVerificationCodeAuthenticationFilter emailVerificationCodeAuthenticationFilter = new EmailVerificationCodeAuthenticationFilter();
        // 手动设置AuthenticationManager,解决authenticationManager must be specified 启动异常
        emailVerificationCodeAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
        emailVerificationCodeAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        emailVerificationCodeAuthenticationFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return emailVerificationCodeAuthenticationFilter;
    }

    @Bean
    EmailVerificationCodeAuthenticationProvider emailVerificationCodeAuthenticationProvider() {
        EmailVerificationCodeAuthenticationProvider provider = new EmailVerificationCodeAuthenticationProvider(emailVerificationCodeService);
        return provider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 这行需要加上,否则请求参数需要带上 _csrf
        http.cors().and().csrf().disable();

        http.authorizeRequests()  //开启配置
                // 需要放行的接口路径
                .antMatchers().permitAll()
                .anyRequest() //其他请求
                .authenticated(); //验证   表示其他请求需要登录才能访问


       // 将邮箱身份验证过滤器放置在 UsernamePasswordAuthenticationFilter 之后
       http.addFilterBefore(emailVerificationCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       
    // 将自定义的 Provider 添加到 authenticationProviders 属性中
 auth.authenticationProvider(emailVerificationCodeAuthenticationProvider());
    }

}
复制代码

注意:在调用 addFilterBefore、addFilterAt 时,不能随意的在过滤器前后添加,只能在有排序的过滤器前后添加,否则会出现 Cannot register after unregistered Filter 异常。

已有排序的过滤器可以查看 FilterComparator 类。

springsecurity多种登录,spring,servlet,java

接下来就可以来测试一下,我们自定义的方式是否生效了。 在请求工具中请求接口:

POST
http://localhost:8080/email-login

{
    "email":"1990848@163.com",
    "emailCode":"123456"
}
复制代码

springsecurity多种登录,spring,servlet,java

如果小伙伴想断点调试,可以先在 FilterChainProxy 类的 doFilterInternal方法打上一个断点,可以查看 Spring Security 过滤器链中的过滤器。会按照其顺序一个一个执行。

springsecurity多种登录,spring,servlet,java

因为 EmailVerificationCodeAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter,所以会先执行父类逻辑。

springsecurity多种登录,spring,servlet,java

确定请求路径没有错,就会进入对应的Filter逻辑。

springsecurity多种登录,spring,servlet,java

EmailVerificationCodeAuthenticationFilter 类的 attemptAuthentication 方法最后一行会调用 this.getAuthenticationManager().authenticate(authRequest)。 执行 ProviderManager 类的 authenticate 方法。

springsecurity多种登录,spring,servlet,java

可以看到现在 getProviders 中有两个对象,一个是 AnonymousAuthenticationProvider,另外一个就是我们自定义的 EmailVerificationCodeAuthenticationProvider。 循环每个 AuthenticationProvider 对象,调用其 supports 方法进行验证,验证成功,调用其 authenticate 方法,进入到对应的 AuthenticationProvider 实现类。

EmailVerificationCodeAuthenticationProvider

springsecurity多种登录,spring,servlet,java

进入到我们自定义的 Provider 类中,调用 EmailVerificationCodeServiceloadUserByEmail方法。

总结

自定义认证方式总体来说还是很简单的;需要从头重写的也就三个类,然后就是将其配置到Spring Security 中。如果需要再自定义手机号验证码登录,按照上述流程再走一遍就行。其实也是 copy 一份,然后进行小幅度的修改即可。

springsecurity多种登录,spring,servlet,java

开启表单用户名密码登录

如果还需要开启用户名密码登录(JSON形式自行改造吧),需要加上如下配置:文章来源地址https://www.toymoban.com/news/detail-667295.html

@Bean
PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

@Bean
@Override
protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
    manager.createUser(User.withUsername("security").password("security").roles("user").build());
    return manager;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 这行需要加上,否则请求参数需要带上 _csrf
    http.cors().and().csrf().disable();
    // 开启表单配置
    http.formLogin().permitAll();
    http.authorizeRequests()  //开启配置
            // 需要放行的接口路径
            .antMatchers().permitAll()
            .anyRequest() //其他请求
            .authenticated(); //验证   表示其他请求需要登录才能访问

   // 将邮箱身份验证过滤器放置在 UsernamePasswordAuthenticationFilter 之后
   http.addFilterAfter(emailVerificationCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   // 设置 UserDetailsService以及密码匹配器
 auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
   
  // 将自定义的 Provider 添加到 authenticationProviders 属性中
 auth.authenticationProvider(emailVerificationCodeAuthenticationProvider());
}
复制代码

验证


  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞 + 收藏。

到了这里,关于在 Spring Security 中定义多种登录方式,比如邮箱、手机验证码登录的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【深入浅出Spring Security(五)】自定义过滤器进行前后端登录认证

    在【深入浅出Spring Security(二)】Spring Security的实现原理 中小编阐述了默认加载的过滤器,里面有些过滤器有时并不能满足开发中的实际需求,这个时候就需要我们自定义过滤器,然后填入或者替换掉原先存在的过滤器。 首先阐述一下添加过滤器的四个方法(都是 HttpSecur

    2024年02月08日
    浏览(34)
  • Springboot +spring security,实现前后端分离,使用JSON数据格式登录(将表单提交方式改成json格式登录)

    在前面的文章中,我们使用表单方式完成登录提交,但是目前基本都是前后端分离项目,很少使用表单提交的方式,基本都是json方式,使用ajax提交,那么我们怎么将表单提交方式改成json格式登录呢? 通过前面源码部分学习中,已经知道在HttpSecurity配置中,每新增一种配置,

    2024年02月06日
    浏览(37)
  • 溯源!获取ToDesk登录邮箱和手机号

    这篇文章记录的是这次使用ToDesk过程中发现的问题,可以获取ToDesk的登录邮箱和手机号。起因是前同事让我帮她把D盘的容量再分些给C盘,远程给她处理了下,分区助手专业版就能搞定。 关注【 潇湘信安 】、【 Hack分享吧 】公众号,一起学网络安全知识! 原来安装的ToDesk一

    2024年02月09日
    浏览(33)
  • Spring方式发送邮箱

      1.导入依赖 2.导入工具类 2.导入application.yml 4.test类测试

    2024年02月17日
    浏览(44)
  • Spring Security--自动登录

    也就是remember me 在配置链上加一个  然后发送请求时加上:remember-me字段 value值可以为,ture,1,on 我们记住登录后,关掉浏览器再打开,访问一下接口,可以访问,说明记住登录成功了。    因为有的接口可以支持rememberMe认证,有的接口不支持,用上图的方式做区别。 //禁止

    2024年02月08日
    浏览(28)
  • Spring Security多登录页面示例

    在 Web 应用程序开发中,有两个单独的模块是很常见的 - 一个用于管理员用户,一个用于普通用户。每个模块都有一个单独的登录页面,并且可以与相同或不同的身份验证源相关联。换句话说,应用程序为不同类型的用户提供了多个登录页面:管理员和用户,或管理员和客户

    2023年04月23日
    浏览(23)
  • Spring Security进行登录认证和授权

    用户首次登录提交用户名和密码后spring security 的 UsernamePasswordAuthenticationFilter 把用户名密码封装 Authentication 对象 然后内部调用 ProvideManager 的 authenticate 方法进行认证,然后 ProvideManager 进一步通过内部调用 DaoAuthencationPriovider 的 authenticate 方法进行认证 DaoAuthencationPriovider 通过

    2024年02月11日
    浏览(33)
  • Spring Security登录用户数据获取(4)

    登录成功之后,在后续的业务逻辑中,开发者可能还需要获取登录成功的用户对象,如果不使用任何安全管理框架,那么可以将用户信息保存在HttpSession中,以后需要的时候直接从HttpSession中获取数据。在Spring Security中,用户登录信息本质上还是保存在 HttpSession中,但是为了方

    2024年02月03日
    浏览(36)
  • Spring-Security实现登录接口

    Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架 Shiro ,它提供了更丰富的功能,社区资源也比Shiro丰富。 具体介绍和入门看springSecurity入门 在实现之前幺要了解一下登录校验的流程以及 SpringSecurity 的原理以及认证流程 1、登录校验流程 2、 SpringSecurity完

    2024年01月18日
    浏览(37)
  • C#生成自定义登录验证码(微信公众号验证码,邮箱验证码等)

    本文只讲解自定义验证码生成的实现,其他两种见我其他文章。 在项目上线使用时,网络对外警察后受到攻击,特别是尝试各种密码组合对用户名密码登录的暴力破解,需要增加验证码来增加用户信息的安全性。 ps:注意生成的验证码要具备时效性,设置3-5分钟失效,或登录

    2024年02月16日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包