在线抽奖系统——管理员登录

devtools/2025/2/28 7:35:54/

目录

获取验证码

时序图

集成阿里云短信服务

SMSUtil

验证码生成

约定前后端交互接口

controller 层接口设计

Service 层接口设计

Redis

sendVerificationCode

getVerificationCode

接口测试

JWT

JWTUtil

定义拦截器

配置拦截路径

用户登录

时序图

验证码登录

密码登录

约定前后端交互接口

定义请求/响应类型

controller 层接口设计

service 层接口设计

login 接口实现

loginByShortMessage

loginByPassword

dao 层接口设计

登录接口测试

修改前端

登录功能验证


用户在进行登录时,有两种登录方式:

1. 验证码登录

2. 密码登录 

其中,验证码登录主要分为两个部分:

1. 输入手机号,获取验证码

2. 输入验证码,进行登录

而密码登录只需要:

输入手机号/邮箱 和 密码,进行登录

我们首先来看验证码登录中 获取验证码 的过程

获取验证码

时序图

在这里,我们使用 阿里云提供的短信服务 来完成短信发送 

我们来理解一下验证码发送的过程:

1. 用户点击获取验证码按钮,发送获取验证码请求

2. 服务器校验手机号格式,校验通过,生成随机验证码

3. 验证码生成后,我们使用阿里云提供的短信服务来将生成的验证码发送给用户

4. 服务器将生成的验证码存储在 redis 中,并设置其有效期为 5min

集成阿里云短信服务

要使用阿里云提供的短信服务,我们首先要 开通对应的短信服务

开通过程可参考:springboot集成阿里云短信服务_springboot整合阿里云短信-CSDN博客

添加类似短信通知模版:

 

 开通完成之后,我们需要将其集成到项目中,可参考官方文档:

短信服务_SDK中心-阿里云OpenAPI开发者门户

首先在 pom.xml 中引入依赖:

        <!--        阿里云短信服务--><dependency><groupId>com.aliyun</groupId><artifactId>dysmsapi20170525</artifactId><version>3.1.0</version></dependency>

通过官方提供的代码示例来进行学习:

