前言

  • 源码讲解
  • 源码分析
    • AbstractAuthenticationProcessingFilter.java
    • UsernamePasswordAuthenticationFilter.java
    • ProviderManager
    • AbstractUserDetailsAuthenticationProvider.java
    • DaoAuthenticationProvider
    • AbstractAuthenticationProcessingFilter.java
    • AbstractRememberMeServices.java
    • PersistentTokenBasedRememberMeServices.java
  • 实现短信验证码
    • 前言
    • 增加发送验证码功能
    • 增加验证码逻辑流程(重点)

本章较为复杂,自定义 Spring Security 组件比较麻烦,必须对源码有清晰的了解。需要明细源码在表单验证时候,都经过了哪些过滤器与哪些类,并且都做了什么。所以先对源码进行解析。
在了解源码之后,会开始自定义个别组件,然后整合给 Spring Security 管理器,然后再经过过滤器时候走自己定义的。
如果明白流程,直接可以跳过这一章。

源码讲解

在看源码前要先明白表单验证时候经过了什么东西,以下是执行流程

  1. AbstractAuthenticationProcessingFilter.java:身份验证过滤器,判断是走身份验证过滤器,还是不走。如果验证成功走什么逻辑,失败走什么逻辑。

  2. UsernamePasswordAuthenticationFilter.java:获取用户与密码,封装 Token 开始验证

  3. ProviderManager:认证提供者管理器,找到 Token 与之对应的 Provider。因为 ProviderManager 里面会有多个 Provide

  4. DaoAuthenticationProvider 继承了 AbstractUserDetailsAuthenticationProvider.java,实现类,走了默认的 Provider

  5. 在 DaoAuthenticationProvider 验证完毕后,完成验证。这里假设验证成功

  6. 返回 AbstractAuthenticationProcessingFilter.java (第一步的),执行 successfulAuthentication(xxx) 方法,执行成功后的处理,里面主要会走 “记住我” 功能 与 成功后的 Handler。

  7. AbstractRememberMeServices.java:执行 “记住我” 业务类,判断是否开启了记住我,然后执行记住我逻辑

  8. PersistentTokenBasedRememberMeServices.java:,存入数据库并且将 cookie 写到 响应里

ok,以上为主要执行的流程,下面开始源码分析

源码分析

AbstractAuthenticationProcessingFilter.java

身份验证过滤器,判断是走身份验证过滤器,还是不走。如果验证成功走什么逻辑,失败走什么逻辑。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;//判断此url是否需要身份验证,进入if表示不需要if (!requiresAuthentication(request, response)) {chain.doFilter(request, response);return;}if (logger.isDebugEnabled()) {logger.debug("Request is to process authentication");}Authentication authResult;try {//走身份认证,在示例中实现了 UserDetailsService,那么则会走自己定义的authResult = attemptAuthentication(request, response);if (authResult == null) {// return immediately as subclass has indicated that it hasn't completed// authenticationreturn;}sessionStrategy.onAuthentication(authResult, request, response);}catch (InternalAuthenticationServiceException failed) {logger.error("An internal error occurred while trying to authenticate the user.",failed);//如果出异常,那就执行异常后的处理,目前走的是自定义的 CustomAuthenticationFailureHandlerunsuccessfulAuthentication(request, response, failed);return;}catch (AuthenticationException failed) {// Authentication failedunsuccessfulAuthentication(request, response, failed);return;}// Authentication successif (continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}//成功就走自定义成功的 CustomAuthenticationSuccessHandler, 如果开启“记住我”会在内部调用执行successfulAuthentication(request, response, chain, authResult);
}

UsernamePasswordAuthenticationFilter.java

获取用户与密码,封装 Token 开始验证

public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {//postOnly 标识表单是否是post提交。这个判断是如果设置了为post提交就检查请求类型是否为 postif (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//获取用户名与密码String username = obtainUsername(request);String password = obtainPassword(request);if (username == null) {username = "";}if (password == null) {password = "";}username = username.trim();//获取封装用户名密码之类的类,继承自 Authentication 接口,后面都会以 Authentication 类去接受用户信息UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// 这里主要初始化一个 详细信息类,用于存储请ip,sessionid 之类setDetails(request, authRequest);//交给认证管理器然后再执行认证return this.getAuthenticationManager().authenticate(authRequest);
}

