作者:后端小肥肠
🍇 我写过的文章中的相关代码放到了gitee,地址:xfc-fdw-cloud: 公共解决方案
🍊 有疑问可私信或评论区联系我。
🥑 创作不易未经允许严禁转载。
姊妹篇:
【Spring Security系列】基于Spring Security实现权限动态分配之菜单-角色分配及动态鉴权实践_spring secrity权限角色动态管理-CSDN博客
【Spring Security系列】基于Spring Security实现权限动态分配之用户-角色分配_spring security 角色-CSDN博客
【Spring Security系列】权限之旅:SpringSecurity小程序登录深度探索_spring security 微信小程序登录-CSDN博客
【Spring Security系列】Spring Security+JWT+Redis实现用户认证登录及登出_spring security jwt 退出登录-CSDN博客
1. 前言
在当今的互联网应用中,手机验证码登录已经成为一种常见的用户身份验证方式。相比传统的用户名密码登录方式,手机验证码具有使用方便、安全性较高的特点。对于开发者来说,如何在现有的系统中快速集成这一功能,尤其是在Spring Security框架下,可能是一个具有挑战性的任务。这篇文章将详细介绍如何利用Spring Security来实现手机验证码的注册和登录功能,帮助你在短时间内搞定这一需求。
2. 注册
2.1. 手机验证码注册流程
以下是对流程图的具体分析:
- 前端请求和手机号码处理:- 用户发起获取验证码的请求,后端接收手机号码,生成随机验证码并存储在Redis中,这部分流程是标准的短信验证流程。- 在存储到Redis时明确了验证码的有效时间(5分钟)。
- 验证码发送:- 验证码通过调用短信服务发送,这里需要自行选择像阿里云、华为云等短信发送平台。
- 用户验证和注册提交:- 用户收到验证码后,在前端输入验证码并提交注册请求。- 系统从Redis中获取验证码并与用户输入的验证码进行匹配。- 如果匹配成功,注册流程继续进行并完成注册。- 如果匹配失败,提示用户验证码错误。
2.2. 代码实现(仅核心)
- 匹配短信消息发送相关参数(以华为云为例)
- 编写短信发送工具类
@Component
public class SendSmsUtil {
@Value("${huawei.sms.url}")
private String url;
@Value("${huawei.sms.appKey}")
private String appKey;
@Value("${huawei.sms.appSecret}")
private String appSecret;
@Value("${huawei.sms.sender}")
private String sender;
@Value("${huawei.sms.signature}")
private String signature;
/**
* 无需修改,用于格式化鉴权头域,给"X-WSSE"参数赋值
*/
private static final String WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\"";
/**
* 无需修改,用于格式化鉴权头域,给"Authorization"参数赋值
*/
private static final String AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\"";
public void sendSms(String templateId,String receiver, String templateParas) throws IOException {
String body = buildRequestBody(sender, receiver, templateId, templateParas, "", signature);
String wsseHeader = buildWsseHeader(appKey, appSecret);
HttpsURLConnection connection = null;
OutputStreamWriter out = null;
BufferedReader in = null;
StringBuilder result = new StringBuilder();
try {
URL realUrl = new URL(url);
connection = (HttpsURLConnection) realUrl.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Authorization", "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\"");
connection.setRequestProperty("X-WSSE", wsseHeader);
out = new OutputStreamWriter(connection.getOutputStream());
out.write(body);
out.flush();
int status = connection.getResponseCode();
InputStream is;
if (status == 200) {
is = connection.getInputStream();
} else {
is = connection.getErrorStream();
}
in = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
System.out.println(result.toString());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
if (connection != null) {
connection.disconnect();
}
}
}
/**
* 构造请求Body体
* @param sender
* @param receiver
* @param templateId
* @param templateParas
* @param statusCallBack
* @param signature | 签名名称,使用国内短信通用模板时填写
* @return
*/
static String buildRequestBody(String sender, String receiver, String templateId, String templateParas,
String statusCallBack, String signature) {
if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
|| templateId.isEmpty()) {
System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
return null;
}
Map<String, String> map = new HashMap<String, String>();
map.put("from", sender);
map.put("to", receiver);
map.put("templateId", templateId);
if (null != templateParas && !templateParas.isEmpty()) {
map.put("templateParas", templateParas);
}
if (null != statusCallBack && !statusCallBack.isEmpty()) {
map.put("statusCallback", statusCallBack);
}
if (null != signature && !signature.isEmpty()) {
map.put("signature", signature);
}
StringBuilder sb = new StringBuilder();
String temp = "";
for (String s : map.keySet()) {
try {
temp = URLEncoder.encode(map.get(s), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
sb.append(s).append("=").append(temp).append("&");
}
return sb.deleteCharAt(sb.length()-1).toString();
}
/**
* 构造X-WSSE参数值
* @param appKey
* @param appSecret
* @return
*/
static String buildWsseHeader(String appKey, String appSecret) {
if (null == appKey || null == appSecret || appKey.isEmpty() || appSecret.isEmpty()) {
System.out.println("buildWsseHeader(): appKey or appSecret is null.");
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
String time = sdf.format(new Date()); //Created
String nonce = UUID.randomUUID().toString().replace("-", ""); //Nonce
MessageDigest md;
byte[] passwordDigest = null;
try {
md = MessageDigest.getInstance("SHA-256");
md.update((nonce + time + appSecret).getBytes());
passwordDigest = md.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
//如果JDK版本是1.8,请加载原生Base64类,并使用如下代码
String passwordDigestBase64Str = Base64.getEncoder().encodeToString(passwordDigest); //PasswordDigest
//如果JDK版本低于1.8,请加载三方库提供Base64类,并使用如下代码
//String passwordDigestBase64Str = Base64.encodeBase64String(passwordDigest); //PasswordDigest
//若passwordDigestBase64Str中包含换行符,请执行如下代码进行修正
//passwordDigestBase64Str = passwordDigestBase64Str.replaceAll("[\\s*\t\n\r]", "");
return String.format(WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, time);
}
/*** @throws Exception
*/
static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return;
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return;
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
}
上述工具类 **
SendSmsUtil
是一个用于通过华为云短信服务发送短信验证码的工具类。它通过构建请求体和鉴权头信息,将短信发送请求发送到华为短信服务接口。该类包含了短信发送的核心逻辑,包括生成
X-WSSE
**头用于请求认证、构造请求体以及处理HTTPS连接的相关逻辑。同时,工具类还包含了信任所有HTTPS证书的设置,以确保与华为云服务器的安全连接。
- 发送验证码函数方法
public String sendSMS(SendSMSDTO sendSMSDTO) throws IOException {
String phone = sendSMSDTO.getPhone();
String captcha = generateCaptcha();
String redisKey = sendSMSDTO.getCaptchaType().equals(0)
? REDIS_REGISTER_CAPTCHA_KEY + phone
: REDIS_LOGIN_CAPTCHA_KEY + phone;
String message = sendSMSDTO.getCaptchaType().equals(0)
? "发送注册短信验证码:{}"
: "发送登录短信验证码:{}";
sendSmsUtil.sendSms(templateId, phone, "[\"" + captcha + "\"]");
log.info(message, captcha);
redisUtils.set(redisKey, captcha, 300);
return "发送短信成功";
}
上述代码实现了一个短信验证码发送流程。首先,通过**
generateCaptcha()
** 方法生成一个验证码,并调用**
sendSmsUtil.sendSms()
** 将验证码发送到用户的手机号码。短信发送后,利用日志记录了发送的验证码。接着,验证码被存储在 Redis 中,键为手机号加上特定前缀,且设置了300秒的有效期。最后,返回一个短信发送成功的消息。
**之后还有提交注册时的验证,这个较为简单,不做讲解,本来发送验证码函数我都不想写的╮(╯▽╰)╭。 **
3. 登录
3.1. 手机验证码登录流程
以下是对流程图的具体分析:
- 验证码发送流程:- 流程依然从用户请求验证码开始,后端接收手机号并生成验证码,通过短信服务平台(如阿里云、华为云)发送验证码。
- 验证码验证及登录提交:- 用户收到验证码后输入并提交登录请求,系统从Redis中获取存储的验证码,与用户输入的验证码进行匹配。- 如果验证码匹配失败,系统会提示用户验证码错误。
- 用户信息查询及Token生成:- 当验证码匹配成功后,系统会进一步查询用户信息,检查是否存在有效的用户账号。- 如果用户信息存在,系统生成Token完成登录,确保用户的身份验证。
3.2. 涉及到的Spring Security组件
要实现手机验证码登录,我们需要灵活使用Spring Security的认证流程,并在其中引入自定义的验证码验证逻辑。以下是关键的Spring Security组件及其在实现手机验证码登录时的作用:
1**. AuthenticationManager**
**
AuthenticationManager
**是Spring Security认证的核心组件,负责处理不同的认证请求。我们可以自定义一个 **
AuthenticationProvider
** 来处理手机验证码的认证逻辑,并将其注入到 **
AuthenticationManager
** 中。这样当用户提交验证码登录请求时,
AuthenticationManager
会调用我们的自定义认证提供者进行验证。
2.AuthenticationProvider
**
AuthenticationProvider
** 是处理认证逻辑的核心接口。为了支持手机验证码登录,我们需要实现一个自定义的 **
AuthenticationProvider
**,其中包含以下逻辑:
- 接收包含手机号和验证码的登录请求。
- 验证Redis中存储的验证码是否与用户输入的验证码匹配。
- 验证成功后,创建并返回 **
Authentication
**对象,表示用户已通过认证。
- UserDetailsService
**
UserDetailsService
**是Spring Security中用于加载用户信息的接口。我们可以通过实现 **
UserDetailsService
** 来查询和加载用户信息,比如通过手机号查询用户的详细信息(包括权限、角色等)。如果用户信息存在且验证码验证通过,系统将生成相应的 **
UserDetails
**对象,并将其与Spring Security的认证上下文进行关联。
- AuthenticationToken
在Spring Security中,**
AuthenticationToken
**是认证过程中传递用户凭据的对象。我们需要自定义一个 **
SmsAuthenticationToken
**,用于封装手机号和验证码,并传递给
AuthenticationProvider
进行处理。这个Token类需要继承自 **
AbstractAuthenticationToken
**,并包含手机号和验证码信息。
5.** SecurityConfigurerAdapter**
**
SecurityConfigurerAdapter
**是Spring Security配置的核心类,用于配置Spring Security的各种安全策略。为了集成手机验证码登录,我们需要扩展
SecurityConfigurerAdapter
并在其中配置我们的 **
AuthenticationProvider
**和自定义的登录过滤器。
- 自定义过滤器
为了支持手机验证码登录,我们可以自定义一个类似的过滤器 **
SmsAuthenticationFilter
**,在其中获取用户的手机号和验证码,然后交给 **
AuthenticationManager
**进行处理。这个过滤器将拦截验证码登录请求,并调用 **
AuthenticationProvider
**进行验证。
- SecurityContextHolder
**
SecurityContextHolder
**是Spring Security中用于存储当前认证信息的类。在用户成功通过验证码登录认证后,系统会将 **
Authentication
**对象存储到 **
SecurityContextHolder
** 中,表明当前用户已经成功登录。
3.3. 代码实现(仅核心)
3.3.1. 编写SmsAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String PHONE_KEY = "phone"; // 手机号字段
public static final String CAPTCHA_KEY = "captcha"; // 验证码字段
private boolean postOnly = true;
private final ObjectMapper objectMapper = new ObjectMapper();
public SmsAuthenticationFilter() {
super("/sms/login"); // 拦截短信验证码登录请求
}
@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());
}
String phone;
String captcha;
try {
// 读取请求体中的 JSON 数据并解析
Map<String, String> requestBody = objectMapper.readValue(request.getInputStream(), Map.class);
phone = requestBody.get(PHONE_KEY); // 获取手机号
captcha = requestBody.get(CAPTCHA_KEY); // 获取验证码
} catch (IOException e) {
throw new AuthenticationServiceException("Failed to parse authentication request body", e);
}
if (phone == null) {
phone = "";
}
if (captcha == null) {
captcha = "";
}
phone = phone.trim();
// 创建验证请求的 Token
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, captcha);
return this.getAuthenticationManager().authenticate(authRequest);
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
上述代码实现了一个 **
SmsAuthenticationFilter
**,用于处理短信验证码登录请求。它继承了 **
AbstractAuthenticationProcessingFilter
**,并在接收到 **
POST
**请求时从请求体中解析手机号和验证码的 JSON 数据,创建一个 **
SmsAuthenticationToken
**,然后通过 Spring Security 的认证管理器进行身份验证。如果请求不是 **
POST
**方法或解析 JSON 失败,会抛出相应的异常。
3.3.2. 编写SmsAuthenticationProvider
public class SmsAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final RedisUtils redisUtils;
public SmsAuthenticationProvider(UserDetailsService userDetailsService, RedisUtils redisUtils) {
this.userDetailsService = userDetailsService;
this.redisUtils = redisUtils;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phone = (String) authentication.getPrincipal(); // 获取手机号
String captcha = (String) authentication.getCredentials(); // 获取验证码
if(!redisUtils.hasKey(REDIS_LOGIN_CAPTCHA_KEY + phone)){
throw new BadCredentialsException("验证码已过期");
}
// 验证码是否正确
String redisCaptcha = redisUtils.get(REDIS_LOGIN_CAPTCHA_KEY + phone).toString();
if (redisCaptcha == null || !redisCaptcha.equals(captcha)) {
throw new BadCredentialsException("验证码错误");
}
// 验证用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(phone);
if (userDetails == null) {
throw new BadCredentialsException("未找到对应的用户,请先注册");
}
// 创建已认证的Token
return new SmsAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
上述代码实现了一个 **
SmsAuthenticationProvider
**,用于处理短信验证码登录的身份验证逻辑。它通过 **
UserDetailsService
**加载用户信息,并使用 **
RedisUtils
**从 Redis 中获取验证码进行比对。如果验证码不存在或不匹配,会抛出 **
BadCredentialsException
**异常。如果验证码正确且用户存在,则生成已认证的 **
SmsAuthenticationToken
**并返回,完成用户身份验证。该类还定义了它支持的身份验证类型为 **
SmsAuthenticationToken
**。
3.3.3. 编写SmsAuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public SmsAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal; // 用户的手机号
this.credentials = credentials; // 验证码
setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
上述代码实现了一个自定义的 **
SmsAuthenticationToken
**,继承自 **
AbstractAuthenticationToken
,用于表示短信验证码登录的认证信息。它包含用户的手机号 (
principal
) 和验证码 (
credentials
**) 两个字段,并提供两种构造方法:一种用于未认证的登录请求,另一种用于已认证的用户信息。通过
**getPrincipal**()
获取手机号,
**getCredentials**()
获取验证码,并且在调用
**eraseCredentials**()
时清除验证码以增强安全性。
3.3.4. 配置WebSecurityConfigurerAdapter
新增验证码过滤
// 添加短信验证码过滤器
http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
定义短信验证码认证过滤器,设置认证管理器及认证成功和失败的处理器。
@Bean
public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception {
SmsAuthenticationFilter filter = new SmsAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean()); // 设置认证管理器
filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler); // 设置成功处理器
filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler); // 设置失败处理器
return filter;
}
定义短信验证码认证提供者,注入用户详情服务和 Redis 工具类,用于处理短信验证码的认证逻辑。
@Bean
public SmsAuthenticationProvider smsAuthenticationProvider() {
return new SmsAuthenticationProvider(smeUserDetailsService,redisUtils);
}
配置认证管理器,添加短信验证码、微信登录以及用户名密码的认证提供者。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加短信验证码认证提供者
auth.authenticationProvider(smsAuthenticationProvider());
// 添加微信登录认证提供者
auth.authenticationProvider(weChatAuthenticationProvider());
// 添加用户名密码登录认证提供者
auth.authenticationProvider(daoAuthenticationProvider());
}
3.4. 效果测试
基于上述的手机验证码登录代码,我们来测试一下接口成果:
到此圆满完结✿✿ヽ(°▽°)ノ✿
4. 结语
通过以上步骤,我们成功实现了基于Spring Security的手机验证码登录功能。无论是注册流程中的验证码发送与验证,还是登录时的身份认证,Spring Security提供了足够的灵活性,让我们能够快速集成这项功能。在实际应用中,开发者可以根据自身需求进一步优化和扩展,比如增加更复杂的验证逻辑或增强安全性。希望本教程能帮助你轻松解决验证码登录的问题,让开发过程更加顺畅高效。
版权归原作者 后端小肥肠 所有, 如有侵权,请联系我们删除。