前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你有没有遇到要实现多种登录方式的场景丫 一起来看看咯 Spring Security 实现多种登录方式,如常规方式外的邮件、手机验证码登录

你有没有遇到要实现多种登录方式的场景丫 一起来看看咯 Spring Security 实现多种登录方式,如常规方式外的邮件、手机验证码登录

作者头像
宁在春
发布2022-10-31 15:42:50
1.3K0
发布2022-10-31 15:42:50
举报

你好丫,我是博主宁在春,一起加油吧!!! 不知道, 你在用Spring Security的时候,有没有想过,用它实现多种登录方式勒,这次我的小伙伴就给我提了一些登录方面的需求,需要在原有账号密码登录的基础上,另外实现电话验证码以及邮件验证码登录,以及在实现之后,让我能够做到实现第三方登录,如giteegithub等。 本文主要是讲解Security在实现账号密码的基础上,并且不改变原有业务情况下,实现邮件、电话验证码登录。

在这里插入图片描述
在这里插入图片描述

前言:

上一篇文章我写了 Security登录详细流程详解有源码有分析。掌握这个登录流程,我们才能更好的做Security的定制操作。

我在写这篇文章之前,也看过很多博主的文章,写的非常好,有对源码方面的解析,也有对一些相关设计理念的理解的文章。

这对于已经学过一段时间,并且对Security已经有了解的小伙伴来说,还是比较合适的,但是对于我以及其他一些急于解决当下问题的小白,并不是那么友善。😂

一、🤸‍♂️理论知识

我们先思考一下这个流程大致是如何的?

  1. 填写邮件号码,获取验证码
  2. 输入获取到的验证码进行登录(登录的接口:/email/login,这里不能使用默认的/login,因为我们是扩展)
  3. 在自定义的过滤器 EmailCodeAuthenticationFilter 中获取发送过来的邮件号码及验证码,判断验证码是否正确,邮件账号是否为空等
  4. 封装成一个需要认证的 Authentication ,此处我们自定义实现为 EmailCodeAuthenticationToken
  5. Authentiction 传给 AuthenticationManager 接口中 authenticate 方法进行认证处理
  6. AuthenticationManager 默认是实现类为 ProviderManager ,ProviderManager 又委托给 AuthenticationProvider 进行处理
  7. 我们自定义一个 EmailCodeAuthenticationProvider 实现 AuthenticationProvider ,实现身份验证。
  8. 自定义的 EmailCodeAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter 抽象类, AbstractAuthenticationProcessingFilter 在 successfulAuthentication 方法中对登录成功进行了处理,通过 SecurityContextHolder.getContext().setAuthentication() 方法将 Authentication 认证信息对象绑定到 SecurityContext即安全上下文中。
  9. 其实对于身份验证通过后的处理,有两种方案,一种是直接在过滤器重写successfulAuthentication,另外一种就是实现AuthenticationSuccessHandler来处理身份验证通过。
  10. 身份验证失败也是一样,可重写unsuccessfulAuthentication方法,也可以实现 AuthenticationFailureHandler来对身份验证失败进行处理。

大致流程就是如此。从这个流程中我们可以知道,需要重写的组件有以下几个:

  1. EmailCodeAuthenticationFilter:邮件验证登录过滤器
  2. EmailCodeAuthenticationToken:身份验证令牌
  3. EmailCodeAuthenticationProvider:邮件身份认证处理
  4. AuthenticationSuccessHandler:处理登录成功操作
  5. AuthenticationFailureHandler:处理登录失败操作

接下来,我是模仿着源码写出我的代码,建议大家可以在使用的时候,多去看看,我这里去除了一些不是和这个相关的代码。

来吧!!

二、EmailCodeAuthenticationFilter

我们需要重写的 EmailCodeAuthenticationFilter,实际继承了AbstractAuthenticationProcessingFilter抽象类,我们不会写,可以先看看它的默认实现UsernamePasswordAuthenticationFilter是怎么样的吗,抄作业这是大家的强项的哈。