ProviderManager

认证提供者管理器,找到 Token 与之对应的 Provider。因为 ProviderManager 里面会有多个 Provide

public Authentication authenticate(Authentication authentication)throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();AuthenticationException lastException = null;AuthenticationException parentException = null;Authentication result = null;Authentication parentResult = null;boolean debug = logger.isDebugEnabled();//认证管理器中会存在多个 Provider,这里主要只用执行验证逻辑。既然存在多个哪会执行哪个呢?//每一个实现 AuthenticationProvider 接口的类,都会提供 supports(xxx) 是否匹配,//从这个集合中循环每一个 Provider,判断是否与当前传入的 authentication 匹配,如果匹配,则执行执行//所以只会找到一个。这里是的 authentication 是 UsernamePasswordAuthenticationTokenfor (AuthenticationProvider provider : getProviders()) {//会有多个 provider,挑选出与 toTest 匹配的provider然后执行下面if (!provider.supports(toTest)) {continue;}if (debug) {logger.debug("Authentication attempt using "+ provider.getClass().getName());}try {//执行验证result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}}catch (AccountStatusException | InternalAuthenticationServiceException e) {prepareException(e, authentication);// SEC-546: Avoid polling additional providers if auth failure is due to// invalid account statusthrow e;} catch (AuthenticationException e) {lastException = e;}}//如果上面没有找到,那就使用父类默认的if (result == null && parent != null) {// Allow the parent to try.try {result = parentResult = parent.authenticate(authentication);}catch (ProviderNotFoundException e) {// ignore as we will throw below if no other exception occurred prior to// calling parent and the parent// may throw ProviderNotFound even though a provider in the child already// handled the request}catch (AuthenticationException e) {lastException = parentException = e;}}if (result != null) {if (eraseCredentialsAfterAuthentication&& (result instanceof CredentialsContainer)) {// Authentication is complete. Remove credentials and other secret data// from authentication((CredentialsContainer) result).eraseCredentials();}// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published itif (parentResult == null) {eventPublisher.publishAuthenticationSuccess(result);}return result;}// Parent was null, or didn't authenticate (or throw an exception).if (lastException == null) {lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",new Object[] { toTest.getName() },"No AuthenticationProvider found for {0}"));}// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published itif (parentException == null) {prepareException(lastException, authentication);}throw lastException;
}

AbstractUserDetailsAuthenticationProvider.java

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 user = this.userCache.getUserFromCache(username);//判断缓存中是否存在if (user == null) {cacheWasUsed = false;try {//如果缓存中没有继续执行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);//校验用户密码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();}//重现构造用户信息token,标识认证通过,存入权限return createSuccessAuthentication(principalToReturn, authentication, user);
}

DaoAuthenticationProvider

验证完毕后,完成验证。这里假设验证成功

protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {//这里就是获取自己实现的 UserDetailsServiceUserDetails 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);}
}

AbstractAuthenticationProcessingFilter.java

(回到第一步,执行成功后的处理),执行 successfulAuthentication(xxx) 方法,执行成功后的处理,里面主要会走 “记住我” 功能 与 成功后的 Handler。

protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response, FilterChain chain, Authentication authResult)throws IOException, ServletException {if (logger.isDebugEnabled()) {logger.debug("Authentication success. Updating SecurityContextHolder to contain: "+ authResult);}//登录成功后,会将认证信息存入 ThreadLocal 中SecurityContextHolder.getContext().setAuthentication(authResult);//执行记住我rememberMeServices.loginSuccess(request, response, authResult);// Fire eventif (this.eventPublisher != null) {eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}//执行成功的处理,如果有自定义,会调用自定义的successHandler.onAuthenticationSuccess(request, response, authResult);
}

AbstractRememberMeServices.java

执行 “记住我” 业务类,判断是否开启了记住我,然后执行记住我逻辑

public final void loginSuccess(HttpServletRequest request,HttpServletResponse response, Authentication successfulAuthentication) {//判断是否在页面选中记住我,以请求只能中的request.getParameter("Remember-me")//如果不记住,那就直接返回if (!rememberMeRequested(request, parameter)) {logger.debug("Remember-me login not requested.");return;}//执行记住我onLoginSuccess(request, response, successfulAuthentication);
}

