目录
获取验证码
时序图
集成阿里云短信服务
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&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; // 阻止表单的默认提交行为}});
登录功能验证
运行程序,访问登录页面:
验证码登录:
密码登录:
登录后跳转:
异常情况在这里就不再一一进行演示了