【尚庭公寓SpringBoot + Vue 项目实战】移动端登录管理(二十)
文章目录
- 【尚庭公寓SpringBoot + Vue 项目实战】移动端登录管理(二十)
- 1、登录业务
- 2、接口开发
- 2.1、获取短信验证码
- 2.2、登录和注册接口
- 2.3、查询登录用户的个人信息
1、登录业务
登录管理共需三个接口,分别是获取短信验证码、登录、查询登录用户的个人信息。除此之外,同样需要编写HandlerInterceptor
来为所有受保护的接口增加验证JWT的逻辑。移动端的具体登录流程如下图所示
2、接口开发
2.1、获取短信验证码
前置条件
该接口需向登录手机号码发送短信验证码,各大云服务厂商都提供短信服务,本项目使用阿里云完成短信验证码功能,下面介绍具体配置。
-
配置短信服务
-
开通短信服务
-
在阿里云官网,注册阿里云账号,并按照指引,完成实名认证(不认证,无法购买服务)
-
找到短信服务,选择免费开通
-
进入短信服务控制台,选择快速学习和测试
-
找到发送测试下的API发送测试,绑定测试用的手机号(只有绑定的手机号码才能收到测试短信),然后配置短信签名和短信模版,这里选择**[专用]测试签名/模版**。
-
-
创建AccessKey
云账号 AccessKey 是访问阿里云 API 的密钥,没有AccessKey无法调用短信服务。点击页面右上角的头像,选择AccessKey管理,然后创建AccessKey。
-
查看接口
代码开发
-
配置所需依赖
如需调用阿里云的短信服务,需使用其提供的SDK,具体可参考官方文档。
在common模块的pom.xml文件中增加如下内容
<dependency> <groupId>com.aliyun</groupId> <artifactId>dysmsapi20170525</artifactId> </dependency>
-
配置发送短信客户端
-
在
application.yml
中增加如下内容aliyun: sms: access-key-id: <access-key-id> access-key-secret: <access-key-secret> endpoint: dysmsapi.aliyuncs.com
注意:
上述
access-key-id
、access-key-secret
需根据实际情况进行修改。 -
在common模块中创建
com.atguigu.lease.common.sms.AliyunSMSProperties
类,内容如下@Data @ConfigurationProperties(prefix = "aliyun.sms") public class AliyunSMSProperties { private String accessKeyId; private String accessKeySecret; private String endpoint; }
-
在common模块中创建
com.atguigu.lease.common.sms.AliyunSmsConfiguration
类,内容如下@Configuration @EnableConfigurationProperties(AliyunSMSProperties.class) @ConditionalOnProperty(name = "aliyun.sms.endpoint") public class AliyunSMSConfiguration { @Autowired private AliyunSMSProperties properties; @Bean public Client smsClient() { Config config = new Config(); config.setAccessKeyId(properties.getAccessKeyId()); config.setAccessKeySecret(properties.getAccessKeySecret()); config.setEndpoint(properties.getEndpoint()); try { return new Client(config); } catch (Exception e) { throw new RuntimeException(e); } } }
-
-
配置Redis连接参数
spring: data: redis: host: 192.168.10.101 port: 6379 database: 0
-
编写Controller层逻辑
在
LoginController
中增加如下内容@GetMapping("login/getCode") @Operation(summary = "获取短信验证码") public Result getCode(@RequestParam String phone) { service.getSMSCode(phone); return Result.ok(); }
-
编写Service层逻辑
-
编写发送短信逻辑
-
在
SmsService
中增加如下内容void sendCode(String phone, String verifyCode);
-
在
SmsServiceImpl
中增加如下内容@Override public void sendCode(String phone, String code) { SendSmsRequest smsRequest = new SendSmsRequest(); smsRequest.setPhoneNumbers(phone); smsRequest.setSignName("阿里云短信测试"); smsRequest.setTemplateCode("SMS_154950909"); smsRequest.setTemplateParam("{\"code\":\"" + code + "\"}"); try { client.sendSms(smsRequest); } catch (Exception e) { throw new RuntimeException(e); } }
-
-
编写生成随机验证码逻辑
在common模块中创建
com.atguigu.lease.common.utils.VerifyCodeUtil
类,内容如下public class VerifyCodeUtil { public static String getVerifyCode(int length) { StringBuilder builder = new StringBuilder(); Random random = new Random(); for (int i = 0; i < length; i++) { builder.append(random.nextInt(10)); } return builder.toString(); } }
-
编写获取短信验证码逻辑
-
在
LoginServcie
中增加如下内容void getSMSCode(String phone);
-
在
LoginServiceImpl
中增加如下内容@Override public void getSMSCode(String phone) { //1. 检查手机号码是否为空 if (!StringUtils.hasText(phone)) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY); } //2. 检查Redis中是否已经存在该手机号码的key String key = RedisConstant.APP_LOGIN_PREFIX + phone; boolean hasKey = redisTemplate.hasKey(key); if (hasKey) { //若存在,则检查其存在的时间 Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS); if (RedisConstant.APP_LOGIN_CODE_TTL_SEC - expire < RedisConstant.APP_LOGIN_CODE_RESEND_TIME_SEC) { //若存在时间不足一分钟,响应发送过于频繁 throw new LeaseException(ResultCodeEnum.APP_SEND_SMS_TOO_OFTEN); } } //3.发送短信,并将验证码存入Redis String verifyCode = VerifyCodeUtil.getVerifyCode(6); smsService.sendCode(phone, verifyCode); redisTemplate.opsForValue().set(key, verifyCode, RedisConstant.APP_LOGIN_CODE_TTL_SEC, TimeUnit.SECONDS); }
注意:需要注意防止频繁发送短信。
-
-
2.2、登录和注册接口
查看接口
登录注册校验逻辑
- 前端发送手机号码
phone
和接收到的短信验证码code
到后端。 - 首先校验
phone
和code
是否为空,若为空,直接响应手机号码为空
或者验证码为空
,若不为空则进入下步判断。 - 根据
phone
从Redis中查询之前保存的验证码,若查询结果为空,则直接响应验证码已过期
,若不为空则进入下一步判断。 - 比较前端发送的验证码和从Redis中查询出的验证码,若不同,则直接响应
验证码错误
,若相同则进入下一步判断。 - 使用
phone
从数据库中查询用户信息,若查询结果为空,则创建新用户,并将用户保存至数据库,然后进入下一步判断。 - 判断用户是否被禁用,若被禁,则直接响应
账号被禁用
,否则进入下一步。 - 创建JWT并响应给前端。
代码开发
-
接口实现
-
编写Controller层逻辑
在
LoginController
中增加如下内容@PostMapping("login") @Operation(summary = "登录") public Result<String> login(LoginVo loginVo) { String token = service.login(loginVo); return Result.ok(token); }
-
编写Service层逻辑
-
在
LoginService
中增加如下内容String login(LoginVo loginVo);
-
在
LoginServiceImpl
总增加如下内容@Override public String login(LoginVo loginVo) { //1.判断手机号码和验证码是否为空 if (!StringUtils.hasText(loginVo.getPhone())) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY); } if (!StringUtils.hasText(loginVo.getCode())) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EMPTY); } //2.校验验证码 String key = RedisConstant.APP_LOGIN_PREFIX + loginVo.getPhone(); String code = redisTemplate.opsForValue().get(key); if (code == null) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EXPIRED); } if (!code.equals(loginVo.getCode())) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_ERROR); } //3.判断用户是否存在,不存在则注册(创建用户) LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserInfo::getPhone, loginVo.getPhone()); UserInfo userInfo = userInfoService.getOne(queryWrapper); if (userInfo == null) { userInfo = new UserInfo(); userInfo.setPhone(loginVo.getPhone()); userInfo.setStatus(BaseStatus.ENABLE); userInfo.setNickname("用户-"+loginVo.getPhone().substring(6)); userInfoService.save(userInfo); } //4.判断用户是否被禁 if (userInfo.getStatus().equals(BaseStatus.DISABLE)) { throw new LeaseException(ResultCodeEnum.APP_ACCOUNT_DISABLED_ERROR); } //5.创建并返回TOKEN return JwtUtil.createToken(userInfo.getId(), loginVo.getPhone()); }
-
-
编写HandlerInterceptor
-
编写AuthenticationInterceptor
在web-app模块创建
com.atguigu.lease.web.app.custom.interceptor.AuthenticationInterceptor
,内容如下@Component public class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("access-token"); Claims claims = JwtUtil.parseToken(token); Long userId = claims.get("userId", Long.class); String username = claims.get("username", String.class); LoginUserHolder.setLoginUser(new LoginUser(userId, username)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { LoginUserHolder.clear(); } }
-
注册AuthenticationInterceptor
在web-app模块创建
com.atguigu.lease.web.app.custom.config.WebMvcConfiguration
,内容如下@Configuration public class WebMvcConfiguration implements WebMvcConfigurer { @Autowired private AuthenticationInterceptor authenticationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/app/**").excludePathPatterns("/app/login/**"); } }
-
-
-
Knife4j增加认证相关配置
在增加上述拦截器后,为方便继续调试其他接口,可以获取一个长期有效的Token,将其配置到Knife4j的全局参数中。
2.3、查询登录用户的个人信息
查看接口
代码开发
-
查看响应数据结构
查看web-app模块下的
com.atguigu.lease.web.app.vo.user.UserInfoVo
,内容如下@Schema(description = "用户基本信息") @Data @AllArgsConstructor public class UserInfoVo { @Schema(description = "用户昵称") private String nickname; @Schema(description = "用户头像") private String avatarUrl; }
-
编写Controller层逻辑
在
LoginController
中增加如下内容@GetMapping("info") @Operation(summary = "获取登录用户信息") public Result<UserInfoVo> info() { UserInfoVo info = service.getUserInfoById(LoginUserHolder.getLoginUser().getUserId()); return Result.ok(info); }
-
编写Service层逻辑
-
在
LoginService
中增加如下内容UserInfoVo getUserInfoId(Long id);
-
在
LoginServiceImpl
中增加如下内容@Override public UserInfoVo getUserInfoId(Long id) { UserInfo userInfo = userInfoService.getById(id); return new UserInfoVo(userInfo.getNickname(), userInfo.getAvatarUrl()); }
-