PersistentTokenBasedRememberMeServices.java

存入数据库并且将 cookie 写到 响应里

protected void onLoginSuccess(HttpServletRequest request,HttpServletResponse response, Authentication successfulAuthentication) {//从认证信息中获取用户名String username = successfulAuthentication.getName();logger.debug("Creating new persistent login for user " + username);//生成记住我信息的包装类,里面有:用户名、随机base加密生成的series字符串、随机base加密生成的token字符串PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date());try {//将新生成的信息存入数据tokenRepository.createNewToken(persistentToken);//并且写入响应中addCookie(persistentToken, request, response);}catch (Exception e) {logger.error("Failed to save persistent token ", e);}
}

实现短信验证码

前言

目前我们只需要实现的验证码流程所需要的如下:

也就要实现4个组件,新增1个过滤器,1个配置整合组件类,全部类如下:
MobilePhoneVerificationCodeFilter:手机验证码过滤器
MobilePhoneAuthenticationFilter:手机验证码认证过滤器
MobilePhoneAuthenticationToken:认证的Token
MobilePhoneCodeAuthenticationProvider:认证处理 Provider
MobilePhoneUserDetailsService:校验手机号逻辑(主要用作查询数据库手机号与用户的业务类)
MobilePhoneAuthenticationConfig:整合类

以下代码实现流程:

增加发送验证码功能:
1. pom.xml 增加对应的依赖
2. 定义手机验证码接口,与实现类
3. 增加 手机验证码 RequestMapper (这里记得开放对应的请求)
4. html 代码页面增加验证码逻辑流程(重点):
1. 增加短信验证码过滤器
2. 增加自定义的 AuthenticationToken
3. 增加自定义的 provider
4. 实现 AuthenticationConfig 整管理组件
5. 一些列的 SpringSecurity 配置,将整合配置类放入 HTTPSecurity 中 与 加入过滤器
6. 修改错误时候,跳转的地址逻辑
7. 修改移动端记住我

代码实现

增加发送验证码功能

1. pom.xml 增加对应的依赖

<!--图形验证码-->
<dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId>
</dependency><!--数据库依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>

2. 定义手机验证码接口,与实现类

手机验证码接口

import javax.naming.AuthenticationException;
/*** 发送手机验证码接口* @Author : Vincent.jiao*/
public interface SmsSend {/*** 发送手机验证码*/public boolean send(String phone, String content) ;
}

实现类

import com.jiao.impl.SmsSend;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.naming.AuthenticationException;/*** 这里模拟 Alibaba 的发送验证码逻辑, 特别注意一下说明:* 这个bean需要注入spring,但是这里不用 @Component 实现,而是用 @Bean 注入,* 目的是为了这个是默认实现,如果有了别的实现则之类覆盖,而 @Component 达不到这个效果。* 在这里注入:{@link com.jiao.config.VerificationCodeConfig#sendPhoneCodeVerification()}* @Author : Vincent.jiao* @Date : 2020/5/12 21:38* @Version : 1.0*/public class AlibabaSmsSend implements SmsSend {Logger logger = LoggerFactory.getLogger(getClass());/*** 模拟发送验证码,这里只是打印日志* @param phone* @param content* @return* @throws AuthenticationException*/@Overridepublic boolean send(String phone, String content) {if(StringUtils.isEmpty(phone)){throw new NullPointerException("手机号不能为空");}if(StringUtils.isEmpty(content)){throw new NullPointerException("内容不能为空");}logger.info("手机号:" + phone + ", 内容为:" + content);return true;}
}

加入到 springbean 管理

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import com.jiao.impl.SmsSend;
import com.jiao.service.AlibabaSmsSend;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Properties;/*** 用于注册验证码bean* @Author : Vincent.jiao*/
@Configuration
public class VerificationCodeConfig {public static final String PHONE_CODE_VALUE = "code";@Beanpublic DefaultKaptcha getDefaultKaptcha(){DefaultKaptcha defaultKaptcha = new DefaultKaptcha();Properties properties = new Properties();properties.setProperty(Constants.KAPTCHA_BORDER, "yes");properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192");properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110");properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "36");properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "28");properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "宋体");properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");// 图片效果properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");Config config = new Config(properties);defaultKaptcha.setConfig(config);return defaultKaptcha;}@Bean@ConditionalOnMissingBean(SmsSend.class)public SmsSend sendPhoneCodeVerification(){return new AlibabaSmsSend();}
}

