Python 官方文档:入门教程 => 点击学习
目录前言:一、?♂️理论知识二、EmailCodeAuthenticationFilter三、EmailCodeAuthenticationToken四、EmailCode
不知道, 你在用spring Security的时候,有没有想过,用它实现多种登录方式勒,这次我的小伙伴就给我提了一些登录方面的需求,需要在原有账号密码登录的基础上,另外实现电话验证码以及邮件验证码登录,以及在实现之后,让我能够做到实现第三方登录,如gitee、GitHub等。
本文主要是讲解Security在实现账号密码的基础上,并且不改变原有业务情况下,实现邮件、电话验证码登录。
上一篇文章我写了 Security登录详细流程详解有源码有分析。掌握这个登录流程,我们才能更好的做Security的定制操作。
我在写这篇文章之前,也看过很多博主的文章,写的非常好,有对源码方面的解析,也有对一些相关设计理念的理解的文章。
这对于已经学过一段时间,并且对Security已经有了解的小伙伴来说,还是比较合适的,但是对于我以及其他一些急于解决当下问题的小白,并不是那么友善。?
我们先思考一下这个流程大致是如何的?
大致流程就是如此。从这个流程中我们可以知道,需要重写的组件有以下几个:
接下来,我是模仿着源码写出我的代码,建议大家可以在使用的时候,多去看看,我这里去除了一些不是和这个相关的代码。
来吧!!
我们需要重写的 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);
}
@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;
public class EmailCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
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);
}
private boolean postOnly = true;
public EmailCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/email/login","POST"));
}
@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);
}
}
public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public String getEmail(HttpServletRequest request ){
String result= request.getParameter(DEFAULT_EMAIL_NAME);
return result;
}
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是继承AbstractAuthenticationToken的,按照同样的方式,我们接着去看看AbstractAuthenticationToken的默认实现是什么样的就行了。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// 这里指的账号密码哈
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
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;
}
}
日常抄作业哈:
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {
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是实现了AuthenticationProvider接口,抄作业就得学会看看源码。我们接着来。
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();
}
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;
}
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);
}
}
看完源码,其实我们如果要重写的话,主要要做到以下几个事情:
重写public boolean supports(Class<?> authentication)方法。
有时候我们有多个不同的 AuthenticationProvider,它们分别支持不同的 Authentication对象,那么当一个具体的 AuthenticationProvier 传进入 ProviderManager的内部时,就会在 AuthenticationProvider列表中挑选其对应支持的 provider 对相应的 Authentication对象进行验证
简单说就是指定AuthenticationProvider验证哪个 Authentication 对象。如指定DaoAuthenticationProvider认证UsernamePasswordAuthenticationToken,
所以我们指定EmailCodeAuthenticationProvider认证EmailCodeAuthenticationToken。
检索数据库,返回一个安全核心类UserDetail。
创建一个经过身份验证的Authentication对象
了解要做什么事情了,我们就可以动手看看代码啦。
@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());
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);
}
@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文件、配置文件以及相关博客链接,源代码中也加了很多注释,尽最大程度让大家能够看明白。
在最大程度上保证大家都能正确的运行及测试。
源码:gitee-Security
如果这篇存在不太懂的内容,可以先看我的另一篇文章:
SpringBoot集成Security实现安全控制,使用Jwt制作Token令牌。
之后再回过头来看这一篇文章,应该会更加容易理解。
到此这篇关于Spring Security 实现多种登录方式(常规方式外的邮件、手机验证码登录)的文章就介绍到这了,更多相关SpringSecurity 登录 内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
--结束END--
本文标题: SpringSecurity实现多种登录方式(常规方式外的邮件、手机验证码登录)
本文链接: https://lsjlt.com/news/162054.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-03-01
2024-03-01
2024-03-01
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0