短信登录作为一种常见的登录认证方式,在
Spring Security
中是没有的,本篇博客将继续在之前文章的基础上,建立一套短信登录验证机制,并将其集成到Spring Security
中。
一、短信登录验证机制原理分析
在Spring Security
中,我们最常用的登录验证机制是基于用户名和密码的,输入了用户名和密码以及图片验证码之后,就会进入一系列的过滤器链中,直到验证成功或者验证失败为止。结合下面的图,我们来简要分析一下Spring Security
是如何验证基于用户名和密码登录方式的,分析完毕之后,再一起思考如何将短信登录验证方式集成到Spring Security
中。
基于用户名和密码的认证流程分析
- 第一步:在登录页面输入了用户名、密码和图片验证码,点击登录,那么这个登录请求首先被图片验证码的验证过滤器
ValidateCodeFilter
拦截,因为我们在BrowserSecurityConfig
类中将拦截器ValidateCodeFilter
配置到了UsernamePasswordAuthenticationFilter
之前,如果验证码验证通过之后,请求将继续被UsernamePasswordAuthenticationFilter
拦截,进入到它的attemptAuthentication
方法中进行校验,该方法首先对请求方法进行了校验,默认是POST
方法,然后从请求中获取用户名和密码,并基于用户名和密码生成了UsernamePasswordAuthenticationToken
,并在生成Token
的过程中将是否认证通过设置为false
。 - 第二步:
UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法继续调用AuthenticationManager
的authenticate
方法进行验证,在真正验证之前,验证器会从集合循环遍历AuthenticationProvider
,使用AuthenticationProvider
中的supports
方法来判断当前传递过来的Token
到底应该由哪个AuthenticationProvider
来进行校验,UsernamePasswordAuthenticationToken
将由DaoAuthenticationProvider
来进行校验,通过调用UserDetailService
的loadUserByUsername
方法来完成验证,最后结合验证结果,重新构建UsernamePasswordAuthenticationToken
,并将Token
中是否认证通过设置为true
完成认证步骤。
基于短信的认证流程分析
分析完基于用户名和密码的认证流程之后,我们可以将整个流程应用到需要我们自己定义短信认证的流程中,也就是说,短信认证的流程完全可以参考基于用户名和密码的认证流程,那么我们的短信认证过程也需要有相应的SmsAuthenticationToken
、SmsAuthenticationFilter
、SmsAuthenticationProvider
以及验证短信验证码的SmsCodeFilter
。
- 第一步:类似于图片验证码的校验,
SmsCodeFilter
也需要加到SmsAuthenticationFilter
之前,在短信验证码验证通过之后,那么登录请求到达SmsAuthenticationFilter
,进入到它的attemptAuthentication
方法中进行校验,该方法首先对请求方法进行了校验,默认是POST
方法,然后从请求中获取到手机号码,并基于手机号码来生成SmsAuthenticationToken
,并在生成Token
的过程中将是否认证通过设置为false
。 - 第二步:
SmsAuthenticationFilter
的attemptAuthentication
方法继续调用AuthenticationManager
的authenticate
方法进行验证,在真正验证之前,验证器会从集合循环遍历AuthenticationProvider
,使用AuthenticationProvider
中的supports
方法来判断当前传递过来的Token
到底应该由哪个AuthenticationProvider
来进行校验,SmsAuthenticationToken
将由SmsAuthenticationProvider
来进行校验,通过调用UserDetailService
的loadUserByUsername
方法来完成验证,最后结合验证结果,重新构建UsernamePasswordAuthenticationToken
,并将Token
中是否认证通过设置为true
完成认证步骤。
分析的过程和基于用户名和密码的方式是一模一样的,那么我们该如何来写这四个类的代码呢?我们当然是要参考对应的代码了!首先我们先一起写SmsAuthenticationToken
的代码,那么我们来参考一下UsernamePasswordAuthenticationToken
的代码,直接将其拷贝过来进行简要修改。
package com.lemon.security.core.authentication.mobile;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* 使用短信验证码登录的Token,写法类似{@link UsernamePasswordAuthenticationToken}
*
* @author lemon
* @date 2018/5/7 下午8:38
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>SmsCodeAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
* @param mobile 手机号
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal 认证信息
* @param authorities 权限列表
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}
// ~ Methods
// ========================================================================================================
@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");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
第一个构造方法用于在短信认证前使用,将手机号码传入到构造方法中,并将Authenticated
设置为false
,第二个构造方法用在认证成功之后,重新构造Token
的时候,将手机号和权限列表传入到其中,并设置Authenticated
为true
。
我们继续编写SmsAuthenticationFilter
的代码,当然,也是得参考UsernamePasswordAuthenticationFilter
,代码如下所示。
package com.lemon.security.core.authentication.mobile;
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;
/**
* @author lemon
* @date 2018/5/7 下午8:54
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
private static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private static final String SPRING_SECURITY_FORM_SUBMIT_METHOD = "POST";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
// ~ Methods
// ========================================================================================================
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !SPRING_SECURITY_FORM_SUBMIT_METHOD.equals(request.getMethod())) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 从请求中获取手机号的方法
*
* @param request 请求
* @return 手机号
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
/**
* Provided so that subclasses may configure what is put into the authentication
* request's details property.
*
* @param request that an authentication request is being created for
* @param authRequest the authentication request object that should have its details
* set
*/
protected void setDetails(HttpServletRequest request,
SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the mobile from the login
* request.
*
* @param mobileParameter the parameter name. Defaults to "mobile".
*/
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
* true, and an authentication request is received which is not a POST request, an
* exception will be raised immediately and authentication will not be attempted. The
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
* authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
写完SmsCodeAuthenticationFilter
,我们继续编写SmsCodeAuthenticationProvider
,那么这个Provider
也可以继续模仿DaoAuthenticationProvider
来进行编写,也可以自己实现一套校验逻辑,主要是调用UserDetailService
的loadUserByUsername
方法来进行校验,只是要注意的是,校验完毕后,需要重新构建SmsCodeAuthenticationToken
,将权限列表传入到SmsCodeAuthenticationToken
中,也就是使用SmsCodeAuthenticationToken
的第二个构造方法,构造方法中将Authenticated
设置成为true
,最后设置用户信息细节,并返回该认证后的Token
,代码如下:
package com.lemon.security.core.authentication.mobile;
import lombok.Data;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
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;
/**
* 短信登录的校验逻辑类
*
* @author lemon
* @date 2018/5/7 下午9:16
*/
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 将Authentication的对象强转为SmsCodeAuthenticationToken对象
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
// 根据手机号载入用户信息
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("用户信息不存在");
}
// 将获取到的用户信息封装到SmsCodeAuthenticationToken第二个构造方法中,在这个方法中设置为"已认证"
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
// 将用户的细节信息封装到已认证的token中
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
前面三个主要的类代码写完后,我们继续写一个校验短信验证码的类SmsCodeFilter
,代码如下:
package com.lemon.security.core.validate.code;
import com.lemon.security.core.properties.SecurityProperties;
import com.lemon.security.core.validate.code.sms.SmsCode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
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 java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author lemon
* @date 2018/4/6 下午8:23
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
private static final String SUBMIT_FORM_DATA_PATH = "/authentication/mobile";
private static final String SMS_SESSION_KEY = ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS";
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
urls.addAll(Arrays.asList(configUrls));
// 登录的链接是必须要进行验证码验证的
urls.add(SUBMIT_FORM_DATA_PATH);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
// 如果实际访问的URL可以与用户在YML配置文件中配置的相同,那么就进行验证码校验
if (antPathMatcher.match(url, request.getRequestURI())) {
action = true;
}
}
if (action) {
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 验证码校验逻辑
*
* @param request 请求
* @throws ServletRequestBindingException 请求异常
*/
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 从session中获取短信验证码
SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(request, SMS_SESSION_KEY);
// 从请求中获取用户填写的验证码
String smsCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
if (StringUtils.isBlank(smsCodeInRequest)) {
throw new ValidateCodeException("验证码不能为空");
}
if (null == smsCodeInSession) {
throw new ValidateCodeException("验证码不存在");
}
if (smsCodeInSession.isExpired()) {
sessionStrategy.removeAttribute(request, SMS_SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equalsIgnoreCase(smsCodeInRequest, smsCodeInSession.getCode())) {
throw new ValidateCodeException("验证码不匹配");
}
// 验证成功,删除session中的验证码
sessionStrategy.removeAttribute(request, SMS_SESSION_KEY);
}
}
我们将这四个类写好了,那么如何按照上面图片显示的那样,将其纳入到Spring Security
的管理之中呢,我们在第二小节下分析。
二、将短信登录验证机制集成到Spring Security中
我们需要将各个组件集成到Spring Security
中,那么就需要有一个配置类来完成配置,我们来编写一个SmsCodeAuthenticationSecurityConfig
类,将这几个组件集成到一起,代码如下:
package com.lemon.security.core.authentication.mobile;
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.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* 短信验证的配置类
*
* @author jiangpingping
* @date 2019-01-31 22:17
*/
@Component("smsCodeAuthenticationSecurityConfig")
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final AuthenticationSuccessHandler lemonAuthenticationSuccessHandler;
private final AuthenticationFailureHandler lemonAuthenticationFailureHandler;
private final UserDetailsService userDetailsService;
@Autowired
public SmsCodeAuthenticationSecurityConfig(AuthenticationSuccessHandler lemonAuthenticationSuccessHandler,
AuthenticationFailureHandler lemonAuthenticationFailureHandler,
UserDetailsService userDetailsService) {
this.lemonAuthenticationSuccessHandler = lemonAuthenticationSuccessHandler;
this.lemonAuthenticationFailureHandler = lemonAuthenticationFailureHandler;
this.userDetailsService = userDetailsService;
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 首先配置Sms验证的过滤器,在其中配置AuthenticationManager,验证成功处理器、失败处理器和认证的Provider等信息
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(lemonAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(lemonAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
// 将Provider注册到Spring Security中,将Filter加到UsernamePasswordAuthenticationFilter后面
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
在上面的配置方法configure
中,我们首先创建一个SmsCodeAuthenticationFilter
实例对象,然后设置了一个通用的AuthenticationManager
,还设置了验证成功处理器和失败处理器,然后又创建了一个SmsCodeAuthenticationProvider
实例对象,并将UserDetailService
实现类对象设置到了其中,并将SmsCodeAuthenticationFilter
过滤器加到了UsernamePasswordAuthenticationFilter
的后面。这样做还是不够的,我们还需要在类BrowserSecurityConfig
中加入一下代码:
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
smsCodeFilter.setAuthenticationFailureHandler(lemonAuthenticationFailureHandler);
smsCodeFilter.setSecurityProperties(securityProperties);
smsCodeFilter.afterPropertiesSet();
http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.apply(smsCodeAuthenticationSecurityConfig);
完整的配置如下所示:
package com.lemon.security.browser;
import com.lemon.security.core.authentication.mobile.SmsCodeAuthenticationSecurityConfig;
import com.lemon.security.core.properties.SecurityProperties;
import com.lemon.security.core.validate.code.SmsCodeFilter;
import com.lemon.security.core.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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 org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* 浏览器安全验证的配置类
*
* @author lemon
* @date 2018/4/3 下午7:35
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
private final SecurityProperties securityProperties;
private final AuthenticationSuccessHandler lemonAuthenticationSuccessHandler;
private final AuthenticationFailureHandler lemonAuthenticationFailureHandler;
private final DataSource dataSource;
@Autowired
public BrowserSecurityConfig(SecurityProperties securityProperties, AuthenticationSuccessHandler lemonAuthenticationSuccessHandler, AuthenticationFailureHandler lemonAuthenticationFailureHandler, DataSource dataSource) {
this.securityProperties = securityProperties;
this.lemonAuthenticationSuccessHandler = lemonAuthenticationSuccessHandler;
this.lemonAuthenticationFailureHandler = lemonAuthenticationFailureHandler;
this.dataSource = dataSource;
}
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
/**
* 配置了这个Bean以后,从前端传递过来的密码将被加密
*
* @return PasswordEncoder实现类对象
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(lemonAuthenticationFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
smsCodeFilter.setAuthenticationFailureHandler(lemonAuthenticationFailureHandler);
smsCodeFilter.setSecurityProperties(securityProperties);
smsCodeFilter.afterPropertiesSet();
http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(lemonAuthenticationSuccessHandler)
.failureHandler(lemonAuthenticationFailureHandler)
.and()
.rememberMe()
.tokenRepository(tokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage(), "/code/*").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.apply(smsCodeAuthenticationSecurityConfig);
}
}
直到这里,我们算是完成短信验证码的登录方式,并将其集成到了Spring Security
中,我们访问项目的默认登录页面http://localhost:8080/login.html
,就可以使用用户名或者手机进行登录,更多详细代码可以从码云中下载查看。大家有什么疑问可以在下方进行留言,我会一一回复。
Spring Security技术栈开发企业级认证与授权系列文章列表:
Spring Security技术栈学习笔记(一)环境搭建
Spring Security技术栈学习笔记(二)RESTful API详解
Spring Security技术栈学习笔记(三)表单校验以及自定义校验注解开发
Spring Security技术栈学习笔记(四)RESTful API服务异常处理
Spring Security技术栈学习笔记(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈学习笔记(六)使用REST方式处理文件服务
Spring Security技术栈学习笔记(七)使用Swagger自动生成API文档
Spring Security技术栈学习笔记(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈学习笔记(九)开发图形验证码接口
Spring Security技术栈学习笔记(十)开发记住我功能
Spring Security技术栈学习笔记(十一)开发短信验证码登录
Spring Security技术栈学习笔记(十二)将短信验证码验证方式集成到Spring Security
Spring Security技术栈学习笔记(十三)Spring Social集成第三方登录验证开发流程介绍
Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式
Spring Security技术栈学习笔记(十五)解决Spring Social集成QQ登录后的注册问题
Spring Security技术栈学习笔记(十六)使用Spring Social集成微信登录验证方式
示例代码下载地址:
项目已经上传到码云,欢迎下载,内容所在文件夹为
chapter012
。
更多干货分享,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)