java">// This file is auto-generated, don't edit it. Thanks.
package com.aliyun.sample;import com.aliyun.tea.*;public class Sample {/*** <b>description</b> :* <p>使用AK&amp;SK初始化账号Client</p>* @return Client* * @throws Exception*/public static com.aliyun.dysmsapi20170525.Client createClient() throws Exception {// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。// 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。.setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。.setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapiconfig.endpoint = "dysmsapi.ap-southeast-1.aliyuncs.com";return new com.aliyun.dysmsapi20170525.Client(config);}public static void main(String[] args_) throws Exception {java.util.List<String> args = java.util.Arrays.asList(args_);com.aliyun.dysmsapi20170525.Client client = Sample.createClient();com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest().setPhoneNumbers("your_value").setSignName("your_value");try {// 复制代码运行请自行打印 API 的返回值client.sendSmsWithOptions(sendSmsRequest, new com.aliyun.teautil.models.RuntimeOptions());} catch (TeaException error) {// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。// 错误 messageSystem.out.println(error.getMessage());// 诊断地址System.out.println(error.getData().get("Recommend"));com.aliyun.teautil.Common.assertAsString(error.message);} catch (Exception _error) {TeaException error = new TeaException(_error.getMessage(), _error);// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。// 错误 messageSystem.out.println(error.getMessage());// 诊断地址System.out.println(error.getData().get("Recommend"));com.aliyun.teautil.Common.assertAsString(error.message);}        }
}

其中,AccessKey ID AccessKey Secret 是通过配置环境变量的方式来获取的

因此,我们在 yml 文件中添加环境变量

java">sms:access-key-id: 自己的 AccessKey IDaccess-key-secret: 自己的 AccessKey Secretsign-name: 自己的签名名称

SMSUtil

实现 SMSUtil 工具类,提供 sendMessage 方法,用于发送短信:

java">@Slf4j
@Component
public class SMSUtil {@Value(value = "${sms.sign-name}")private String signName;@Value(value = "${sms.access-key-id}")private String accessKeyId;@Value(value = "${sms.access-key-secret}")private String accessKeySecret;/*** 发送短信** @param templateCode 模板号* @param phoneNumbers 手机号* @param templateParam 模板参数 {"key":"value"}*/public void sendMessage(String templateCode, String phoneNumbers, String templateParam) {try {// 初始化请求客户端Client client = createClient();// 构造请求对象,填入请求参数值SendSmsRequest sendSmsRequest = new SendSmsRequest().setSignName(signName).setTemplateCode(templateCode).setPhoneNumbers(phoneNumbers).setTemplateParam(templateParam);// 获取响应对象SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, new RuntimeOptions());// 判断短信是否发送成功if (null != response.getBody()&& null != response.getBody().getMessage()&& "OK".equals(response.getBody().getMessage())) {log.info("向 {} 发送信息成功,templateCode= {}", phoneNumbers, templateCode);return;}// 短信发送失败,打印错误原因log.error("向{}发送信息失败,templateCode={},失败原因:{}",phoneNumbers, templateCode, response.getBody().getMessage());} catch (TeaException error) {log.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);} catch (Exception _error) {TeaException error = new TeaException(_error.getMessage(), _error);log.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);}}/*** 使用AK&SK初始化账号Client* @return Client*/private Client createClient() throws Exception {// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。// 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。Config config = new Config()// 配置 AccessKey ID.setAccessKeyId(accessKeyId)// 配置 AccessKey Secret.setAccessKeySecret(accessKeySecret);// 配置 Endpoint 参考 https://api.aliyun.com/product/Dysmsapiconfig.endpoint = "dysmsapi.aliyuncs.com";return new Client(config);}
}

测试短信服务:

java">@SpringBootTest
public class SMSTest {@Autowiredprivate SMSUtil smsUtil;@Testvoid sendMessageTest() {// 模版参数: {"code": "1234"}smsUtil.sendMessage("自己的模版号","授权的手机号","{\"code\":\"2346\"}");}
}

观察发送是否成功,以及对应手机号是否能够收到短信

验证码生成

能够发送短信之后,接着需要能够生成随机验证码,我们借助 Hutool 来实现

可参考官方文档:概述 | Hutool

 实现 CaptchaUtil 工具类来生成验证码:

java">public class CaptchaUtil {/*** 生成 length 位验证码* @param length* @return*/public static String createCaptchaCode(int length) {RandomGenerator randomGenerator =  new RandomGenerator("0123456789", length);LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);lineCaptcha.setGenerator(randomGenerator);// 重新生成 codelineCaptcha.createCode();return lineCaptcha.getCode();}
}

在实现了 短信发送 和 验证码生成 之后,我们就可以来实现获取验证码的相关逻辑了

我们先约定前后端交互接口

约定前后端交互接口

[请求] GET /user/verification-code/send?phoneNumber=180xxx

[响应]

java">{"code": 200,"data": true,"errorMessage": ""
}

controller 层接口设计

UserController 中添加 sendVerificationCode 方法,用于处理发送验证码对应逻辑:

1. 日志打印

2. 调用 service 层对应方法,进行业务逻辑处理

3. 构造响应并返回

java">@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate VerificationCodeService verificationCodeService;/*** 发送验证码* @param phoneNumber* @return*/@RequestMapping("/verification-code/send")public CommonResult<Boolean> sendVerificationCode(String phoneNumber) {log.info("UserController sendVerificationCode 接收到参数 phoneNumber: {}", phoneNumber);verificationCodeService.sendVerificationCode(phoneNumber);return CommonResult.success(Boolean.TRUE);}
}

Service 层接口设计

接口定义:

java">@Service
public interface VerificationCodeService {/*** 发送验证码* @param phoneNumber*/void sendVerificationCode(String phoneNumber);/*** 获取验证码* @param phoneNumber* @return*/String getVerificationCode(String phoneNumber);
}

除了发送验证码,后续在使用验证码进行登录时,需要获取验证码,因此我们在这里一起进行实现

在存储验证码时,我们需要使用到 redis 

Redis

添加依赖:

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

配置端口转发:

Redis 服务器安装在云服务器上,而我们的程序运行在本地主机

因此,要让本地主机能够访问 redis,我们可以使用端口转发的方式,将服务器的 redis 端口映射到本地

xshell 中,进行配置:

1. 右键点击云服务器的会话,选择属性

2. 找到属性,配置转移规则

3. 使用该会话连接服务器

 

此时,访问本地的 9999 端口,就相当于访问对应服务器的 6379 端口

接着,我们在 yml 文件中配置 redis

java">spring:data:redis:host: localhostport: 9999timeout: 60slettuce:pool:max-active: 8max-idle: 8min-idle: 0max-wait: 5s

注意,xshell 和 服务器必须处于连接状态,此时的映射才是有效的

测试连接是否成功:

java">@SpringBootTest
public class RedisTest {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testvoid RedisConnectionTest() {stringRedisTemplate.opsForValue().set("key2", "key2");System.out.println(stringRedisTemplate.opsForValue().get("key2"));}
}

运行,观察运行结果以及是否存储成功

 

创建 RedisUtil 工具类,提供相关方法:

java">@Slf4j
@Component
public class RedisUtil {@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 设置超时时间* @param key* @param value* @param ttl* @param timeUnit 超时时间单位* @return*/public boolean set(String key, String value, Long ttl, TimeUnit timeUnit) {try {if (null != key && ttl > 0) {stringRedisTemplate.opsForValue().set(key, value, ttl, timeUnit);return true;}return false;} catch (Exception e) {log.error("RedisUtil set({}, {}, {}, {})", key, value, ttl, timeUnit, e);return false;}}/*** 设置超时时间,单位为 秒* @param key* @param value* @param ttl 单位为 秒* @return*/public boolean set(String key, String value, Long ttl) {return set(key, value, ttl, TimeUnit.SECONDS);}/*** 不设置超时时间* @param key* @param value* @return*/public boolean set(String key, String value) {try {if (null != key) {stringRedisTemplate.opsForValue().set(key, value);return true;}return false;} catch (Exception e) {log.error("RedisUtil set({}, {})", key, value, e);return false;}}/*** 获取 key 对应 value* @param key* @return*/public String get(String key) {try {if (null != key) {return stringRedisTemplate.opsForValue().get(key);}return null;} catch (Exception e) {log.error("RedisUtil get({})", key, e);return null;}}/*** 判断 key 是否存在* @param key* @return*/public boolean hasKey(String key) {try {return stringRedisTemplate.hasKey(key);} catch (Exception e) {log.error("RedisUtil hasKey({})", key, e);return false;}}/*** 为 key 设置超时时间,单位为 timeUnit* @param key* @param ttl* @param timeUnit* @return*/public boolean setExpire(String key, long ttl, TimeUnit timeUnit) {try {if (null != key && ttl > 0) {return stringRedisTemplate.expire(key, ttl, timeUnit);}return false;} catch (Exception e) {log.error("RedisUtil setExpire({}, {}, {})", key, ttl, timeUnit);return false;}}/*** 设置超时时间,单位为 秒* @param key* @param ttl* @return*/public boolean setExpire(String key, long ttl) {return setExpire(key, ttl, TimeUnit.SECONDS);}/*** 获取 key 的超时时间* 获取失败时返回 -1* @param key* @return*/public long getExpire(String key) {try {if (null != key) {return stringRedisTemplate.getExpire(key);}return -1L;} catch (Exception e) {log.error("RedisUtil getExpire({})", key);return -1L;}}/*** 删除 key,删除成功,返回删除个数,删除失败,返回 -1* @param key* @return*/public long del(String... key) {try {if (null != key && key.length > 0) {if (key.length == 1) {return stringRedisTemplate.delete(key[0]) ? 1: 0;} else {return stringRedisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));}}return -1L;} catch (Exception e) {log.error("RedisUtil del({})", Arrays.toString(key));return -1L;}}
}

