【SpringSecurity+OAuth2+JWT入门到实战】11.短信验证码登录

编程

登录流程

  • SmsCodeAuthenticationFilter 短信登录请求
  • SmsCodeAuthenticationProvider 提供短信登录处理的实现类
  • SmsCodeAuthenticationToken 存放认证信息(包括未认证前的参数信息传递)
  • 最后开发一个过滤器放在 短信登录请求之前,进行短信验证码的验证

因为这个过滤器只关心提交的验证码是否正常就行了。所以可以应用到任意业务中,对任意业务提交进行短信的验证

UsernamePasswordAuthenticationToken

security默认密码登录,在这个基础上把密码相关的代码清理掉

//

// Source code recreated from a .class file by IntelliJ IDEA

// (powered by Fernflower decompiler)

//

package org.springframework.security.authentication;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = 520L;

private final Object principal;

private Object credentials;

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {

super((Collection)null);

this.principal = principal;

this.credentials = credentials;

this.setAuthenticated(false);

}

public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {

super(authorities);

this.principal = principal;

this.credentials = credentials;

super.setAuthenticated(true);

}

public Object getCredentials() {

return this.credentials;

}

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");

} else {

super.setAuthenticated(false);

}

}

public void eraseCredentials() {

super.eraseCredentials();

this.credentials = null;

}

}

SmsCodeAuthenticationToken

core项目创建SmsCodeAuthenticationToken类

复制UsernamePasswordAuthenticationToken类全部内容到SmsCodeAuthenticationToken类

删除credentials值相关的代码, credentials字段去掉因为短信认证在授权认证前已经过滤了

package com.spring.security.authentication.mobile;

import org.springframework.security.authentication.AbstractAuthenticationToken;

import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = 520L;

//在认证之前应该放手机号 在认证之后放认证用户

private final Object principal;

public SmsCodeAuthenticationToken(String mobile) {

super((Collection)null);

this.principal = mobile;

//没登录之前放手机号

this.setAuthenticated(false);

}

public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {

super(authorities);

this.principal = principal;

//登录成功放用户信息

super.setAuthenticated(true);

}

@Override

public Object getPrincipal() {

return this.principal;

}

@Override

public Object getCredentials() {

return null;

}

@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);

}

}

@Override

public void eraseCredentials() {

super.eraseCredentials();

}

}

 

UsernamePasswordAuthenticationFilter

写SmsCodeAuthenticationFilter同意去参考security默认拦截器

//

// Source code recreated from a .class file by IntelliJ IDEA

// (powered by Fernflower decompiler)

//

package org.springframework.security.web.authentication;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.springframework.lang.Nullable;

import org.springframework.security.authentication.AuthenticationServiceException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import org.springframework.util.Assert;

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 String usernameParameter = "username";

private String passwordParameter = "password";

private boolean postOnly = true;

public UsernamePasswordAuthenticationFilter() {

super(new AntPathRequestMatcher("/login", "POST"));

}

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());

} else {

String username = this.obtainUsername(request);

String password = this.obtainPassword(request);

if (username == null) {

username = "";

}

if (password == null) {

password = "";

}

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

this.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));

}

public void setUsernameParameter(String usernameParameter) {

Assert.hasText(usernameParameter, "Username parameter must not be empty or null");

this.usernameParameter = usernameParameter;

}

public void setPasswordParameter(String passwordParameter) {

Assert.hasText(passwordParameter, "Password parameter must not be empty or null");

this.passwordParameter = passwordParameter;

}

public void setPostOnly(boolean postOnly) {

this.postOnly = postOnly;

}

public final String getUsernameParameter() {

return this.usernameParameter;

}

public final String getPasswordParameter() {

return this.passwordParameter;

}

}

SmsCodeAuthenticationFilter

同样复制UsernamePasswordAuthenticationFilter代码

package com.spring.security.authentication.mobile;

import org.springframework.lang.Nullable;

import org.springframework.security.authentication.AuthenticationServiceException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

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;

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

//定义携带手机号参数名

public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;

private boolean postOnly = true;

public SmsCodeAuthenticationFilter() {

//要处理的请求连接

//登录页面form表单action <form action="/authentication/mobile" method="post">

super(new AntPathRequestMatcher("/authentication/mobile", "POST"));

}