代码语言:javascript
复制
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
			"POST");
	//从前台传过来的参数
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

	private boolean postOnly = true;
	
    //  初始化一个用户密码 认证过滤器  默认的登录uri 是 /login 请求方式是POST
	public UsernamePasswordAuthenticationFilter() {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

	public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
	}

    /**
    执行实际身份验证。实现应执行以下操作之一:
	1、为经过身份验证的用户返回填充的身份验证令牌,表示身份验证成功
	2、返回null,表示认证过程还在进行中。 在返回之前,实现应该执行完成流程所需的任何额外工作。
	3、如果身份验证过程失败,则抛出AuthenticationException
    */
	@Override
	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());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
        //生成 UsernamePasswordAuthenticationToken 稍后交由AuthenticationManager中的authenticate进行认证
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// 可以放一些其他信息进去
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	@Nullable
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(this.passwordParameter);
	}

	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(this.usernameParameter);
	}

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

	//set、get方法
}

接下来我们就抄个作业哈:

代码语言:javascript
复制
package com.crush.security.auth.email_code;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
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.security.web.util.matcher.RequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;

/**
 * @Author: crush
 * @Date: 2021-09-08 21:13
 * version 1.0
 */
public class EmailCodeAuthenticationFilter  extends AbstractAuthenticationProcessingFilter {
    /**
     * 前端传来的 参数名 - 用于request.getParameter 获取
     */
    private final String DEFAULT_EMAIL_NAME="email";

    private final String DEFAULT_EMAIL_CODE="e_code";

    @Autowired
    @Override
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }
    /**
     * 是否 仅仅post方式
     */
    private boolean postOnly = true;

    /**
     * 通过 传入的 参数 创建 匹配器
     * 即 Filter过滤的url
     */
    public EmailCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/email/login","POST"));
    }


    /**
     * filter 获得 用户名(邮箱) 和 密码(验证码) 装配到 token 上 ,
     * 然后把token 交给 provider 进行授权
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if(postOnly && !request.getMethod().equals("POST") ){
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }else{
            String email = getEmail(request);
            if(email == null){
                email = "";
            }
            email = email.trim();
            //如果 验证码不相等 故意让token出错 然后走springsecurity 错误的流程
            boolean flag = checkCode(request);
            //封装 token
            EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>());
            this.setDetails(request,token);
            //交给 manager 发证
            return this.getAuthenticationManager().authenticate(token);
        }
    }

    /**
     * 获取 头部信息 让合适的provider 来验证他
     */
    public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){
        token.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    /**
     * 获取 传来 的Email信息
     */
    public String getEmail(HttpServletRequest request ){
        String result=  request.getParameter(DEFAULT_EMAIL_NAME);
        return result;
    }

    /**
     * 判断 传来的 验证码信息 以及 session 中的验证码信息
     */
    public boolean checkCode(HttpServletRequest request ){
        String code1 = request.getParameter(DEFAULT_EMAIL_CODE);
        System.out.println("code1**********"+code1);
        // TODO 另外再写一个链接 生成 验证码 那个验证码 在生成的时候  存进redis 中去
        //TODO  这里的验证码 写在Redis中, 到时候取出来判断即可 验证之后 删除验证码
        if(code1.equals("123456")){
            return true;
        }
        return false;
    }
	// set、get方法...
}

三、🤖EmailCodeAuthenticationToken

我们EmailCodeAuthenticationToken是继承AbstractAuthenticationToken的,按照同样的方式,我们接着去看看AbstractAuthenticationToken的默认实现是什么样的就行了。

代码语言:javascript
复制
/**

 */
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // 这里指的账号密码哈
	private final Object principal;

	private Object credentials;

	/**
	没经过身份验证时,初始化权限为空,setAuthenticated(false)设置为不可信令牌
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	/**
	经过身份验证后,将权限放进去,setAuthenticated(true)设置为可信令牌
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}

	@Override
	public Object getCredentials() {
		return this.credentials;
	}

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

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
		Assert.isTrue(!isAuthenticated,
				"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
		this.credentials = null;
	}

}

日常抄作业哈:

代码语言:javascript
复制
/**
 * @Author: crush
 * @Date: 2021-09-08 21:13
 * version 1.0
 */
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {


    /**
     * 这里的 principal 指的是 email 地址(未认证的时候)
     */
    private final Object principal;

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

    public EmailCodeAuthenticationToken(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);
        }
    }

}