测试对应方法:

java">@SpringBootTest
public class RedisTest {@Autowiredprivate RedisUtil redisUtil;@Testvoid redisUtilTest() throws InterruptedException {System.out.println("set key3: " + redisUtil.set("key3", "key3", 2L));System.out.println("getExpire key3: " + redisUtil.getExpire("key3"));Thread.sleep(2000);System.out.println("get key3: " + redisUtil.get("key3"));System.out.println("hasKey key3: " + redisUtil.hasKey("key3"));System.out.println("set key4:" + redisUtil.set("key4", "key4"));System.out.println("get key4: " + redisUtil.get("key4"));System.out.println("del key3, key4: " + redisUtil.del("key3", "key4"));System.out.println("get key4: " + redisUtil.get("key4"));}
}

观察运行结果:

接着,我们继续实现验证码的发送和获取

sendVerificationCode

1. 校验手机号是否符合要求

2. 生成验证码

3. 发送验证码

4. 将验证码存储到 redis 中

java">@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {@Autowiredprivate SMSUtil smsUtil;@Autowiredprivate RedisUtil redisUtil;private static final int CAPTCHA_CODE_LENGTH = 4;private static final String TEMPLATE_CODE = "自己的模版号";private static final Long CAPTCHA_CODE_TIMEOUT = 5 * 60L; // 单位为 sprivate static final String CAPTCHA_CODE_REDIS_PREFIX = "captcha_code_";@Overridepublic void sendVerificationCode(String phoneNumber) {// 校验手机号是否正确if (!StringUtils.hasText(phoneNumber) || !RegexUtil.checkMobile(phoneNumber)) {throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);}// 生成验证码String captcha = CaptchaUtil.createCaptchaCode(CAPTCHA_CODE_LENGTH);// 发送验证码Map<String, String> templateParam = new HashMap<>();templateParam.put("code", captcha);smsUtil.sendMessage(TEMPLATE_CODE, phoneNumber, JacksonUtil.writeValueAsString(templateParam));log.info("向用户 {} 发送验证码 {}", phoneNumber, captcha);// 将验证码存储到 redis 中redisUtil.set(CAPTCHA_CODE_REDIS_PREFIX + phoneNumber, captcha, CAPTCHA_CODE_TIMEOUT);}
}

