【尚庭公寓springboot + vue的项目实战】- 移动端登录模块的实现
技术选型与核心组件
1. JWT(JSON Web Token)认证方案
什么是Token?
-
令牌本质:代表用户身份和权限的安全字符串,用于客户端与服务端间的身份验证
-
JWT结构:
textHeader.Payload.Signature-
Header
Base64Url编码的JSON对象,声明令牌类型和签名算法:
json{ "alg": "HS256", "typ": "JWT" } -
Payload
存储用户声明信息,支持标准字段与自定义字段:
json{ "sub": "userInfo", "name": "John Doe", "iat": 1516239022 } -
Signature
使用Header指定算法对前两部分签名,确保数据完整性
-
2. 手机验证码实现
- 技术工具:阿里云短信接口
- 存储方案:Redis单节点存储验证码
3. 登录状态管理
- ThreadLocal:保存Token解析后的用户上下文信息
- Redis:存储验证码及登录态关键数据
核心功能实现详解
一、 登录流程
移动端的具体登录流程如下图所示
二、短信验证码获取
该接口需向登录手机号码发送短信验证码,各大云服务厂商都提供短信服务,本项目使用阿里云完成短信验证码功能,下面介绍具体配置。
-
配置短信服务
-
创建AccessKey
云账号 AccessKey 是访问阿里云 API 的密钥,没有AccessKey无法调用短信服务。点击页面右上角的头像,选择AccessKey管理 ,然后创建AccessKey。
1. 依赖配置
如需调用阿里云的短信服务,需使用其提供的SDK,具体可参考官方文档。
xml
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
</dependency>
2.属性配置
配置发送短信客户端
-
在
application.yml中增加如下内容yamlaliyun: sms: access-key-id: <access-key-id> access-key-secret: <access-key-secret> endpoint: dysmsapi.aliyuncs.com
注意 :上述access-key-id、access-key-secret需根据实际情况进行修改
- 添加属性配置类 :在
com.atguigu.lease.common.sms创建AliyunSMSProperties类,内容如下
java
@Component
@Data
@ConfigurationProperties(prefix = "aliyun.sms")
public class AliyunSMSProperties {
private String accessKeyId;
private String accessKeySecret;
private String endpoint;
}
-
在common模块 中创建
com.atguigu.lease.common.sms.AliyunSmsConfiguration类,内容如下java@Configuration @EnableConfigurationProperties(AliyunSMSProperties.class) @ConditionalOnProperty(prefix = "aliyun.sms", name = "endpoint") public class AliyunSMSConfiguration { @Autowired private AliyunSMSProperties aliyunSMSProperties; @Bean public Client client(){ try { Config config = new Config(); config.setAccessKeyId(aliyunSMSProperties.getAccessKeyId()); config.setAccessKeySecret(aliyunSMSProperties.getAccessKeySecret()); config.setEndpoint(aliyunSMSProperties.getEndpoint()); return new Client(config); } catch (Exception e) { e.printStackTrace(); } return null; } }
3.编写逻辑代码
-
编写Controller层逻辑
在
LoginController中增加如下内容
java
@GetMapping("login/getCode")
@Operation(summary = "获取短信验证码")
public Result getCode(@RequestParam String phone) {
loginService.getCode(phone);
return Result.ok();
}
-
编写Service层逻辑
-
编写发送代码逻辑
-
在
SmsService中增加如下内容javavoid sendSms(String phone, String code); -
在
SmsServiceImpl中增加如下内容java@Service public class SmsServiceImpl implements SmsService { @Autowired private Client client; @Override public void sendSms(String phone, String code) { try { SendSmsRequest sendSmsRequest = new SendSmsRequest() .setSignName("阿里云短信测试") .setTemplateCode("SMS_154950909") .setPhoneNumbers(phone) .setTemplateParam("{\"code\":\""+code+"\"}"); client.sendSms(sendSmsRequest); } catch (Exception e) { e.printStackTrace(); } } }
-
-
-
编写生成随机验证码逻辑
在common模块 中创建
com.atguigu.lease.common.utils.VerifyCodeUtil类,内容如下javapackage com.atguigu.lease.common.utils; import java.util.Random; /** * ClassName: VerifyCodeUtil * Description: 生成验证码的工具类 * * @Author linz * @Creat 2025/2/11 23:12 * @Version 1.00 */ public class VerifyCodeUtil { public static String generateVerifyCode(int len){ StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i++) { sb.append(new Random().nextInt(10)); } return sb.toString(); } }-
编写获取短信验证码逻辑
-
在
LoginServcie中增加如下内容javavoid getSMSCode(String phone); -
在
LoginServiceImpl中增加如下内容java@Override public void getCode(String phone) { if(StringUtils.isEmpty(phone)){ throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY); } //验证码的key String key = RedisConstant.APP_LOGIN_PREFIX+phone; //redis中是否已经包含手机的key Boolean result = redisTemplate.hasKey(key); if(result){ //该手机号发送过短信验证码且在redis中尚未过期 //继续判断验证码是否是一分钟内发送的 //获取验证码还有多长时间过期 Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS); //验证码过期时间(总的时间) Long totalTime=RedisConstant.APP_LOGIN_CODE_TTL_SEC * 60 * 60L; if(totalTime-expire<RedisConstant.APP_LOGIN_CODE_RESEND_TIME_SEC){ throw new LeaseException(ResultCodeEnum.APP_SEND_SMS_TOO_OFTEN); } } //生成随机6位的验证码 String code = VerifyCodeUtil.generateVerifyCode(6); //发送手机验证码 smsService.sendSms(phone,code); //将验证码存储到redis中(存储过期时间为60 * 10小时,不用重复扣费) redisTemplate.opsForValue().set(key,code, RedisConstant.APP_LOGIN_CODE_TTL_SEC, TimeUnit.HOURS); }
-
-
三、登录和注册接口
1.登录注册校验逻辑
-
登录注册校验逻辑
-
前端发送手机号码
phone和接收到的短信验证码code到后端。 -
首先校验
phone和code是否为空,若为空,直接响应手机号码为空或者验证码为空,若不为空则进入下步判断。 -
根据
phone从Redis中查询之前保存的验证码,若查询结果为空,则直接响应验证码已过期,若不为空则进入下一步判断。 -
比较前端发送的验证码和从Redis中查询出的验证码,若不同,则直接响应
验证码错误,若相同则进入下一步判断。 -
使用
phone从数据库中查询用户信息,若查询结果为空,则创建新用户,并将用户保存至数据库,然后进入下一步判断。 -
判断用户是否被禁用,若被禁,则直接响应
账号被禁用,否则进入下一步。 -
创建JWT并响应给前端。
-
2.编写逻辑代码
-
接口实现
-
编写Controller层逻辑
在
LoginController中增加如下内容java@PostMapping("login") @Operation(summary = "登录") public Result<String> login(@RequestBody LoginVo loginVo) { String token = loginService.login(loginVo); return Result.ok(token); } -
编写Service层逻辑
在
LoginService中增加如下内容javaString login(LoginVo loginVo);在
LoginServiceImpl总增加如下内容java@Override public String login(LoginVo loginVo) { String phone = loginVo.getPhone(); //判断手机号码是否为空 if(!StringUtils.hasText(phone)){ throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY); } String code = loginVo.getCode(); //判断验证码是否为空 if(!StringUtils.hasText(code)){ throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EMPTY); } //判断验证码是否正确 //获取redis中的验证码 String key = RedisConstant.APP_LOGIN_PREFIX+phone; String redisCode = redisTemplate.opsForValue().get(key); if(redisCode==null){ throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EXPIRED); } if(!redisCode.equals(code)){ throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_ERROR); } //查询手机号的用户是否注册为会员 UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getPhone, phone)); if(userInfo==null){ userInfo=new UserInfo(); userInfo.setPhone(phone); userInfo.setStatus(BaseStatus.ENABLE); userInfo.setNickname("用户-"+phone.substring(5)); userInfoMapper.insert(userInfo); } if(userInfo.getStatus()==BaseStatus.DISABLE){ throw new LeaseException(ResultCodeEnum.APP_ACCOUNT_DISABLED_ERROR); } //登录成功,创建token String token = JwtUtil.createToken(phone, userInfo.getId()); return token; }
-
3.实现简化登录
-
添加拦截器
-
在
om.atguigu.lease.web.app.custom.interceptor中创建AuthenticationInterceptor类添加以下内容
java@Component public class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("access_token"); if(token==null){ throw new LeaseException(ResultCodeEnum.APP_LOGIN_AUTH); }else{ //解析token Claims claims = JwtUtil.parseToken(token); String phone = claims.get("userName", String.class); Long userId = claims.get("userId", Long.class); //将负载信息保存到ThreadLocal中 LoginUser loginUser = new LoginUser(userId,phone); LoginUserContext.set(loginUser); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //将保存到ThreadLocal中的token的负载信息删除 LoginUserContext.remove(); } }
-
-
注册AuthenticationInterceptor(前期可以先不添加拦截器,不方便测试)
在web-app模块 创建
com.atguigu.lease.web.app.custom.config.WebMvcConfiguration,内容如下java@Configuration public class WebMvcConfiguration implements WebMvcConfigurer { @Autowired AuthenticationInterceptor authenticationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor).addPathPatterns("/app/**").excludePathPatterns("/app/login/**"); } }