图形验证码
SpringSecurity 实现的用户名、密码登录是在 UsernamePasswordAuthenticationFilter 过滤器进行认证的,而图形验证码一般是在用户名、密码认证之前进行验证的,所以需要在 UsernamePasswordAuthenticationFilter 过滤器之前添加一个自定义过滤器 ImageCodeValidateFilter,用来校验用户输入的图形验证码是否正确。
实现逻辑
自定义过滤器继承 OncePerRequestFilter 类,该类是 Spring 提供的在一次请求中只会调用一次的 filter,确保每个请求只会进入过滤器一次,避免了多次执行的情况
自定义的过滤器 ImageCodeValidateFilter 首先会判断请求是否为 POST 方式的登录表单提交请求,如果是就将其拦截进行图形验证码校验。如果验证错误,会抛出自定义异常类对象 ValidateCodeException,该异常类需要继承 AuthenticationException 类。在自定义过滤器中,我们需要手动捕获自定义异常类对象,并将捕获到自定义异常类对象交给自定义失败处理器进行处理

添加验证码配置
<!-- 图形验证码工具 kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
/**
* 图形验证码的配置类
*/
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha captchaProducer() {
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, "40");
// 验证码颜色
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "0,0,0");
// 验证码字体大小
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "32");
// 验证码生成几个字符
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码随机字符库
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
// 验证码图片默认是有线条干扰的,我们设置成没有干扰
properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
提供验证码接口
public class CheckCode implements Serializable {
private String code; // 验证码字符
private LocalDateTime expireTime; // 过期时间
/**
* @param code 验证码字符
* @param expireTime 过期时间,单位秒
*/
public CheckCode(String code, int expireTime) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
}
public CheckCode(String code) {
// 默认验证码 60 秒后过期
this(code, 60);
}
// 是否过期
public boolean isExpried() {
returnthis.expireTime.isBefore(LocalDateTime.now());
}
public String getCode() {
returnthis.code;
}
}
@Controller
public class LoginController {
// Session 中存储图形验证码的属性名
public static final String KAPTCHA_SESSION_KEY = "KAPTCHA_SESSION_KEY";
@Autowired
private DefaultKaptcha defaultKaptcha;
@GetMapping("/login/page")
public String login() {
return"login";
}
@GetMapping("/code/image")
public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 创建验证码文本
String capText = defaultKaptcha.createText();
// 创建验证码图片
BufferedImage image = defaultKaptcha.createImage(capText);
// 将验证码文本放进 Session 中
CheckCode code = new CheckCode(capText);
request.getSession().setAttribute(KAPTCHA_SESSION_KEY, code);
// 将验证码图片返回,禁止验证码图片缓存
response.setHeader("Cache-Control", "no-store");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
ImageIO.write(image, "jpg", response.getOutputStream());
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h3>表单登录</h3>
<form method="post" th:action="@{/login/form}">
<input type="text" name="username" placeholder="用户名"><br>
<input type="password" name="password" placeholder="密码"><br>
<input name="imageCode" type="text" placeholder="验证码"><br>
<img th:onclick="this.src='/code/image?'+Math.random()" th:src="@{/code/image}" alt="验证码"/><br>
<div th:if="${param.error}">
<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span>
</div>
<button type="submit">登录</button>
</form>
</body>
</html>
自定义验证码过滤器
/**
* 自定义验证码校验错误的异常类,继承 AuthenticationException
*/
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg, Throwable t) {
super(msg, t);
}
public ValidateCodeException(String msg) {
super(msg);
}
}
@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {
private String codeParamter = "imageCode"; // 前端输入的图形验证码参数名
@Autowired
private AuthenticationFailureHandlerImpl authenticationFailureHandler; // 自定义认证失败处理器
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 非 POST 方式的表单提交请求不校验图形验证码
if ("/login/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {
try {
// 校验图形验证码合法性
validate(request);
} catch (ValidateCodeException e) {
// 手动捕获图形验证码校验过程抛出的异常,将其传给失败处理器进行处理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
// 放行请求,进入下一个过滤器
filterChain.doFilter(request, response);
}
// 判断验证码的合法性
private void validate(HttpServletRequest request) {
// 获取用户传入的图形验证码值
String requestCode = request.getParameter(this.codeParamter);
if(requestCode == null) {
requestCode = "";
}
requestCode = requestCode.trim();
// 获取 Session
HttpSession session = request.getSession();
// 获取存储在 Session 里的验证码值
CheckCode savedCode = (CheckCode) session.getAttribute(LoginController.KAPTCHA_SESSION_KEY);
if (savedCode != null) {
// 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
session.removeAttribute(LoginController.KAPTCHA_SESSION_KEY);
}
// 校验出错,抛出异常
if (StringUtils.isBlank(requestCode)) {
thrownew ValidateCodeException("验证码的值不能为空");
}
if (savedCode == null) {
thrownew ValidateCodeException("验证码不存在");
}
if (savedCode.isExpried()) {
thrownew ValidateCodeException("验证码过期");
}
if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {
thrownew ValidateCodeException("验证码输入错误");
}
}
}
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("root".equals(username)) {
returnnew User(username, passwordEncoder.encode("123"), AuthorityUtils.createAuthorityList("admin"));
} else {
thrownew UsernameNotFoundException("用户名不存在");
}
}
}
设置过滤器顺序
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandlerImpl authenticationFailureHandler;
@Autowired
private ImageCodeValidateFilter imageCodeValidateFilter; // 自定义过滤器(图形验证码校验)
@Autowired
private UserDetailServiceImpl userDetailService;
/**
* 密码编码器,密码不能明文存储
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 使用 BCryptPasswordEncoder 密码编码器,该编码器会将随机产生的 salt 混入最终生成的密文中
returnnew BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
}
/**
* 定制基于 HTTP 请求的用户访问控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 启动 form 表单登录
http.formLogin()
// 设置登录页面的访问路径,默认为 /login,GET 请求;该路径不设限访问
.loginPage("/login/page")
// 设置登录表单提交路径,默认为 loginPage() 设置的路径,POST 请求
.loginProcessingUrl("/login/form")
// 使用自定义的认证成功和失败处理器
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
// 开启基于 HTTP 请求访问控制
http.authorizeRequests()
// 以下访问不需要任何权限,任何人都可以访问
.antMatchers("/login/page", "/code/image").permitAll()
// 其它任何请求访问都需要先通过认证
.anyRequest().authenticated();
// 关闭 csrf 防护
http.csrf().disable();
// 将自定义过滤器(图形验证码校验)添加到 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 定制一些全局性的安全配置,例如:不拦截静态资源的访问
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源的访问不需要拦截,直接放行
web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
手机短信验证码
验证流程
带有图形验证码的用户名、密码登录流程:
-
在 ImageCodeValidateFilter 过滤器中校验用户输入的图形验证码是否正确。 -
在 UsernamePasswordAuthenticationFilter 过滤器中将 username 和 password 生成一个用于认证的 Token(UsernamePasswordAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。 -
AuthenticationManager 管理器寻找到一个合适的处理器 DaoAuthenticationProvider 来处理 UsernamePasswordAuthenticationToken。 -
DaoAuthenticationProvider 通过 UserDetailsService 接口的实现类 CustomUserDetailsService 从数据库中获取指定 username 的相关信息,并校验用户输入的 password。如果校验成功,那就认证通过,用户信息类对象 Authentication 标记为已认证。 -
认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中。
仿照上述流程,我们分析手机短信验证码登录流程:
-
仿照 ImageCodeValidateFilter 过滤器设计 MobileVablidateFilter 过滤器,该过滤器用来校验用户输入手机短信验证码。 -
仿照 UsernamePasswordAuthenticationFilter 过滤器设计 MobileAuthenticationFilter 过滤器,该过滤器将用户输入的手机号生成一个 Token(MobileAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。 -
AuthenticationManager 管理器寻找到一个合适的处理器 MobileAuthenticationProvider 来处理 MobileAuthenticationToken,该处理器是仿照 DaoAuthenticationProvider 进行设计的。 -
MobileAuthenticationProvider 通过 UserDetailsService 接口的实现类 MobileUserDetailsService 从数据库中获取指定手机号对应的用户信息,此处不需要进行任何校验,直接将用户信息类对象 Authentication 标记为已认证。 -
认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中,此处的操作不需要我们编写。
最后通过自定义配置类 MobileAuthenticationConfig 组合上述组件,并添加到安全配置类 SpringSecurityConfig 中。

提供短信发送接口
@Controller
public class LoginController {
// Session 中存储手机短信验证码的属性名
public static final String MOBILE_SESSION_KEY = "MOBILE_SESSION_KEY";
@GetMapping("/mobile/page")
public String mobileLoginPage() { // 跳转到手机短信验证码登录页面
return"login-mobile";
}
@GetMapping("/code/mobile")
@ResponseBody
public Object sendMoblieCode(HttpServletRequest request) {
// 随机生成一个 4 位的验证码
String code = RandomStringUtils.randomNumeric(4);
// 将手机验证码文本存储在 Session 中,设置过期时间为 10 * 60s
CheckCode mobileCode = new CheckCode(code, 10 * 60);
request.getSession().setAttribute(MOBILE_SESSION_KEY, mobileCode);
return mobileCode;
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
<script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<form method="post" th:action="@{/mobile/form}">
<input id="mobile" name="mobile" type="text" placeholder="手机号码"><br>
<div>
<input name="mobileCode" type="text" placeholder="验证码">
<button type="button" id="sendCode">获取验证码</button>
</div>
<div th:if="${param.error}">
<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span>
</div>
<button type="submit">登录</button>
</form>
<script>
// 获取手机短信验证码
$("#sendCode").click(function () {
var mobile = $('#mobile').val().trim();
if(mobile == '') {
alert("手机号不能为空");
return;
}
// /code/mobile?mobile=123123123
var url = "/code/mobile?mobile=" + mobile;
$.get(url, function(data){
alert(data);
});
});
</script>
</body>
</html>
自定义短信验证码校验过滤器
更改自定义失败处理器 CustomAuthenticationFailureHandler,原先的处理器在认证失败时,会直接重定向到/login/page?error 显示认证异常信息。现在我们有两种登录方式,应该进行以下处理:
-
带图形验证码的用户名、密码方式登录方式出现认证异常,重定向到/login/page?error -
手机短信验证码方式登录出现认证异常,重定向到/mobile/page?error
/**
* 继承 SimpleUrlAuthenticationFailureHandler 处理器,该类是 failureUrl() 方法使用的认证失败处理器
*/
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
String xRequestedWith = request.getHeader("x-requested-with");
// 判断前端的请求是否为 ajax 请求
if ("XMLHttpRequest".equals(xRequestedWith)) {
// 认证成功,响应 JSON 数据
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("认证失败");
}else {
// 用户名、密码方式登录出现认证异常,需要重定向到 /login/page?error
// 手机短信验证码方式登录出现认证异常,需要重定向到 /mobile/page?error
// 使用 Referer 获取当前登录表单提交请求是从哪个登录页面(/login/page 或 /mobile/page)链接过来的
String refer = request.getHeader("Referer");
String lastUrl = StringUtils.substringBefore(refer, "?");
// 设置默认的重定向路径
super.setDefaultFailureUrl(lastUrl + "?error");
// 调用父类的 onAuthenticationFailure() 方法
super.onAuthenticationFailure(request, response, e);
}
}
}
/**
* 手机短信验证码校验
*/
@Component
public class MobileCodeValidateFilter extends OncePerRequestFilter {
private String codeParamter = "mobileCode"; // 前端输入的手机短信验证码参数名
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定义认证失败处理器
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 非 POST 方式的手机短信验证码提交请求不进行校验
if("/mobile/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {
try {
// 检验手机验证码的合法性
validate(request);
} catch (ValidateCodeException e) {
// 将异常交给自定义失败处理器进行处理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
// 放行,进入下一个过滤器
filterChain.doFilter(request, response);
}
/**
* 检验用户输入的手机验证码的合法性
*/
private void validate(HttpServletRequest request) {
// 获取用户传入的手机验证码值
String requestCode = request.getParameter(this.codeParamter);
if(requestCode == null) {
requestCode = "";
}
requestCode = requestCode.trim();
// 获取 Session
HttpSession session = request.getSession();
// 获取 Session 中存储的手机短信验证码
CheckCode savedCode = (CheckCode) session.getAttribute(LoginController.MOBILE_SESSION_KEY);
if (savedCode != null) {
// 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
session.removeAttribute(LoginController.MOBILE_SESSION_KEY);
}
// 校验出错,抛出异常
if (StringUtils.isBlank(requestCode)) {
thrownew ValidateCodeException("验证码的值不能为空");
}
if (savedCode == null) {
thrownew ValidateCodeException("验证码不存在");
}
if (savedCode.isExpried()) {
thrownew ValidateCodeException("验证码过期");
}
if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {
thrownew ValidateCodeException("验证码输入错误");
}
}
}
自定义短信验证码认证过滤器
-
仿照 UsernamePasswordAuthenticationToken 类进行编写 -
仿照 UsernamePasswordAuthenticationFilter 过滤器进行编写
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 520L;
private final Object principal;
/**
* 认证前,使用该构造器进行封装信息
*/
public MobileAuthenticationToken(Object principal) {
super(null); // 用户权限为 null
this.principal = principal; // 前端传入的手机号
this.setAuthenticated(false); // 标记为未认证
}
/**
* 认证成功后,使用该构造器封装用户信息
*/
public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities); // 用户权限集合
this.principal = principal; // 封装认证用户信息的 UserDetails 对象,不再是手机号
super.setAuthenticated(true); // 标记认证成功
}
@Override
public Object getCredentials() {
// 由于使用手机短信验证码登录不需要密码,所以直接返回 null
returnnull;
}
@Override
public Object getPrincipal() {
returnthis.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
thrownew IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
@Override
public void eraseCredentials() {
// 手机短信验证码认证方式不必去除额外的敏感信息,所以直接调用父类方法
super.eraseCredentials();
}
}
/**
* 手机短信验证码认证过滤器,仿照 UsernamePasswordAuthenticationFilter 过滤器编写
*/
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParamter = "mobile"; // 默认手机号参数名为 mobile
private boolean postOnly = true; // 默认请求方式只能为 POST
protected MobileAuthenticationFilter() {
// 默认登录表单提交路径为 /mobile/form,POST 方式请求
super(new AntPathRequestMatcher("/mobile/form", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//(1) 默认情况下,如果请求方式不是 POST,会抛出异常
if(postOnly && !request.getMethod().equals("POST")) {
thrownew AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}else {
//(2) 获取请求携带的 mobile
String mobile = request.getParameter(mobileParamter);
if(mobile == null) {
mobile = "";
}
mobile = mobile.trim();
//(3) 使用前端传入的 mobile 构造 Authentication 对象,标记该对象未认证
// MobileAuthenticationToken 是我们自定义的 Authentication 类,后续介绍
MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile);
//(4) 将请求中的一些属性信息设置到 Authentication 对象中,如:remoteAddress,sessionId
this.setDetails(request, authRequest);
//(5) 调用 ProviderManager 类的 authenticate() 方法进行身份认证
returnthis.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParamter);
}
protected void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParamter) {
Assert.hasText(mobileParamter, "Mobile par ameter must not be empty or null");
this.mobileParamter = mobileParamter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public String getMobileParameter() {
return mobileParamter;
}
}
自定义短信验证码认证处理器
-
仿照 DaoAuthenticationProvider 处理器进行编写 -
MobileAuthenticationProvider 处理器传入的 UserDetailsService 对象的类型需要我们自定义
public class MobileAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsChecker authenticationChecks = new MobileAuthenticationProvider.DefaultAuthenticationChecks();
/**
* 处理认证
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//(1) 如果入参的 Authentication 类型不是 MobileAuthenticationToken,抛出异常
Assert.isInstanceOf(MobileAuthenticationToken.class, authentication, () -> {
returnthis.messages.getMessage("MobileAuthenticationProvider.onlySupports", "Only MobileAuthenticationToken is supported");
});
// 获取手机号
String mobile = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
//(2) 根据手机号从数据库中查询用户信息
UserDetails user = this.userDetailsService.loadUserByUsername(mobile);
if (user == null) {
//(3) 未查询到用户信息,抛出异常
thrownew AuthenticationServiceException("该手机号未注册");
}
//(4) 检查账号是否锁定、账号是否可用、账号是否过期、密码是否过期
this.authenticationChecks.check(user);
//(5) 查询到了用户信息,则认证通过,构建标记认证成功用户信息类对象 AuthenticationToken
MobileAuthenticationToken result = new MobileAuthenticationToken(user, user.getAuthorities());
// 需要把认证前 Authentication 对象中的 details 信息加入认证后的 Authentication
result.setDetails(authentication.getDetails());
return result;
}
/**
* ProviderManager 管理器通过此方法来判断是否采用此 AuthenticationProvider 类
* 来处理由 AuthenticationFilter 过滤器传入的 Authentication 对象
*/
@Override
public boolean supports(Class<?> authentication) {
// isAssignableFrom 返回 true 当且仅当调用者为父类.class,参数为本身或者其子类.class
// ProviderManager 会获取 MobileAuthenticationFilter 过滤器传入的 Authentication 类型
// 所以当且仅当 authentication 的类型为 MobileAuthenticationToken 才返回 true
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* 此处传入自定义的 MobileUserDetailsSevice 对象
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
/**
* 检查账号是否锁定、账号是否可用、账号是否过期、密码是否过期
*/
private class DefaultAuthenticationChecks implements UserDetailsChecker {
private DefaultAuthenticationChecks() {
}
@Override
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
thrownew LockedException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} elseif (!user.isEnabled()) {
thrownew DisabledException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} elseif (!user.isAccountNonExpired()) {
thrownew AccountExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
} elseif (!user.isCredentialsNonExpired()) {
thrownew CredentialsExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
}
}
}
@Service
public class MobileUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
//(1) 从数据库尝试读取该用户
User user = userMapper.selectByMobile(mobile);
// 用户不存在,抛出异常
if (user == null) {
thrownew UsernameNotFoundException("用户不存在");
}
//(2) 将数据库形式的 roles 解析为 UserDetails 的权限集合
// AuthorityUtils.commaSeparatedStringToAuthorityList() 是 Spring Security 提供的方法,用于将逗号隔开的权限集字符串切割为可用权限对象列表
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
//(3) 返回 UserDetails 对象
return user;
}
}
自定义短信验证码认证方式配置类
-
将上述组件进行管理,仿照 SecurityConfigurerAdapter类进行编写 -
绑定到最终的安全配置类 SpringSecurityConfig 中
@Component
publicclass MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; // 自定义认证成功处理器
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; // 自定义认证失败处理器
@Autowired
private MobileCodeValidateFilter mobileCodeValidaterFilter; // 手机短信验证码校验过滤器
@Autowired
private MobileUserDetailsService userDetailsService; // 手机短信验证方式的 UserDetail
@Override
public void configure(HttpSecurity http) throws Exception {
//(1) 将短信验证码认证的自定义过滤器绑定到 HttpSecurity 中
//(1.1) 创建手机短信验证码认证过滤器的实例 filer
MobileAuthenticationFilter filter = new MobileAuthenticationFilter();
//(1.2) 设置 filter 使用 AuthenticationManager(ProviderManager 接口实现类) 认证管理器
// 多种登录方式应该使用同一个认证管理器实例,所以获取 Spring 容器中已经存在的 AuthenticationManager 实例
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
filter.setAuthenticationManager(authenticationManager);
//(1.3) 设置 filter 使用自定义成功和失败处理器
filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
//(1.4) 设置 filter 使用 SessionAuthenticationStrategy 会话管理器
// 多种登录方式应该使用同一个会话管理器实例,获取 Spring 容器已经存在的 SessionAuthenticationStrategy 实例
SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
//(1.5) 在 UsernamePasswordAuthenticationFilter 过滤器之前添加 MobileCodeValidateFilter 过滤器
// 在 UsernamePasswordAuthenticationFilter 过滤器之后添加 MobileAuthenticationFilter 过滤器
http.addFilterBefore(mobileCodeValidaterFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
//(2) 将自定义的 MobileAuthenticationProvider 处理器绑定到 HttpSecurity 中
//(2.1) 创建手机短信验证码认证过滤器的 AuthenticationProvider 实例,并指定所使用的 UserDetailsService
MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
//(2.2) 将该 AuthenticationProvider 实例绑定到 HttpSecurity 中
http.authenticationProvider(provider);
}
}
@Configuration
publicclass SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailServiceImpl userDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler authenticationSuccessHandler; // 自定义认证成功处理器
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定义认证失败处理器
@Autowired
private ImageCodeValidateFilter imageCodeValidateFilter; // 自定义过滤器(图形验证码校验)
@Autowired
private MobileAuthenticationConfig mobileAuthenticationConfig; // 手机短信验证码认证方式的配置类
/**
* 密码编码器,密码不能明文存储
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 使用 BCryptPasswordEncoder 密码编码器,该编码器会将随机产生的 salt 混入最终生成的密文中
returnnew BCryptPasswordEncoder();
}
/**
* 定制用户认证管理器来实现用户认证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 不再使用内存方式存储用户认证信息,而是动态从数据库中获取
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 定制基于 HTTP 请求的用户访问控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 启动 form 表单登录
http.formLogin()
// 设置登录页面的访问路径,默认为 /login,GET 请求;该路径不设限访问
.loginPage("/login/page")
// 设置登录表单提交路径,默认为 loginPage() 设置的路径,POST 请求
.loginProcessingUrl("/login/form")
// 使用自定义的认证成功和失败处理器
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
// 开启基于 HTTP 请求访问控制
http.authorizeRequests()
// 以下访问不需要任何权限,任何人都可以访问
.antMatchers("/login/page", "/code/image","/mobile/page", "/code/mobile").permitAll()
// 其它任何请求访问都需要先通过认证
.anyRequest().authenticated();
// 关闭 csrf 防护
http.csrf().disable();
// 将自定义过滤器(图形验证码校验)添加到 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
// 将手机短信验证码认证的配置与当前的配置绑定
http.apply(mobileAuthenticationConfig);
}
/**
* 定制一些全局性的安全配置,例如:不拦截静态资源的访问
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源的访问不需要拦截,直接放行
web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
原文始发于微信公众号(爱编程的小生):SpringSecurity5(5-自定义短信、手机验证码)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/314521.html