getVerificationCode

1. 校验手机号是否符合要求

2. 从 redis 中获取验证码

java">    @Overridepublic String getVerificationCode(String phoneNumber) {// 校验手机号是否正确if (!StringUtils.hasText(phoneNumber) || !RegexUtil.checkMobile(phoneNumber)) {throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);}// 从 redis 中获取验证码 并返回return redisUtil.get(CAPTCHA_CODE_REDIS_PREFIX + phoneNumber);}

实现完成后,我们运行程序,使用 postman 来对接口进行测试

接口测试

观察 redis 中是否存储成功:

JWT

在登录之后,访问其他页面之前我们需要对用户的身份进行验证,判断其是否已经登录

在这里,我们使用 JWT 令牌来进行身份校验

更多可参考JWT令牌技术_keys.hmacshakeyfor-CSDN博客

JWTUtil

实现 JWTUtil 工具类,提供令牌的生成、校验,以及从令牌中获取用户 id 方法:

java">@Slf4j
public class JWTUtils {private static final String JWT_SECRET_KEY_STRING = "Snqhyg+gzcvCpmwNE8m46AumlYkolbLXO0/5FXT8eZc="; // 密钥内容private static final Key JWT_SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(JWT_SECRET_KEY_STRING)); // 用于加密的密钥private static final long JWT_EXPIRATION = 60 * 60 * 1000; // 超时时间 1hprivate static final String JWT_USER_ID = "userId";/*** 生成令牌* @param claim* @return*/public static String genToken(Map<String, Object> claim) {if (null == claim) {return null;}try {String token = Jwts.builder().setClaims(claim) // 自定义信息.setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis()+ JWT_EXPIRATION)).signWith(JWT_SECRET_KEY).compact();return token;} catch (Exception e) {log.error("JWTUtils genToken 生成令牌失败", e);return null;}}/*** 解析令牌* @param token* @return*/public static Claims parseToken(String token) {if (null == token) {return null;}try {JwtParser build = Jwts.parserBuilder().setSigningKey(JWT_SECRET_KEY).build();Claims claims = build.parseClaimsJws(token).getBody();return claims;} catch (Exception e) {log.error("JWTUtils parseToken 解析 token: {} 异常, ", token, e);return null;}}/*** 从令牌中获取用户 id* @param token* @return*/public static Integer getIdByToken(String token) {Claims claims = parseToken(token);return claims == null ? null : (Integer) claims.get(JWT_USER_ID);}
}

定义拦截器

拦截器可参考:拦截器(Interceptor)_implements interceptor-CSDN博客

java">@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {private static final String USER_TOKEN = "user_token";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求头String token = request.getHeader(USER_TOKEN);// 打印获取信息log.info("获取 token: {}", USER_TOKEN);log.info("获取路径: {}", request.getRequestURI());// 解析令牌Claims claims = JWTUtils.parseToken(token);if (null == claims) {log.error("JWT 令牌解析失败!");response.setStatus(401);return false;}log.info("解析 JWT 令牌成功!");return true;}
}

配置拦截路径

java">@Configuration
public class WebMvcConfigurer implements org.springframework.web.servlet.config.annotation.WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;public final List<String> excludes = Arrays.asList("/**/*.html","/css/**","/js/**","/pic/**","/*.jpg","/*.png","/favicon.ico","/user/**/login","/user/register","/user/verification-code/send");@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns(excludes);}
}