这个很简单的哈。👨‍💻

四、EmailCodeAuthenticationProvider

自定义的EmailCodeAuthenticationProvider是实现了AuthenticationProvider接口,抄作业就得学会看看源码。我们接着来。

4.1、先看看AbstractUserDetailsAuthenticationProvider,我们再来模仿

AuthenticationProvider 接口有很多实现类,不一一说明了,直接看我们需要看的AbstractUserDetailsAuthenticationProvider, 该类旨在响应 UsernamePasswordAuthenticationToken 身份验证请求。但是它是一个抽象类,但其实就一个步骤在它的实现类中实现的,很简单,稍后会讲到。

在这个源码中我把和检查相关的一些操作都给删除,只留下几个重点,我们一起来看一看哈。

代码语言:javascript
复制
//该类旨在响应UsernamePasswordAuthenticationToken身份验证请求。
public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {

	protected final Log logger = LogFactory.getLog(getClass());

	private UserCache userCache = new NullUserCache();

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
        //获取用户名
		String username = determineUsername(authentication);
		//判断缓存中是否存在
        boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                // 缓存中没有 通过字类实现的retrieveUser 从数据库进行检索,返回一个 UserDetails 对象
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
            //进行相关检查  因为可能是从缓存中取出来的 并非是最新的
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
            // 没有通过检查, 重新检索最新的数据
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
        // 再次进行检查
		this.postAuthenticationChecks.check(user);
		// 存进缓存中去
        if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        //创建一个可信的身份令牌返回
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	private String determineUsername(Authentication authentication) {
		return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
	}

	/**
简而言之就是创建了一个通过身份验证的UsernamePasswordAuthenticationToken
	 */
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		this.logger.debug("Authenticated user");
		return result;
	}


	/**
允许子类从特定于实现的位置实际检索UserDetails ,如果提供的凭据不正确,则可以选择立即抛出AuthenticationException (如果需要以用户身份绑定到资源以获得或生成一个UserDetails )
	 */
	protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;
	//...
	

    //简而言之:当然有时候我们有多个不同的 `AuthenticationProvider`,它们分别支持不同的 `Authentication`对象,那么当一个具体的 `AuthenticationProvier`传进入 `ProviderManager`的内部时,就会在 `AuthenticationProvider`列表中挑选其对应支持的provider对相应的 Authentication对象进行验证
	@Override
	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
	}

}

关于 protected abstract UserDetails retrieveUser 的实现,AbstractUserDetailsAuthenticationProvider实现是DaoAuthenticationProvider.

DaoAuthenticationProvider主要操作是两个,第一个是从数据库中检索出相关信息,第二个是给检索出的用户信息进行密码的加密操作。

代码语言:javascript
复制
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	private UserDetailsService userDetailsService;
    
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            // 检索用户,一般我们都会实现 UserDetailsService接口,改为从数据库中检索用户信息 返回安全核心类 UserDetails
			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);
		}
	}

	@Override
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
        // 判断是否用了密码加密 针对这个点 没有深入 大家好奇可以去查一查这个知识点
		boolean upgradeEncoding = this.userDetailsPasswordService != null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user);
	}

}

4.2、抄作业啦

看完源码,其实我们如果要重写的话,主要要做到以下几个事情:

  1. 重写public boolean supports(Class<?> authentication)方法。 有时候我们有多个不同的 AuthenticationProvider,它们分别支持不同的 Authentication对象,那么当一个具体的 AuthenticationProvier 传进入 ProviderManager的内部时,就会在 AuthenticationProvider列表中挑选其对应支持的 provider 对相应的 Authentication对象进行验证 简单说就是指定AuthenticationProvider验证哪个 Authentication 对象。如指定DaoAuthenticationProvider认证UsernamePasswordAuthenticationToken, 所以我们指定EmailCodeAuthenticationProvider认证EmailCodeAuthenticationToken
  2. 检索数据库,返回一个安全核心类UserDetail
  3. 创建一个经过身份验证的Authentication对象