3. 增加 手机验证码 RequestMapper (这里记得开放对应的请求)

/*** 登录用的controller*/
@Controller
public class LoginController {Logger logger = LoggerFactory.getLogger(getClass());@AutowiredDefaultKaptcha defaultKaptcha;@AutowiredSmsSend smsSend;//手机验证码登录页面@RequestMapping("mobile/page")public String mobilePage(){return "login-mobile";}//发送手机验证码@GetMapping("/code/mobile")@ResponseBodypublic RestResult sendPhoneCode(HttpServletRequest request){//1. 生成验证码String code = "1234";//2. 发送验证码smsSend.send(request.getParameter("mobile").toString(), "验证码为:"+code);//3. 存入 sessionrequest.getSession().setAttribute(PHONE_CODE_VALUE, code);//4. 返回return RestResult.ok();}
}

4. html 代码页面

<form th:action="@{/mobile/form}" action="index.html" method="post"><div class="input-group mb-3"><input id="mobile" name="mobile" type="text" class="form-control" placeholder="手机号码"><div class="input-group-append"><div class="input-group-text"><span class="fa fa-user"></span></div></div></div><div class="mb-3 row"><div class="col-7"><input type="text" name="code" class="form-control" placeholder="验证码"></div><div class="col-5"><a id="sendCode" th:attr="code_url=@{/code/mobile?mobile=}" class="btn  btn-outline-primary btn-large" href="#"> 获取验证码 </a></div></div><div th:if="${param.error}"><span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION?.message}" style="color:red"></span></div><div class="row"><div class="col-8"><div class="icheck-primary"><input name="remember-me" type="checkbox" id="remember"><label for="remember">记住我</label></div></div><!-- /.col --><div class="col-4"><button type="submit" class="btn btn-primary btn-block">登录</button></div><!-- /.col --></div></form>

增加验证码逻辑流程(重点)

1. 增加短信验证码过滤器 MobilePhoneVerificationCodeFilter

package com.jiao.filter.security;import com.jiao.exception.VerificationCodeException;
import com.jiao.handler.CustomAuthenticationFailureHandler;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;import static com.jiao.config.VerificationCodeConfig.PHONE_CODE_VALUE;/**** @Author : Vincent.jiao* @Date : 2020/5/12 22:20* @Version : 1.0*/
@Component
public class MobilePhoneVerificationCodeFilter extends OncePerRequestFilter {@AutowiredCustomAuthenticationFailureHandler failureHandler;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//判断是否为登录到请求,如果是则校验验证码,不是则放行if(request.getRequestURI().equals("/mobile/form") && "POST".equalsIgnoreCase(request.getMethod()) ){try {validate(request);}catch (AuthenticationException e){//错处时候走自定义登录错误处理 handlerfailureHandler.onAuthenticationFailure(request, response, e);return;}}//放行请求filterChain.doFilter(request, response);}/*** 验证逻辑* @param request* @return*/private boolean validate(HttpServletRequest request){Object codeObj = request.getSession().getAttribute(PHONE_CODE_VALUE);if(null == codeObj){throw new VerificationCodeException("请发送验证码");}String codeStr = codeObj.toString();Object userInputCodeObj = request.getParameter("code");String userInputCodeStr =  userInputCodeObj == null ? null : userInputCodeObj.toString();if(StringUtils.isEmpty(userInputCodeStr)){throw new VerificationCodeException("验证码不能为空");}if(!userInputCodeStr.equals(codeStr)){throw new VerificationCodeException("验证码错误");}return true;}
}

2. 增加短信验证码认证过滤器 MobilePhoneAuthenticationFilter

