你好丫,我是博主
宁在春
,一起加油吧!!!不知道, 你在用
Spring Security
的时候,有没有想过,用它实现多种登录方式勒,这次我的小伙伴就给我提了一些登录方面的需求,需要在原有账号密码登录的基础上,另外实现电话验证码以及邮件验证码登录,以及在实现之后,让我能够做到实现第三方登录,如gitee
、github
等。本文主要是讲解
Security
在实现账号密码的基础上,并且不改变原有业务情况下,实现邮件、电话验证码登录。
封面:傍晚操场的看到的云
前言:
掌握这个登录流程,我们才能更好的做Security
的定制操作。
我在写这篇文章之前,也看过很多博主的文章,写的非常好,有对源码方面的解析,也有对一些相关设计理念的理解的文章。
这对于已经学过一段时间,并且对Security已经有了解的小伙伴来说,还是比较合适的,但是对于我以及其他一些急于解决当下问题的小白,并不是那么友善。😂
一、理论知识
我们先思考一下这个流程大致是如何的?
- 填写邮件号码,获取验证码
- 输入获取到的验证码进行登录(登录的接口:
/email/login
,这里不能使用默认的/login
,因为我们是扩展) - 在自定义的过滤器
EmailCodeAuthenticationFilter
中获取发送过来的邮件号码及验证码,判断验证码是否正确,邮件账号是否为空等 - 封装成一个需要认证的
Authentication
,此处我们自定义实现为EmailCodeAuthenticationToken
。 - 将 Authentiction 传给 AuthenticationManager 接口中
authenticate
方法进行认证处理 AuthenticationManager
默认是实现类为 ProviderManager ,ProviderManager
又委托给AuthenticationProvider
进行处理- 我们自定义一个
EmailCodeAuthenticationProvider
实现AuthenticationProvider
,实现身份验证。 - 自定义的
EmailCodeAuthenticationFilter
继承了 AbstractAuthenticationProcessingFilter 抽象类,AbstractAuthenticationProcessingFilter
在successfulAuthentication
方法中对登录成功进行了处理,通过SecurityContextHolder.getContext().setAuthentication()
方法将 Authentication 认证信息对象绑定到SecurityContext
即安全上下文中。 - 其实对于身份验证通过后的处理,有两种方案,一种是直接在过滤器重写
successfulAuthentication
,另外一种就是实现AuthenticationSuccessHandler
来处理身份验证通过。 - 身份验证失败也是一样,可重写
unsuccessfulAuthentication
方法,也可以实现AuthenticationFailureHandler
来对身份验证失败进行处理。
大致流程就是如此。从这个流程中我们可以知道,需要重写的组件有以下几个:
EmailCodeAuthenticationFilter
:邮件验证登录过滤器EmailCodeAuthenticationToken
:身份验证令牌EmailCodeAuthenticationProvider
:邮件身份认证处理AuthenticationSuccessHandler
:处理登录成功操作AuthenticationFailureHandler
:处理登录失败操作
接下来,我是模仿着源码写出我的代码,建议大家可以在使用的时候,多去看看,我这里去除了一些不是和这个相关的代码。
来吧!!
二、EmailCodeAuthenticationFilter
我们需要重写的EmailCodeAuthenticationFilter
,实际继承了AbstractAuthenticationProcessingFilter
抽象类,我们不会写,可以先看看它的默认实现UsernamePasswordAuthenticationFilter
是怎么样的吗,抄作业这是大家的强项的哈。
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方法 }
接下来我们就抄个作业哈:
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
的默认实现是什么样的就行了。
/** */ 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; } }
日常抄作业哈:
/** * @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
身份验证请求。但是它是一个抽象类,但其实就一个步骤在它的实现类中实现的,很简单,稍后会讲到。
在这个源码中我把和检查相关的一些操作都给删除,只留下几个重点,我们一起来看一看哈。
//该类旨在响应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
主要操作是两个,第一个是从数据库中检索出相关信息,第二个是给检索出的用户信息进行密码的加密操作。
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、抄作业啦
看完源码,其实我们如果要重写的话,主要要做到以下几个事情:
- 重写
public boolean supports(Class<?> authentication)
方法。
有时候我们有多个不同的AuthenticationProvider
,它们分别支持不同的Authentication
对象,那么当一个具体的AuthenticationProvier
传进入ProviderManager
的内部时,就会在AuthenticationProvider
列表中挑选其对应支持的provider
对相应的Authentication
对象进行验证
简单说就是指定AuthenticationProvider
验证哪个Authentication
对象。如指定DaoAuthenticationProvider
认证UsernamePasswordAuthenticationToken
,
所以我们指定EmailCodeAuthenticationProvider
认证EmailCodeAuthenticationToken
。 - 检索数据库,返回一个安全核心类
UserDetail
。 - 创建一个经过身份验证的
Authentication
对象
了解要做什么事情了,我们就可以动手看看代码啦。
/** * @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); } }
五、在配置类中进行配置
主要就是做下面几件事:
- 将过滤器、认证器注入到
spring
中 - 将登录成功处理、登录失败处理器注入到
Spring
中,或者在自定义过滤器中对登录成功和失败进行处理。 - 添加到过滤链中
@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()); }
.and() .authenticationProvider(emailCodeAuthenticationProvider()) .addFilterBefore(emailCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class) .authenticationProvider(mobileCodeAuthenticationProvider()) .addFilterBefore(mobileCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)
六、测试及源代码
项目具体的配置、启动方式、环境等、都在github
及gitee
的文档上有详细说明。
源代码中包含sql
文件、配置文件以及相关博客链接,源代码中也加了很多注释,尽最大程度让大家能够看明白。
在最大程度上保证大家都能正确的运行及测试。
七、自言自语
如果这篇存在不太懂的内容,可以先看我的另一篇文章:
SpringBoot集成Security实现安全控制,使用Jwt制作Token令牌。
之后再回过头来看这一篇文章,应该会更加容易理解。
我是真的想要教会大家,并非是胡乱写写,主要是想收到来自大家成功后的那种开心,会让人心情愉悦。
相关文章:
- SpringBoot集成SpringSecurity做安全框架
- Security的登录流程详解
- Security实现多种登录方式、邮件验证码、手机验证码登录。
- SpringSecurity权限命名ROLE_问题
今天的文章就到这里了。
你好,我是博主
宁在春
:主页如若在文章中遇到疑惑,请留言或私信,或者加主页联系方式,都会尽快回复。
如若发现文章中存在问题,望你能够指正,不胜感谢。
如果觉得对你有所帮助的话,请点个赞再走吧!