@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());

} else {

//获取手机号

String mobile = this.obtainMobile(request);

if (mobile == null) {

mobile = "";

}

//去掉空格

mobile = mobile.trim();

SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

// 把request里面的一些信息copy近token里面

// 后面认证成功的时候还需要copy这信息到新的token

this.setDetails(request, authRequest);

//认证行为已完成

return this.getAuthenticationManager().authenticate(authRequest);

}

}

/**

* 获取手机号

*

* @param request

* @return

*/

@Nullable

protected String obtainMobile(HttpServletRequest request) {

return request.getParameter(this.mobileParameter);

}

/**

* 设置认证信息去请求头

*

* @param request

* @param authRequest

*/

protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {

authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));

}

public void setMobileParameter(String mobileParameter) {

Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");

this.mobileParameter = mobileParameter;

}

public void setPostOnly(boolean postOnly) {

this.postOnly = postOnly;

}

public final String getMobileParameter() {

return this.mobileParameter;

}

}

SmsCodeAuthenticationProvider

这个没有找到仿照的地方。没有发现和usernamePassword类型的提供provider

package com.spring.security.authentication.mobile;

import org.springframework.beans.factory.annotation.Autowired;

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;

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

@Autowired

private UserDetailsService userDetailsService;

/**

* 身份认证逻辑

* @param authentication

* @return

* @throws AuthenticationException

*/

@Override

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

//根据手机号取用户信息

UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

if (user == null) {

throw new InternalAuthenticationServiceException("无法获取用户信息");

}

SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities());

//复制未认证的信息到已认证

authenticationResult.setDetails(authenticationToken.getDetails());

return authenticationResult;

}

@Override

public boolean supports(Class<?> authentication) {

return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);

}

public UserDetailsService getUserDetailsService() {

return userDetailsService;

}

public void setUserDetailsService(UserDetailsService userDetailsService) {

this.userDetailsService = userDetailsService;

}

}

smsCodeFilter

直接复制ValidateCodeFilter类改名为:smsCodeFilter

package com.spring.security.validate.code;

import com.spring.security.properties.SecurityProperties;

import com.spring.security.validate.code.image.ImageCode;

import org.apache.commons.lang.StringUtils;

import org.springframework.beans.factory.InitializingBean;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import org.springframework.social.connect.web.HttpSessionSessionStrategy;

import org.springframework.social.connect.web.SessionStrategy;

import org.springframework.stereotype.Component;

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.HashSet;

import java.util.Set;

/**

* 短信验证码过滤器

*/

@Component

public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {

@Autowired

private AuthenticationFailureHandler authenticationFailureHandler;

private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

@Autowired

private SecurityProperties securityProperties;

private Set<String> urls = new HashSet<>();

@Override

public void afterPropertiesSet() throws ServletException {

super.afterPropertiesSet();

String[] configUrls = StringUtils.splitPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");

for (String configUrl : configUrls) {

urls.add(configUrl);

}

//加入固定提交地址

urls.add("/authentication/mobile");

}

/**

* 做过滤器内部

*

* @param httpServletRequest http Servlet请求

* @param httpServletResponse

* @param filterChain 过滤器链

* @throws ServletException Servlet异常

* @throws IOException IOException

*/

@Override

protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

boolean action = false;

for (String url : urls) {

if (StringUtils.startsWithIgnoreCase(url, httpServletRequest.getRequestURI())

&& StringUtils.startsWithIgnoreCase(httpServletRequest.getMethod(), "post")) {

action = true;

}

}

if (action) {

try {

validateCode(new ServletWebRequest(httpServletRequest));

} catch (ValidateCodeException e) {

authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);

return;

}

}

filterChain.doFilter(httpServletRequest, httpServletResponse);

}

/**

* 验证代码

*

* @param servletWebRequest servlet的Web请求

* @throws ServletRequestBindingException Servlet请求绑定异常

*/

private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {

ValidateCode validateSession = (ValidateCode) sessionStrategy.getAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");

String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode");

if (StringUtils.isEmpty(codeInRequest)) {

throw new ValidateCodeException("验证码不能为空!");

}

if (validateSession == null) {

throw new ValidateCodeException("验证码不存在!");

}

if (validateSession.isExpire()) {

sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");

throw new ValidateCodeException("验证码已过期!");

}

if (!StringUtils.startsWithIgnoreCase(validateSession.getCode(), codeInRequest)) {

throw new ValidateCodeException("验证码不正确!");

}

sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");

}

}

SmsCodeAuthenticationSecurityConfig