package com.jiao.filter.security;import com.jiao.authentication.MobilePhoneAuthenticationToken;
import org.springframework.lang.Nullable;
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.util.Assert;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 移动端过滤器作为身份验证,对应复制 {@like UsernamePasswordAuthenticationFilter}对应代码就行* @Author : Vincent.jiao* @Version : 1.0*/
public class MobilePhoneAuthenticationFilter  extends AbstractAuthenticationProcessingFilter {//页面表单 手机号的name值private String mobilePhoneParameter = "mobile";//是否必须是 post 提交private boolean postOnly = true;public MobilePhoneAuthenticationFilter() {super(new AntPathRequestMatcher("/mobile/form", "POST"));}public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {//是否开启必须 post 提交if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//获取手机号String mobilePhone = obtainUsername(request);if (mobilePhone == null) {mobilePhone = "";}mobilePhone = mobilePhone.trim();//这里需要构造自己的手机验证码的身份认证 token,对应替换源码的 UsernamePasswordAuthenticationTokenMobilePhoneAuthenticationToken authRequest = new MobilePhoneAuthenticationToken(mobilePhone);//构造请求详情,如:ip、sessionidsetDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}@Nullableprotected String obtainUsername(HttpServletRequest request) {return request.getParameter(mobilePhoneParameter);}//设置请求详情,以供子类调用protected void setDetails(HttpServletRequest request,  MobilePhoneAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}public void setUsernameParameter(String usernameParameter) {Assert.hasText(usernameParameter, "Username parameter must not be empty or null");this.mobilePhoneParameter = usernameParameter;}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public final String getUsernameParameter() {return mobilePhoneParameter;}}

3. 增加自定义的 AuthenticationToken

package com.jiao.authentication;import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;/*** 实现手机验证码身份认证 token 类,对应源码的 {@like UsernamePasswordAuthenticationToken}* 这里直接复制对应类的源码* @Author : Vincent.jiao* @Version : 1.0*/
public class MobilePhoneAuthenticationToken  extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;//一个比较主要的属性,在源码中,认证前为密码(这里存验证码),认证后为用户详情private final Object principal;//在认证前构造这个,用户名与密码与权限封装构造public MobilePhoneAuthenticationToken(Object principal) {super(null);this.principal = principal;setAuthenticated(false);}//当认证成功后,重新构造一个。用来此时的 principal 存用户详情,别的还是一样public MobilePhoneAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true); // must use super, as we override}//因为是父类抽象中的方法,这里直接就返回null了,在源码中为密码,但是这里没有密码。@Overridepublic Object getCredentials() {return null;}public Object getPrincipal() {return this.principal;}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");}super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}
}

4. 增加自定义的 provider

package com.jiao.authentication;import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;/*** 手机验证码 Provider, 这里直接实现 {@ AuthenticationProvider} 接口* @Author : Vincent.jiao* @Version : 1.0*/
public class MobilePhoneCodeAuthenticationProvider  implements AuthenticationProvider {private UserDetailsService userDetailsService;public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}/*** 最终会执行这个方法去根据用户名查询获取到有个用户信息详情 UserDetails* @param authentication* @return* @throws AuthenticationException*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {MobilePhoneAuthenticationToken mobileAuthenticationToken = (MobilePhoneAuthenticationToken)authentication;// 获取手机号码String mobile = (String)mobileAuthenticationToken.getPrincipal();// 通过 手机号码 查询用户信息( UserDetailsService实现)UserDetails userDetails =userDetailsService.loadUserByUsername(mobile);// 未查询到用户信息if(userDetails == null) {throw new AuthenticationServiceException("该手机号未注册");}// 认证通过// 封装到 MobileAuthenticationTokenMobilePhoneAuthenticationToken authenticationToken =new MobilePhoneAuthenticationToken(userDetails, userDetails.getAuthorities());authenticationToken.setDetails(mobileAuthenticationToken.getDetails());//最终返回认证信息return authenticationToken;}/*** 通过这个方法,在 ProviderManager 的循环中来选择对应的Provider, 即选择当前这个类* @param authentication* @return*/@Overridepublic boolean supports(Class<?> authentication) {return MobilePhoneAuthenticationToken.class.isAssignableFrom(authentication);}
}

5. 实现 AuthenticationConfig 整管理组件