用户登录

时序图

验证码登录

验证码登录过程:

1. 客户端发送验证码登录请求

2. 服务器校验登录信息是否符合要求

3. 从 MySQL 中获取用户信息

4. 校验用户的身份信息

5. 校验验证码是否正确

6. 生成 token 令牌

7. 构造响应并返回 

密码登录

 

密码登录过程:

1. 客户端发送密码登录请求

2. 服务器校验登录信息

3. 从 MySQL 中获取用户信息

4. 校验用户身份信息

5. 校验用户密码是否正确

6. 生成 token 令牌

7. 构造响应并返回

约定前后端交互接口

验证码登录请求:

POST /user/message/login

java">{"loginMobile": "13333333333","verificationCode": "8904","mandatoryIdentify": "ADMIN"
}

密码登录请求:

POST /user/password/login

java">{"loginName": "13333333333","password": "13333333333","mandatoryIdentify": "ADMIN"
}

返回的响应类型是相同的:

java">{"code": 200,"data": {"token": "eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGlmeSI6IkFETUlOIiwibmFtZSI6ImdnZyIsImlkIjo0OCwiaWF0IjoxNzM1NDUyODY3LCJleHAiOjE3MzU0NTY0Njd9.bjfmvANln7UCCiXB4gxhcmjno2geISB7bH-sWBW1j9k","identify": "ADMIN"},"errorMessage": ""
}

定义请求/响应类型

无论是验证码登录,还是手机号登录,都需要指定用户身份信息,因此,我们将其抽取出来:

java">@Data
public class UserLoginParam implements Serializable {/*** 用户身份* @see UserIdentityEnum#name()*/@NotBlank(message = "用户身份信息不能为空")private String mandatoryIdentity;
}

 定义 java">UserLoginParam ,也方便我们后续对不同的登录方式进行统一的处理

验证码登录请求:

java">@Data
@EqualsAndHashCode(callSuper = true)
public class ShortMessageLoginParam extends UserLoginParam {@NotBlank(message = "手机号不能为空")private String loginMobile;@NotBlank(message = "验证码不能为空")private String verificationCode;
}

密码登录请求:

java">@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam {@NotBlank(message = "用户名不能为空")private String loginName;@NotBlank(message = "密码不能为空")private String password;}

登录响应:

java">@Data
public class UserLoginResult implements Serializable {/*** JWT 令牌*/private String token;/*** 用户身份信息*/private String identify;
}

controller 层接口设计

无论是密码登录还是验证码登录,其处理过程都是类似的:

1. 打印接收到的参数信息

2. 调用 service 层方法进行登录处理

3. 判断业务是否处理成功

4. 构造响应结果并返回

java">    /*** 密码登录* @param param* @return*/@RequestMapping("/password/login")public CommonResult<UserLoginResult> loginByPassword(@Validated @RequestBody UserPasswordLoginParam param) {// 日志打印log.info("UserController loginByPassword 接收到参数 UserPasswordLoginParam: {}", JacksonUtil.writeValueAsString(param));// 业务处理UserLoginDTO userLoginDTO = userService.login(param);// 业务是否处理成功if (null == userLoginDTO) {throw new ControllerException(ControllerErrorCodeConstants.LOGIN_ERROR);}// 构造结果并返回return CommonResult.success(convertToLoginResult(userLoginDTO));}/*** 验证码登录* @param param* @return*/@RequestMapping("/message/login")public CommonResult<UserLoginResult> loginByShortMessage(@Validated @RequestBody ShortMessageLoginParam param) {// 日志打印log.info("UserController loginByShortMessage 接收到参数 ShortMessageLoginParam: {}", JacksonUtil.writeValueAsString(param));// 业务处理UserLoginDTO userLoginDTO = userService.login(param);// 业务是否处理成功if (null == userLoginDTO) {throw new ControllerException(ControllerErrorCodeConstants.LOGIN_ERROR);}// 构造结果并返回return CommonResult.success(convertToLoginResult(userLoginDTO));}

UserLoginDTO: 

java">@Data
public class UserLoginDTO implements Serializable {/*** JWT 令牌*/private String token;/*** 登录人员身份信息*/private UserIdentityEnum identity;
}

结果类型转化:

