【尚庭公寓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 {@Autowiredprivate AliyunSMSProperties properties;@Beanpublic 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.101port: 6379database: 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中是否已经存在该手机号码的keyString 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.发送短信,并将验证码存入RedisString 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.创建并返回TOKENreturn JwtUtil.createToken(userInfo.getId(), loginVo.getPhone()); }
-
-
编写HandlerInterceptor
-
编写AuthenticationInterceptor
在web-app模块创建
com.atguigu.lease.web.app.custom.interceptor.AuthenticationInterceptor
,内容如下@Component public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic 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;}@Overridepublic 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 {@Autowiredprivate AuthenticationInterceptor authenticationInterceptor;@Overridepublic 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()); }
-