package com.jiao.config;import com.jiao.authentication.MobilePhoneCodeAuthenticationProvider;
import com.jiao.filter.security.MobilePhoneAuthenticationFilter;
import com.jiao.handler.CustomAuthenticationFailureHandler;
import com.jiao.handler.CustomAuthenticationSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;import javax.annotation.Resource;/*** 这里将自定义验证码的组件整合,设置到 spring security,需要继承抽象类 SecurityConfigurerAdapter* 实现 configure(xxx) 方法* @Author : Vincent.jiao* @Version : 1.0*/
@Component
public class MobilePhoneAuthenticationConfigextends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@AutowiredCustomAuthenticationSuccessHandler successHandler;@AutowiredCustomAuthenticationFailureHandler failureHandler;@Resource(name="mobilePhoneUserDetailsService")UserDetailsService userDetailsService;/*** 1. 配置定义的 MobilePhoneAuthenticationFilter(对应 {@like UsernamePasswordAuthenticationFilter}) 过滤器所需的东西* 2. 配置自定义的Provider,将自定义获取用户的逻辑加入自己的 Provider 中* 3. 设置到 springsecurity(就是HttpSecurity对象) 中** http.getSharedObject(xxx):方法是,在启动时候,会将一些类的实例存入到了 Map<Class<?>, Object> sharedObjects = new HashMap<>() 中** HttpSecurity 类作为 SpringSecurity 主要的配置类,如:filter集合、provider集合、AuthenticationManager认证管理器、默认的UserDetailsService,可以看这个配置类有多主要了* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {//1. 配置 MobilePhoneAuthenticationFilter 所需的东西MobilePhoneAuthenticationFilter authenticationFilter = new MobilePhoneAuthenticationFilter();//初始化认证管理器,获取容器中已经存在的AuthenticationManager对象,在启动时候都会存入一个 map(看文档注释)authenticationFilter.setAuthenticationManager( http.getSharedObject(AuthenticationManager.class));//设置成功与失败后的处理authenticationFilter.setAuthenticationSuccessHandler(successHandler);authenticationFilter.setAuthenticationFailureHandler(failureHandler);//设置 “记住我” 功能的实现类authenticationFilter.setRememberMeServices(http.getSharedObject(RememberMeServices.class));//2. 配置 MobilePhoneCodeAuthenticationProviderMobilePhoneCodeAuthenticationProvider mobileProvider = new MobilePhoneCodeAuthenticationProvider();mobileProvider.setUserDetailsService(userDetailsService );//3. 加入到 httpSecurityhttp.authenticationProvider(mobileProvider);//加入到 UsernamePasswordAuthenticationFilter 之前执行http.addFilterAfter(authenticationFilter, UsernamePasswordAuthenticationFilter.class);}
}

6. 一些列的 SpringSecurity 配置,将整合配置类放入 HttpSecurity 中 与 加入过滤器

package com.jiao.config;import com.jiao.filter.security.ImageCodeVerificationFilter;
import com.jiao.filter.security.MobilePhoneVerificationCodeFilter;
import com.jiao.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;import javax.annotation.Resource;
import javax.sql.DataSource;/*** Spring Security 配置类* @Author : Vincent.jiao* @Version : 1.0*/
@Configurable
@EnableWebSecurity  // 开启 springsecurity 过滤连 filter
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {/*** 表示使用 BCryptPasswordEncoder 方式加密密码,PasswordEncoder 为接口,spring security 实现了比较多的密码加密方式* BCryptPasswordEncoder:官方推荐的实现类,会生成一个盐值,然后实现 MD5 加密* DelegatingPasswordEncoder:目前默认的* NoOpPasswordEncoder:5.0之前默认实现的* @return*/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@AutowiredSecurityProperties securityProperties;@Resource(name="usernamePasswordUserDatailsService")UserDetailsService customUserDatailsService;@AutowiredAuthenticationSuccessHandler successHandler;@AutowiredAuthenticationFailureHandler failureHandler;@AutowiredImageCodeVerificationFilter imageCodeVerificationFilter;@AutowiredMobilePhoneVerificationCodeFilter verificationCodeFilter;@AutowiredDataSource dataSource;@AutowiredMobilePhoneAuthenticationConfig authenticationConfig;//主要用作 “记住我” 功能,它会将一些值存入数据库public JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl(){JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);//启动项目时候创建表jdbcTokenRepository.setCreateTableOnStartup(true);return jdbcTokenRepository;}/*** 身份认证管理器,其实就是校验密码* @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(customUserDatailsService);}/*** 资源权限配置,存在以下功能:* 拦截哪些资源* 校验资源对应的角色* 定义认证方法(HTTPBasic、HTTPForm)* 定制登录页面、请求地址、错误处理方式* 自定义 spring security 过滤器* @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(verificationCodeFilter, UsernamePasswordAuthenticationFilter.class)   //开启手机验证码过滤器,在指定过滤器之前执行.formLogin()        .loginPage(securityProperties.getLoginPage()).loginProcessingUrl(securityProperties.getLoginProcessingUrl())   //登录表单提交处理的url,默认是/login.usernameParameter(securityProperties.getUsernameParameter()).passwordParameter(securityProperties.getPasswordParameter())//定义成功与失败后处理的 handler.successHandler(successHandler).failureHandler(failureHandler)//开启记住我.and().rememberMe().tokenRepository(jdbcTokenRepositoryImpl()).tokenValiditySeconds(60*60*24).and().authorizeRequests()    //认证请求.antMatchers(securityProperties.getLoginPage(), "/code/image", "/mobile/page", "/code/mobile").permitAll().anyRequest().authenticated()           //所有访问应用的http请求都要通过身份认证才可以访问。;http.apply(authenticationConfig);}@Overridepublic void configure(WebSecurity web) {//放行静态资源web.ignoring().antMatchers(securityProperties.getAntMatchers());}
}