java">    /*** 结果类型转化* 将 UserLoginDTO 类型转化为 UserLoginResult* @param userLoginDTO* @return*/private UserLoginResult convertToLoginResult(UserLoginDTO userLoginDTO) {if (null == userLoginDTO) {return null;}UserLoginResult userLoginResult = new UserLoginResult();userLoginResult.setToken(userLoginDTO.getToken());userLoginResult.setIdentify(userLoginDTO.getIdentity().name());return userLoginResult;}

添加错误码:

java">public interface ControllerErrorCodeConstants {// ---------------------- 用户模块错误码 ----------------------ErrorCode REGISTER_ERROR = new ErrorCode(100, "注册失败");ErrorCode LOGIN_ERROR = new ErrorCode(101, "登录失败");
}

service 层接口设计

login 接口

java">    /*** 登录接口* @param param* @return*/UserLoginDTO login(UserLoginParam param);

login 接口实现

1. 参数校验

2. 判断登录方式,调用对应方法进行登录

3. 返回响应结果

java">    /*** 用户登录* @param param* @return*/@Overridepublic UserLoginDTO login(UserLoginParam param) {UserLoginDTO userLoginDTO = null;// 登录if (param instanceof UserPasswordLoginParam) {// 密码登录userLoginDTO = loginByPassword((UserPasswordLoginParam) param);} else if (param instanceof ShortMessageLoginParam) {// 短信登录userLoginDTO = loginByShortMessage((ShortMessageLoginParam) param);} else {throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_ERROR);}return userLoginDTO;}

 对于接收到的参数,我们使用 Validation 进行校验过了,因此在这里就不再进行校验了

 接下来,我们就分别来实现 验证码登录 密码登录 的具体逻辑

loginByShortMessage

1. 校验手机号

2. 获取用户信息

3. 校验身份信息

4. 获取验证码,并对其进行校验

5. 生成 JWT 令牌

6. 构造结果并返回

java">@Slf4j
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate VerificationCodeService verificationCodeService;private static final String JWT_USER_ID = "id";private static final String JWT_USER_NAME = "name";private static final String JWT_USER_IDENTIFY = "identify";/*** 短信登录* @param param* @return*/private UserLoginDTO loginByShortMessage(ShortMessageLoginParam param) {// 手机号校验if (!RegexUtil.checkMobile(param.getLoginMobile())) {throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);}// 获取用户信息UserDO userDO = userMapper.selectByPhoneNumber(new PhoneEncrypt(param.getLoginMobile()));if (null == userDO) {throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);}// 校验身份信息if (StringUtils.hasText(param.getMandatoryIdentity())&& !param.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);}// 验证码校验String code = verificationCodeService.getVerificationCode(param.getLoginMobile()); // 获取存储的验证码if (!param.getVerificationCode().equals(code)) {throw new ServiceException(ServiceErrorCodeConstants.VERIFICATION_CODE_ERROR);}// 生成 JWT 令牌Map<String, Object> claims = new HashMap<>();claims.put(JWT_USER_ID, userDO.getId());claims.put(JWT_USER_NAME, userDO.getUserName());claims.put(JWT_USER_IDENTIFY, userDO.getIdentity());String token = JWTUtils.genToken(claims);// 构造响应并返回if (null == token) {throw new ServiceException(ServiceErrorCodeConstants.JWT_TOKEN_ACQUIRE_ERROR);}UserLoginDTO userLoginDTO = new UserLoginDTO();userLoginDTO.setToken(token);userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));return userLoginDTO;}
}

loginByPassword

1. 判断使用手机号登录还是邮箱登录

2. 根据 手机号 / 邮箱 查询用户信息

3. 校验用户身份信息

4. 校验用户密码

5. 生成 JWT 令牌

6. 构造结果并返回