需要的几个东西已经准备好了。这里要进行配置把这些加入到 security的认证流程中去;

package com.spring.security.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;

@Component

public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired

private AuthenticationFailureHandler hkAuthenticationFailureHandler;

@Autowired

private AuthenticationSuccessHandler hkAuthenticationSuccessHandler;

@Autowired

private UserDetailsService userDetailsService;

@Override

public void configure(HttpSecurity http) throws Exception {

SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();

// 把该过滤器交给管理器

// 图上流程,因为最先走的 短信认证的过滤器(不是验证码,只是认证)

// 要使用管理器来获取provider,所以把管理器注册进去

filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

//认证失败处理器

filter.setAuthenticationFailureHandler(hkAuthenticationFailureHandler);

//认证成功处理器

filter.setAuthenticationSuccessHandler(hkAuthenticationSuccessHandler);

SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();

smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

//加到security认证流程

http

// 注册到AuthenticationManager中去

.authenticationProvider(smsCodeAuthenticationProvider)

// 添加到 UsernamePasswordAuthenticationFilter 之后

// 貌似所有的入口都是 UsernamePasswordAuthenticationFilter

// 然后UsernamePasswordAuthenticationFilter的provider不支持这个地址的请求

// 所以就会落在我们自己的认证过滤器上。完成接下来的认证

.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);

}

}

BrowserSecurityConfig 应用方配置

package com.spring.security;

import com.spring.security.authentication.HkAuthenticationFailureHandler;

import com.spring.security.authentication.HkAuthenticationSuccessHandler;

import com.spring.security.authentication.mobile.SmsCodeAuthenticationSecurityConfig;

import com.spring.security.properties.SecurityProperties;

import com.spring.security.validate.code.SmsCodeFilter;

import com.spring.security.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.UsernamePasswordAuthenticationFilter;

import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

@Configuration

public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired

private SecurityProperties securityProperties;

@Autowired

private HkAuthenticationSuccessHandler hkAuthenticationSuccessHandler;

@Autowired

private HkAuthenticationFailureHandler hkAuthenticationFailureHandler;

@Autowired

private ValidateCodeFilter validateCodeFilter;

@Autowired

private SmsCodeFilter smsCodeFilter;

// 数据源是需要在使用处配置数据源的信息

@Autowired

private DataSource dataSource;

// 之前已经写好的 MyUserDetailsService

@Autowired

private UserDetailsService userDetailsService;

//SmsCodeAuthenticationSecurityConfig加入到认证流程

@Autowired

private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

@Bean

public PersistentTokenRepository persistentTokenRepository() {

// org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer.tokenRepository

JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();

jdbcTokenRepository.setDataSource(dataSource);

// 该对象里面有定义创建表的语句

//create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)

// 可以设置让该类来创建表

// 但是该功能只用使用一次,如果数据库已经存在表则会报错

//jdbcTokenRepository.setCreateTableOnStartup(true);

//两种方法二选一

return jdbcTokenRepository;

}

@Override

protected void configure(HttpSecurity http) throws Exception {

//配置

http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器

// 短信验证码校验过滤器

.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)

.formLogin()

.loginPage("/authentication/require")//登录页面路径

// 处理登录请求路径

.loginProcessingUrl("/authentication/form")

.successHandler(hkAuthenticationSuccessHandler) // 处理登录成功

.failureHandler(hkAuthenticationFailureHandler) // 处理登录失败

.and()

// 从这里开始配置记住我的功能

.rememberMe()

.tokenRepository(persistentTokenRepository())

// 新增过期配置,单位秒

.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())

// userDetailsService 是必须的。不然就报错

.userDetailsService(userDetailsService)

.and()

.authorizeRequests() // 授权配置

//不需要认证的路径

.antMatchers("/authentication/require", "/code/*", "/signIn.html", securityProperties.getBrowser().getLoginPage(), "/failure").permitAll()

.anyRequest() // 所有请求

.authenticated() // 都需要认证

.and().csrf().disable()

//加入短信验证码认证流程

.apply(smsCodeAuthenticationSecurityConfig);

}

}

 测试

启动项目访问:http://127.0.0.1:8080/signIn.html

获取验证码:

输入错误验证码:

输入正确验证码:

以上是 【SpringSecurity+OAuth2+JWT入门到实战】11.短信验证码登录 的全部内容, 来源链接: utcz.com/z/514149.html

回到顶部