8. 成功或失败后的跳转的地址逻辑

package com.jiao.handler;import com.base.utils.ResponseType;
import com.base.utils.RestResult;
import com.jiao.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 登陆成功后怎么处理* @Author : Vincent.jiao* @Version : 1.0*/
@Component
//public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {@AutowiredSecurityProperties securityProperties;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {if(String.valueOf(ResponseType.JSON).equals(securityProperties.getResponseType())){//认证成功后,相应 JSON 字符串RestResult restResult = RestResult.ok("登陆成功");response.setContentType("application/json;charset=UTF-8");response.getWriter().write(restResult.toJsonString());} else {//重定向到上次请求的地址上,引发跳转到认证页面的地址super.onAuthenticationSuccess(request, response, authentication);}}
}
package com.jiao.handler;import com.base.utils.ResponseType;
import com.base.utils.RestResult;
import com.jiao.properties.SecurityProperties;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 失败后的处理* @Author : Vincent.jiao* @Date : 2020/5/11 16:27*/
@Component
//public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {Logger logger = LoggerFactory.getLogger(getClass());@AutowiredSecurityProperties securityProperties;@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {if(String.valueOf(ResponseType.JSON).equals(securityProperties.getResponseType())) {RestResult restResult = RestResult.build(HttpStatus.UNAUTHORIZED.value(), exception.getMessage());response.setContentType("application/json;charset=UTF-8");response.getWriter().write(restResult.toJsonString());}else {//这里为了同时兼用账号密码 与 验证码 失败后,所以直接跳到来源地址String refererUrl = request.getHeader("Referer");logger.info("来源地址为" + refererUrl);refererUrl = StringUtils.substringBefore(refererUrl, "?");logger.info("来源地址为" + refererUrl);// 重写向回认证页面,注意加上 ?errorsuper.setDefaultFailureUrl(refererUrl+"?error");super.onAuthenticationFailure(request, response, exception);}}
}

以上为主要实现。下面附上 Demo 项目示例