java">@Slf4j
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate VerificationCodeService verificationCodeService;private static final String JWT_USER_ID = "id";private static final String JWT_USER_NAME = "name";private static final String JWT_USER_IDENTIFY = "identify";/*** 密码登录* @param param* @return*/private UserLoginDTO loginByPassword(UserPasswordLoginParam param) {UserDO userDO = null;// 判断使用手机号登录还是邮箱登录if (RegexUtil.checkMail(param.getLoginName())) { // 邮箱登录// 根据邮箱查询userDO = userMapper.selectByEmail(param.getLoginName());} else if (RegexUtil.checkMobile(param.getLoginName())) { // 根据手机号查询// 手机号查询userDO = userMapper.selectByPhoneNumber(new PhoneEncrypt(param.getLoginName()));} else {throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_ERROR);}if (null == userDO) {throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);} else if (StringUtils.hasText(param.getMandatoryIdentity())&& !param.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {// 校验身份信息throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);} else if (!SecurityUtil.verifyPassword(param.getPassword(), userDO.getPassword())) {// 校验密码throw new ServiceException(ServiceErrorCodeConstants.LOGIN_PASSWORD_ERROR);}// 生成 JWT 令牌Map<String, Object> claims = new HashMap<>();claims.put(JWT_USER_ID, userDO.getId());claims.put(JWT_USER_NAME, userDO.getUserName());claims.put(JWT_USER_IDENTIFY, userDO.getIdentity());String token = JWTUtils.genToken(claims);// 构造响应并返回if (null == token) {throw new ServiceException(ServiceErrorCodeConstants.JWT_TOKEN_ACQUIRE_ERROR);}UserLoginDTO userLoginDTO = new UserLoginDTO();userLoginDTO.setToken(token);userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));return userLoginDTO;}
}

添加对应错误码:

java">public interface ServiceErrorCodeConstants {// ---------------------- 用户模块错误码 ----------------------ErrorCode REGISTER_INFO_IS_EMPTY = new ErrorCode(100, "用户注册信息为空");ErrorCode MAIN_ERROR = new ErrorCode(101, "用户邮箱信息有误");ErrorCode PHONE_NUMBER_ERROR = new ErrorCode(102, "用户手机号信息有误");ErrorCode IDENTITY_ERROR = new ErrorCode(103, "用户身份信息有误");ErrorCode PASSWORD_ERROR = new ErrorCode(104, "管理员用户密码信息有误");ErrorCode MAIL_USED = new ErrorCode(105, "用户邮箱已被使用");ErrorCode PHONE_NUMBER_USED = new ErrorCode(106, "用户手机号已存在");ErrorCode LOGIN_INFO_ERROR = new ErrorCode(107, "用户登录信息有误");ErrorCode VERIFICATION_CODE_ERROR = new ErrorCode(108, "验证码错误");ErrorCode USER_INFO_IS_EMPTY = new ErrorCode(109, "用户信息为空");ErrorCode JWT_TOKEN_ACQUIRE_ERROR = new ErrorCode(110, "JWT令牌获取失败");
}

dao 层接口设计

UserMapper 中添加通过 邮箱 和 手机号 查询用户信息方法:

java">    /*** 通过手机号查询用户信息* @param phoneNumber* @return*/@Select("select id, gmt_create, gmt_modified, user_name, email, phone_number, password, identity from user where phone_number = #{phoneNumber}")UserDO selectByPhoneNumber(PhoneEncrypt phoneNumber);/*** 通过邮箱查询用户信息* @param email* @return*/@Select("select id, gmt_create, gmt_modified, user_name, email, phone_number, password, identity from user where email = #{email}")UserDO selectByEmail(String email);

登录接口测试

验证码登录:

在登录之前需要获取验证码(注意验证码的有效时间)

密码登录:

 

修改前端

使用 ajax 发送请求

验证码发送:

javascript">  function getCode(){$.ajax({url: '/user/verification-code/send',type: 'GET',data:{ phoneNumber:$('#loginMobile').val() },success: function(result) {console.log(result);if (result.code != 200) {timer&&clearInterval(timer)$('#getVerificationCode').text('重新获取')toastr.error('验证码获取失败');} else {toastr.success('验证码发送成功')}},});}

 验证码登录:

javascript">  // 验证码登录$("#codeForm").validate({rules: {loginMobile: "required",verificationCode:"required",},messages: {loginMobile: "请输入您的手机号",verificationCode:  "请输入验证码",},submitHandler: function(form) {var loginMobile = $('#loginMobile').val();var verificationCode = $('#verificationCode').val();// 清除之前的错误消息$('.error').text('');// 发送AJAX请求$.ajax({url: '/user/message/login',type: 'POST',contentType: 'application/json',data: JSON.stringify({loginMobile: loginMobile ,verificationCode: verificationCode,mandatoryIdentity: "ADMIN"}),success: function(result) {console.log(result);if (result.code != 200) {alert("登录失败, 失败原因" +  result.errorMessage);} else {localStorage.setItem("user_token", result.data.token);localStorage.setItem("user_identity", "ADMIN");location.assign("admin.html");}},});return false; // 阻止表单的默认提交行为}});