了解要做什么事情了,我们就可以动手看看代码啦。

代码语言:javascript
复制
/**
 * @Author: crush
 * @Date: 2021-09-08 21:14
 * version 1.0
 */
@Slf4j
public class EmailCodeAuthenticationProvider implements AuthenticationProvider {

    ITbUserService userService;

    public EmailCodeAuthenticationProvider(ITbUserService userService) {
        this.userService = userService;
    }


    /**
     * 认证
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (!supports(authentication.getClass())) {
            return null;
        }
        log.info("EmailCodeAuthentication authentication request: %s", authentication);
        EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;

        UserDetails user = userService.getByEmail((String) token.getPrincipal());

        System.out.println(token.getPrincipal());
        if (user == null) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }
        System.out.println(user.getAuthorities());
        EmailCodeAuthenticationToken result =
                new EmailCodeAuthenticationToken(user, user.getAuthorities());
                /*
                Details 中包含了 ip地址、 sessionId 等等属性 也可以存储一些自己想要放进去的内容
                */
        result.setDetails(token.getDetails());
        return result;
    }

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

五、在配置类中进行配置

主要就是做下面几件事:

  1. 将过滤器、认证器注入到spring
  2. 将登录成功处理、登录失败处理器注入到Spring中,或者在自定义过滤器中对登录成功和失败进行处理。
  3. 添加到过滤链中
代码语言:javascript
复制
    @Bean
    public EmailCodeAuthenticationFilter emailCodeAuthenticationFilter() {
        EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter();
        emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        emailCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        return emailCodeAuthenticationFilter;
    }

    @Bean
    public EmailCodeAuthenticationProvider emailCodeAuthenticationProvider() {
        return new EmailCodeAuthenticationProvider(userService);
    }

    /**
     * 因为使用了BCryptPasswordEncoder来进行密码的加密,所以身份验证的时候也的用他来判断哈、,
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
        //authenticationProvider 根据传入的自定义AuthenticationProvider添加身份AuthenticationProvider 。
        auth.authenticationProvider(emailCodeAuthenticationProvider());
    }
代码语言:javascript
复制
.and()
    .authenticationProvider(emailCodeAuthenticationProvider())
    .addFilterBefore(emailCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)

    .authenticationProvider(mobileCodeAuthenticationProvider())
    .addFilterBefore(mobileCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)

六、测试及源代码

项目具体的配置、启动方式、环境等、都在githubgitee的文档上有详细说明。

源代码中包含sql文件、配置文件以及相关博客链接,源代码中也加了很多注释,尽最大程度让大家能够看明白。

在最大程度上保证大家都能正确的运行及测试。 源码:gitee-Security

七、自言自语

如果这篇存在不太懂的内容,可以先看我的另一篇文章:

SpringBoot集成Security实现安全控制,使用Jwt制作Token令牌。

之后再回过头来看这一篇文章,应该会更加容易理解。

我是真的想要教会大家,并非是胡乱写写,主要是想收到来自大家成功后的那种开心,会让人心情愉悦。

今天的文章就到这里了。 你好,我是博主宁在春CSDN主页 如若在文章中遇到疑惑,请留言或私信,或者加主页联系方式,都会尽快回复。 如若发现文章中存在问题,望你能够指正,不胜感谢。 如果觉得对你有所帮助的话,请点个赞再走吧!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 一、🤸‍♂️理论知识
  • 二、EmailCodeAuthenticationFilter
  • 三、🤖EmailCodeAuthenticationToken
  • 四、EmailCodeAuthenticationProvider
    • 4.1、先看看AbstractUserDetailsAuthenticationProvider,我们再来模仿
      • 4.2、抄作业啦
      • 五、在配置类中进行配置
      • 六、测试及源代码
      • 七、自言自语
      相关产品与服务
      验证码
      腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档