登录管理共需三个接口,分别是获取短信验证码、登录、查询登录用户的个人信息。除此之外,同样需要编写HandlerInterceptor来为所有受保护的接口增加验证JWT的逻辑。移动端的具体登录流程如下图所示

前置条件
该接口需向登录手机号码发送短信验证码,各大云服务厂商都提供短信服务,本项目使用阿里云完成短信验证码功能,下面介绍具体配置。
配置短信服务
查看接口

代码开发
配置所需依赖
如需调用阿里云的短信服务,需使用其提供的SDK,具体可参考官方文档。
在common模块的pom.xml文件中增加如下内容
<dependency>
<groupId>com.aliyungroupId>
<artifactId>dysmsapi20170525artifactId>
dependency>
配置发送短信客户端
在application.yml中增加如下内容
aliyun:
sms:
access-key-id: -key-id>
access-key-secret: -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);
}
注意:需要注意防止频繁发送短信。
查看接口

登录注册校验逻辑
phone和接收到的短信验证码code到后端。phone和code是否为空,若为空,直接响应手机号码为空或者验证码为空,若不为空则进入下步判断。phone从Redis中查询之前保存的验证码,若查询结果为空,则直接响应验证码已过期 ,若不为空则进入下一步判断。验证码错误,若相同则进入下一步判断。phone从数据库中查询用户信息,若查询结果为空,则创建新用户,并将用户保存至数据库,然后进入下一步判断。账号被禁用,否则进入下一步。代码开发
接口实现
编写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的全局参数中。
查看接口

代码开发
查看响应数据结构
查看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());
}