密码登录:

java">  $("#loginForm").validate({rules: {phoneNumber: "required",password: {required: true,minlength: 6}},messages: {phoneNumber: "请输入您的手机号",password: {required: "请输入密码",minlength: "密码长度至少为6个字符"}},submitHandler: function(form) {var loginName = $('#phoneNumber').val();var password = $('#password').val();// 清除之前的错误消息$('.error').text('');// 发送AJAX请求$.ajax({url: '/user/password/login',type: 'POST',contentType: 'application/json',data: JSON.stringify({loginName: loginName, password: password, mandatoryIdentity: "ADMIN"}),success: function(result) {if (result.code != 200) {alert("登录失败, 失败原因" +  result.errorMessage);} else {localStorage.setItem("user_token", result.data.token);localStorage.setItem("user_identity", "ADMIN");location.assign("admin.html");}},});return false; // 阻止表单的默认提交行为}});

登录功能验证

运行程序,访问登录页面:

​​​​​​​

验证码登录:

密码登录:

登录后跳转:

异常情况在这里就不再一一进行演示了


http://www.ppmy.cn/devtools/163297.html

相关文章

Ubuntu+deepseek+Dify本地部署

1.deepseek本地部署 在Ollama官网下载 需要魔法下载 curl -fsSL https://ollama.com/install.sh | sh 在官网找到需要下载的deepseek模型版本 复制命令到终端 ollama run deepseek-r1:7b 停止ollama服务 sudo systemctl stop ollama # sudo systemctl stop ollama.servi…

MySQL之解决表中存储类型为[1,2,3]这样的字符串中去除括号[]和逗号‘,‘的问题(FIND_IN_SET+replace)

bug&#xff1a;筛选条件时&#xff0c;筛选出了不符合电影类型的影片 问题如下&#xff1a; 数据库的film表中的字段type_ids类型是varchar&#xff0c;他用来存储电影的类型id&#xff0c;如&#xff1a;type_ids里面存的是[1,12,15]&#xff0c;说明他存的是电影类型中id为…

[Java基础] JVM常量池介绍(BeanUtils.copyProperties(source, target)中的属性值引用的是同一个对象吗)

文章目录 1. JVM内存模型2. 常量池中有什么类型&#xff1f;3. 常量池中真正存储的内容是什么4. 判断一个字符串(引用)是否在常量池中5. BeanUtils.copyProperties(source, target)中的属性值引用的是同一个对象吗&#xff1f;6. 获取堆内存使用情况、非堆内存使用情况 1. JVM内…

火绒终端安全管理系统V2.0网络防御功能介绍

网络防御是指通过一系列技术、策略和措施&#xff0c;保护网络系统、数据和资源免受未经授权的访问、攻击、破坏或泄露。 火绒终端安全管理系统&#xff1a;网络防御功能包含网络入侵拦截、横向渗透防护、对外攻击检测、僵尸网络防护、Web服务保护、暴破攻击防护、远程登录防护…

【SpringBoot】论坛项目中如何进行实现发布文章,以及更新对应数据库的数据更新

前言 &#x1f31f;&#x1f31f;本期讲解关于websocket的相关知识介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话不多…

C#实现本地Deepseek模型及其他模型的对话

前言 1、C#实现本地AI聊天功能 WPFOllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。 2、此程序默认你已经安装好了Ollama。 在运行前需要线安装好Ollama,如何安装请自行搜索 Ollama下载地址&#xff1a; https://ollama.org.cn Ollama模型下载地址&#xf…

如何成为一名合格的单片机工程师----引言介绍篇(1)

前言 在当今数字化时代&#xff0c;单片机&#xff08;Microcontroller Unit&#xff0c;MCU&#xff09;已成为电子设备的核心组件之一&#xff0c;广泛应用于智能家居、工业自动化、汽车电子、物联网等领域。作为一名单片机工程师&#xff0c;你将有机会参与到各种创新项目中…

可编辑PPT | DeepSeek如何赋能职场应用

这个PPT的核心内容是介绍DeepSeek如何赋能职场应用&#xff0c;从提示语技巧到多场景应用的详细解读。PPT首先介绍了DeepSeek的背景和团队&#xff0c;展示了其在AI领域的多项赛事奖项和研究成果&#xff0c;突出了其在人机协同和人机共生领域的专业能力。接着&#xff0c;PPT详…