5. SpringSecurity用户认证源码 与 实现短信验证码(自定义SpringSecurity组件)相关推荐

  1. java实现短信上行源码_Java 发送短信验证码 示例源码

    [实例简介] 执行前请先设置修改 src/test.java 文件 //用户名 private static String Uid = "uid"; //接口安全秘钥(不是登录密码 ...

  2. REST framework 用户认证源码

    REST 用户认证源码 在Django中,从URL调度器中过来的HTTPRequest会传递给disatch(),使用REST后也一样 # REST的dispatch def dispatch(sel ...

  3. 短信发送系统后台搭建,源码基础版短信系统

    短信版本基础版本记录 因是基础版本,客户端功能和后台功能尽量简洁,方便实用. 一:后台和客户端的呈现方式 1.1后台采用软件版本,后台不通过URL地址打开.通过安装在使用的电脑上进行连接短信服务器进行 ...

  4. robotframework(12)修改用户密码(从数据库查询短信验证码)

    一.testcase:修改用户密码需要6个参数(短信验证码.设置的新密码.用户已登录的userid及用户唯一标识.接口校验码.被修改的手机号),故先准备这些参数 二.用户登录请求,(获取userid. ...

  5. 京东金融回应用户遭盗刷:系用户点击假冒链接 输短信验证码致密码泄露

    10月21日消息,针对用户银行卡被盗刷一事,京东金融方面回应称,经核查,此用户是本人点击了假冒抽奖链接,并且在操作时受该链接引导,多次输入短信验证码,导致自身在京东平台的支付密码.短信支付验证码均泄露 ...

  6. springCloud-OAuth2 客户端,用户认证源码分析

    SpringCloud-OAuth2提供了获取令牌的端点/oauth/token 那么它,客户端是如何认证的?,以及用户信息是如何认证?让我们一起来看看吧 1. 先说客户端的认证 客户端的认证我们从A ...

  7. 人工智能情感分析源码,垃圾短信邮箱分析

    分享一个机器学习文本分类项目的案例,该分类项目是一个通用的文本分类项目,这里的数据集我酒店用户评价数据,分类模型为二分类,正面评价和负面评价,这里所说的通用,就是你可以根据你自己的数据,进行train ...

  8. 聚会邀请html源码,聚会邀请短信

    1.金秋十月,瓜果飘香,作物成熟,肉类鲜美,一桌佳肴,怎可错过?今日聚餐,迟到后悔. 2.都是兄弟姐妹,我也不整啥弯子啦!我在xxx酒店订了一桌,想跟大家聚聚吃个饭,千万别爽约啊! 3.所谓朋友,就是 ...

  9. php短信接口加密_PHP短信接口、PHP短信验证码接口源码

    PHP短信接口.PHP短信验证码接口源码 时间:2016-06-13 11:53 来源:原创 作者:admin PHP短信接口文档源码,PHP发短信接口,PHP在线发短信,PHP微信发短信接口 /* ...

最新文章

  1. Asp.net 服务器端控件
  2. Springboot+Swagger
  3. yii2 html ul,yii2导航小部件子菜单类
  4. 文件服务器共享文件夹访问权限,5对文件服务器中的共享文件夹进行访问权限控制...
  5. js中常用的日期处理函数
  6. linux系统中怎么设置网络,vmware中linux怎么设置网络
  7. Windows Server 2008 多元密码策略之ADSIEDIT篇
  8. 2021-03-14
  9. 用python计算100以内所有奇数的和_Python-while 计算100以内奇数和的方法
  10. 什么是云计算,云计算的基本原理是什么?
  11. Nescafé2 月之谜 题解
  12. php加密解密 hash,PHP 加解密总结之 hash
  13. 在岗3年才拿8K,别不服人家应届生薪资比你高,你除了待公司久一点,还有什么比他强?
  14. 最牛逼的java代码_分享史上java最牛逼,最简短的代码
  15. 美团外卖API接入(二)
  16. Spring之配置非自定义Bean
  17. 定时器(Timer)
  18. 16哈理工新生赛 H 下雪啦 (哈希表)
  19. Golang占位符大全
  20. matlab用plot三点画圆_怎样用Matlab 过三个点画外接圆?

热门文章

  1. 二次开发发票管理软件应该注意的事项
  2. 女儿说要看烟花,但是政府规定不能放,程序员爸爸默默的拿起了键盘,程序员就是要为所欲为!
  3. 【愚公系列】2023年01月 Java教学课程 003-Hello World的运行
  4. 无刷电机外转子与内转子的区别
  5. day61-git版本控制工具
  6. Unity小技巧 - 绘制瞄准准心
  7. 无损连接和模式分解题型
  8. [InteliJ IDEA] 系统资源不足
  9. 视频教程:Java从入门到精通
  10. ImageLoader全局类配置 及图片展示配置(自定义缓存目录SD卡根目录)