今天给大家讲一下,在springboot框架中如何扩展增加手机短信验证码登录功能
SmsAuthenticationToken
步骤:
principal 原本代表用户名,这里保留,只是代表了手机号码。 credentials 原本代码密码,短信登录用不到,直接删掉。 SmsCodeAuthenticationToken() 两个构造方法一个是构造没有鉴权的,一个是构造有鉴权的。 剩下的几个方法去除无用属性即可。
package com.jjxt.b.config;
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 = 420L;
private final Object principal;
/**
* 没登录之前,principal我们使用手机号
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile) {
super((Collection)null);
this.principal = mobile;
this.setAuthenticated(false);
}
/**
* 登录认证之后,principal我们使用用户信息
* @param principal
* @param authorities
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
public 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");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
}
}
SmsAuthenticationFilter
原本的静态字段有 username 和 password,都干掉,换成我们的手机号字段。 SmsCodeAuthenticationFilter() 中指定了这个 filter 的拦截 Url,我指定为 post 方式的 /sms/login。 剩下来的方法把无效的删删改改就好了。
package com.jjxt.b.config;
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 org.springframework.web.bind.annotation.RequestBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* form表单中手机号码的字段name
*/
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
/**
* 是否仅 POST 方式
*/
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
// 短信登录的请求 post 方式的 /sms/login
super(new AntPathRequestMatcher("/api/sms/login", "POST"));
}
@Override
public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
System.out.println( request.getParameter("mobile"));
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);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public String getMobileParameter() {
return mobileParameter;
}
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;
}
}
SmsAuthenticationProvider
步骤:
实现 AuthenticationProvider 接口,实现 authenticate() 和 supports() 方法。 supports() 方法决定了这个 Provider 要怎么被 AuthenticationManager 挑中,我这里通过 return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication),处理所有 SmsCodeAuthenticationToken 及其子类或子接口。 authenticate() 方法处理验证逻辑。 首先将 authentication 强转为 SmsCodeAuthenticationToken。 从中取出登录的 principal,也就是手机号。 调用自己写的 checkSmsCode() 方法,进行验证码校验,如果不合法,抛出 AuthenticationException 异常。 如果此时仍然没有异常,通过调用 loadUserByUsername(mobile) 读取出数据库中的用户信息。 如果仍然能够成功读取,没有异常,这里验证就完成了。 重新构造鉴权后的 SmsCodeAuthenticationToken,并返回给 SmsCodeAuthenticationFilter 。 SmsCodeAuthenticationFilter 的父类在 doFilter() 方法中处理是否有异常,是否成功,根据处理结果跳转到登录成功/失败逻辑
package com.jjxt.b.config;
import lombok.Data;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
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;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
String mobile = (String) authenticationToken.getPrincipal();
checkSmsCode(mobile);
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
// 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
private void checkSmsCode(String mobile) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String inputCode = request.getParameter("code");
Map<String, Object> smsCode = (Map<String, Object>) request.getSession().getAttribute("smscode");
if(smsCode == null) {
throw new BadCredentialsException("未检测到申请验证码");
}
String applyMobile = (String) smsCode.get("mobile");
int code = (int) smsCode.get("code");
if(!applyMobile.equals(mobile)) {
throw new BadCredentialsException("申请的手机号码与登录手机号码不一致");
}
if(code != Integer.parseInt(inputCode)) {
throw new BadCredentialsException("验证码错误");
}
}
@Override
public boolean supports(Class<?> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
成功与失败处理逻辑
package com.jjxt.b.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.pdfbox.jbig2.util.log.Logger;
import org.apache.pdfbox.jbig2.util.log.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
验证失败处理
package com.jjxt.b.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.pdfbox.jbig2.util.log.Logger;
import org.apache.pdfbox.jbig2.util.log.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.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登陆失败");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
}
}
SmsCodeAuthenticationSecurityConfig
package com.jjxt.b.config;
import com.jjxt.b.config.security.CustomAuthenticationFailureHandler;
import com.jjxt.b.config.security.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.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
SecurityConfiguration 配置短信验证
package com.jjxt.b.config;
import com.jjxt.b.config.security.*;
import com.jjxt.b.service.sys.UsersService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
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.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.*;
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 javax.sql.DataSource;
import java.io.IOException;
import java.util.Arrays;
import java.util.UUID;
//import com.jjxt.b.config.SmsCodeAuthenticationSecurityConfig;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private String key;
@Value("${feature.csrf.enabled}")
private boolean enableCSRF;
@Autowired
private UsersService usersService;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
@Lazy
private PasswordEncoder passwordEncoder;
@Qualifier(PersistentConfiguration.DataSources.CORE)
@Autowired
private DataSource dataSource;
@Autowired
PersistentTokenRepository persistentTokenRepository;
@Autowired
AbstractRememberMeServices rememberMeServices;
@Autowired
CSRFProcessor csrfProcessor;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
private static final String PROJECT_URL="/api/projects/";
private static final String BIDSECT_URL= PROJECT_URL+"bidsect/";
private static final String USER_URL = PROJECT_URL + "users/";
private static final String UNIT_URL = PROJECT_URL+"units/";
private static final String CONTRACT_URL = PROJECT_URL+"contracts/";
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/api/user/login","/api/user/sms/**","/api/sms/**",
"/test/**").permitAll()
.antMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
.antMatchers("/api/admin/company/all").hasAnyRole("ADMIN", "SUPER_ADMIN")
.antMatchers("/api/admin/**").hasAnyRole( "SUPER_ADMIN")
// can only edit by _PEDITOR.
.antMatchers(Arrays.asList("create","update", "delete", "flowupdate").stream().map(i -> PROJECT_URL+i)
.toArray(String[]::new)).hasAnyRole("_PEDITOR")
.antMatchers(Arrays.asList("create","update", "delete").stream().map(i -> BIDSECT_URL+i)
.toArray(String[]::new)).hasAnyRole("_PEDITOR")
.antMatchers(Arrays.asList("add","delete").stream().map(i -> USER_URL+i)
.toArray(String[]::new)).hasAnyRole("_PEDITOR")
.antMatchers(Arrays.asList("create","update", "delete").stream().map(i -> UNIT_URL+i)
.toArray(String[]::new)).hasAnyRole("_PEDITOR")
.antMatchers(Arrays.asList("create","update", "delete").stream().map(i -> CONTRACT_URL+i)
.toArray(String[]::new)).hasAnyRole("_PEDITOR")
// mind the order.
.antMatchers("/api/**").authenticated()
.and().apply(new CustomizedRememberMeConfigurer<>()).key(key)
.rememberMeServices(rememberMeServices).tokenRepository(persistentTokenRepository)
// 以下短信登录认证的配置
.and()
.apply(smsCodeAuthenticationSecurityConfig)
.and()
.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
.authenticationEntryPoint(authenticationEntryPoint);
if (enableCSRF) {
http.csrf().csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
csrfProcessor.process(request, response);
filterChain.doFilter(request, response);
}
}, CsrfFilter.class);
} else {
http.csrf().disable();
}
}
@Bean
CSRFProcessor csrfProcessor(){
return enableCSRF ? new DefaultCSRFProcessor() : new IgnoreCSRFProcessor();
}
private HttpSessionCsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
@Bean
public CustomizedRememberMeServices rememberMeServices() {
CustomizedRememberMeServices services =
new CustomizedRememberMeServices(getKey(),
usersService, persistentTokenRepository);
return services;
}
String getKey() {
if (this.key == null) {
this.key = UUID.randomUUID().toString();
}
return this.key;
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
@Bean
public AuthenticationEntryPoint securityException401EntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
authException.getMessage());
}
};
}
@Bean
public DaoAuthenticationProvider authProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(usersService);
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
return usersService;
}
public static class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final Logger LOG = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOG.error("access denied", accessDeniedException);
}
}
}
代码结构逻辑
发送验证码
@RequestMapping("/sms/code")
@ResponseBody
public Object sms(@RequestParam("mobile") String mobile, HttpSession session) throws ClientException {
UserDetails u= usersService.getPhone(mobile);
if(u!=null)
{
int code = (int) Math.ceil(Math.random() * 9000 + 1000);
Map<String, Object> map = new HashMap<>(16);
map.put("mobile", mobile);
map.put("code", code);
SmsUtil.sendSmsCode(mobile, code+"");
session.setAttribute("smscode", map);
return "1";
}else
{
return "0";
}
}
前端vue 代码
export function plogin(data) {
return request({
url: '/sms/login',
method: 'post',
headers: {
"Content-Type": "multipart/form-data"
},
params:data
})
}