Redis 实现手机号验证码登陆功能,Token 实现登录校验又是如何做? 目录一. 使用 Redis 实现短信登陆功能1.1 前言1.2 简要分析短信登陆的业务流程1.3 代码书写1.4 页面展示效果1.5 代码优化1.6 优化之后的代码二. 登录 Token2.1 JWT简介2.2 JWT的使用方式2.3 返回 token2.4 JWT 的校验方式一. 使用 Redis 实现短信登陆功能1.1 前言对于我们用户来讲我们在登陆一个APP的时候有很多种登陆方式比如微信扫码、手机号登陆、支付宝扫码、账号密码登录而且现在账号普遍都是使用手机号作为账号同时还能确保每个用户账号的唯一性因此狭义上来讲账号密码登录也可以归属于手机号登陆的一种。现在大多都会要求微信扫码登录或者是手机号验证码登录。使用传统账号密码登陆的已经很少了当然并不是说没有了而是不再那么流行了而且很多人其实设置了密码基本都会忘了还不如短信登陆来的快。那么今天我们就来看看我们平常司空见惯的手机号登陆方式是如何通过代码来实现的吧1.2 简要分析短信登陆的业务流程大致分为以下几个步骤当然还有一些细节需要处理我们一步一步来说。1用户输入手机号并点击验证码请求获取验证码2判断手机号是否有效有效则生成6位随机数将验证码存储到 Redis 同时发送给用户3用户短信接收到验证码输入验证码请求登录5再根据用户输入的手机号和验证码做校验看看是否一致5验证码一致则登陆成功放行如果验证码不一致则登陆失败拦截1.3 代码书写发送验证码的接口代码如下// 定义 redis 存储验证码的业务字符前缀 public static final String LOGIN_CODE_KEY login:code:; // 定义验证码超时时间 2单位为分钟 public static final Long LOGIN_CODE_TTL 2L; /* * 发送验证码接口 * */ public Result sendCode(String phone) { String input_phone phone; // 1. 校验手机号,因为校验手机号在后面登录业务或者其他业务中都可能用到所以可以抽取为一个方法 if (!RegexUtils.isValidPhoneNumber(phone)) { // 2.如果不符合返回错误信息 return Result.fail(手机号格式错误); } // 3. 符合生成验证码 String code RandomUtil.randomNumbers(6); // 4. 打印到控制台输出一下 System.out.println(code); // 5. 保存验证码到 redis stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); // 6. 发送验证码这里应该调用真实的第三方的短信发送API简单用 log 日志打印输出替代一下好啦 log.debug(发送短信验证码成功验证码{}, code); // 返回ok return Result.ok(); } // 定义一个正则常量字符串 regex 用于下面比较手机号是否合法 public static final String regex ^1[3456789]\\d{9}$; /** * 是否是无效手机格式 * param * return true:符合false不符合 */ public static boolean isValidPhoneNumber(String phoneNumber) { if (StrUtil.isBlank(phoneNumber)) { return false; } return phoneNumber.matches(regex); }校验验证码登陆的接口代码如下public Result login(String phone,String code){ // 1. 验证手机号是否合法 if (!RegexUtils.isValidPhoneNumber(phone)) { return Result.fail(手机号格式错误); } // 2. 从 redis 中获取手机号对应的验证码 校验验证码是否正确 String cacheCode stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY phone); // 3. 如果验证码为空或二者不相等说明验证码已过期或输入错误返回错误 if (cacheCode null || !cacheCode.equals(code)) { return Result.fail(验证码错误); } // 4. 验证码正确,返回登陆成功 return Result.ok(); }1.4 页面展示效果如下图所示我输入1234567890十位数并不是一个手机号所以在点击发送验证码时页面就报错手机号格式错误我再输入一个正常的手机号页面显示验证码正常发送成功我们到项目控制台看一下如下图所示随机生成的验证码就打印在了控制台我们再到 redis 可视化连接工具查看可以看到短信验证码已经存储到了 redis 中TTL 过期时间还剩下 64 秒。1.5 代码优化优化点一我们日常在使用手机的时候应该都体会过我们去登陆一个APP的时候不管你是新用户还是老用户如果长时间不使用一个软件你再去使用的时候都会让你重新登陆。那么现在就会有一个问题在登陆的时候系统怎么判断你是新用户还是老用户呢如果是新用户是需要帮你去做注册的这个操作的而我们日常在使用的时候大多都会发现即便是首次登陆软件只要验证码通过依然可以登陆成功这其实是因为我们在登陆的时候如果系统发现我们是新用户会自动帮助我们完成注册的操作。因此对于用户而言是无感知的不会说先让我们去注册灾区进行验证码登录。这一点我们目前还没有做可以做一个简单的的优化。优化点二正如我最开始的时候所说现在主流的是采用验证码登录但是我们也会发现有时候我们也可以使用 手机号密码的的登陆方式进行登录这一点也可以进行一个简单的优化。1.6 优化之后的代码1首先对登陆参数进行优化采用实体类传参更加简洁也方便后续扩展Data public class LoginFormDTO { private String phone; private String code; private String password; }2对登录方法进行优化添加新用户注册的方法和密码校验的方式public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号 String phone loginForm.getPhone(); if (!RegexUtils.isValidPhoneNumber(phone)) { return Result.fail(手机号格式错误); } // 2. 获取用户输入的验证码和密码, String code loginForm.getCode(); String password loginForm.getPassword(); // 3. 根据手机号查询用户信息 User user userMapper.selectByPhone(phone); // 4. 先判断验证码 code 是否为空不为空则说明用户是验证码登录 if (code ! null) { // 5. 从redis获取验证码并校验, String cacheCode stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY phone); // 6. 如果redis中为空或而这不相等说明验证码已过期或输入错误返回登陆错误 if (cacheCode null || !cacheCode.equals(code)) { return Result.fail(验证码错误); } // 7. 判断用户是否存在,不存在则调用 createUserWithPhone 方法创建新用户并保存 if (user null) { user createUserWithPhone(phone); } return Result.ok(); } else if (password ! null) { // 8. 密码不为空用户通过密码方式登录实际开发过程中密码是需要进行加密处理和解密处理的这里就不做那么复杂了 if (user.getPassword().equals(loginForm.getPassword())) { return Result.ok(); } } return Result.fail(请输入验证码或密码); }3createUserWithPhone 创建用户方法如下// 提前定义一个统一的用户昵称前缀就像京东会以jd_开头,淘宝会以tb_开头再加上一个随机字符串作为用户昵称后期用户可以自己再进行修改 public static final String USER_NICK_NAME_PREFIX user_; private User createUserWithPhone(String phone) { // 1.创建用户 User user new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX RandomUtil.randomString(10)); // 2.保存用户 userService.save(user); return user; }下面是 user 类的建表SQL 上面出现的几个属性都在下面SQL中CREATE TABLE tb_user ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, phone varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 手机号码, password varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT COMMENT 密码 nick_name varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT COMMENT 昵称 icon varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT COMMENT 人物头像, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id) USING BTREE, UNIQUE KEY uniqe_key_phone (phone) USING BTREE ) ENGINEInnoDB AUTO_INCREMENT1012 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci ROW_FORMATCOMPACT;二. 登录 Token2.1 JWT简介通常情况下当用户登录成功之后服务器通常会将用户的一些常用的、敏感的信息存储起来方便后续用户在进行其他系统操作时使用同时也会存储用户的权限哪些操作可以做哪些操作无权限做。在以前是通过 session 会话来控制用户与服务器的交流现在更多的则是使用 token 。用于在用户认证和授权过程中进行身份验证同时也可以存储用户的信息token 的生成方式有很多种但最流行的是JWT。JWT 全称 JSON Web Token格式为 xxx.xxx.xxx中间有两个小数点将字符串分为三个部分分别对应 Header头、Payload载荷、Signature签名。如下就是一个常见的 JWT 字符串。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c希望进一步了解 Session 和 JWT 的可以看下面的文章Session、Cookie、Token、JWT分别都是什么作用都有哪些-CSDN博客2.2 JWT的使用方式要想使用JWT首先需要引入依赖!--JWT(Json Web Token)登录支持-- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency然后我们单独创建一个方法就命名为 generateToken// 设置加密私钥就是一个随机字符串对 Token 签名时需要用到 private static final String TOKEN_SECRET secret; // 生成JWT的方法 public String generateToken(User user) { // 1.1 JWT 头部分信息【Header】主要存储 token 的加密算法和 token 类型 MapString, Object tokenHeader new HashMap(); // 1.2 设置算法 Algorithm 为 HS256 tokenHeader.put(alg, HS256); // 1.3 设置 token 类型为 JWT tokenHeader.put(typ, JWT); // 2.1 JWT 载荷部分信息【Payload】主要存储用户信息用户信息可以使用参数中传递过来的 user 对象 MapString, Object tokenPayload new HashMap(); // 2.2 设置用户id tokenPayload.put(id, user.getId()); // 2.3 设置用户手机号 tokenPayload.put(phone, user.getPhone()); // 2.4 设置用户昵称 tokenPayload.put(nickName,user.getNickName()); // 3. 声明Token失效时间,这里设置的是3600秒实际开发按项目需求设置即可 Calendar instance Calendar.getInstance(); instance.add(Calendar.SECOND,3600); // 4. 调用JWT包的 builder 方法开始生成 tokentokenHeader。tokenPayload失效时间私钥都已准备就绪 String token Jwts.builder() .setHeader(tokenHeader) // 头部信息 .setClaims(tokenPayload) // 载荷信息 .setExpiration(instance.getTime()) // 失效时间 .signWith(SignatureAlgorithm.HS256,TOKEN_SECRET) // 签名算法私钥 .compact(); // 压缩生成xxx.xxx.xxx return token; }2.3 返回 token这里我们在 login 登陆方法中添加 HttpServletResponse 响应参数然后将生成的 token 对象添加至 response 中进行返回客户端接收到相应之后将 token 存储到本地浏览器后续再发送请求到服务器都需要将 token 添加至请求头让服务器去做校验。public Result login(LoginFormDTO loginForm, HttpSession session, HttpServletResponse response) { // 1.校验手机号 String phone loginForm.getPhone(); if (!RegexUtils.isValidPhoneNumber(phone)) { return Result.fail(手机号格式错误); } // 2. 获取用户输入的验证码和密码, String code loginForm.getCode(); String password loginForm.getPassword(); // 3. 根据手机号查询用户信息 User user userMapper.selectByPhone(phone); String tokenString generateToken(user); // 4. 先判断验证码 code 是否为空不为空则说明用户是验证码登录 if (code ! null) { // 5. 从redis获取验证码并校验, String cacheCode stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY phone); // 6. 如果redis中为空或而这不相等说明验证码已过期或输入错误返回登陆错误 if (cacheCode null || !cacheCode.equals(code)) { return Result.fail(验证码错误); } // 7. 判断用户是否存在,不存在则调用 createUserWithPhone 方法创建新用户并保存 if (user null) { user createUserWithPhone(phone); } response.setHeader(token, tokenString); // 将token存入响应头 return Result.ok(); } else if(password ! null) { // 8. 密码不为空用户通过密码方式登录实际开发过程中密码是需要进行加密处理和解密处理的这里就不做那么复杂了 if (user.getPassword().equals(loginForm.getPassword())) { response.setHeader(token, tokenString); // 将token存入响应头 return Result.ok(); } } return Result.fail(登陆失败); }2.4 JWT 的校验方式由于我们返回给客户端的 token 中存储着 token 的加密方式所以我们就不需要在服务器上存储 token每次用户来进行访问是带着 token 过来。然后在服务器后端我们获取用户的 token再后端获取自定义的密钥通过与生成 token 时同样的加密手法再次对用户发送过来的数据进行签名将签名之后生成的新 token 与 用户携带的签名进行匹配如果相等说明用户校验通过放行请求如果匹配签名不相等说明当前用户的签名被恶意修改过则进行拦截禁止访问服务器。并且这一步通常是在请求前完成的大致逻辑如下所示;用户发送请求——先进入拦截器——再做token校验——校验通过才会到我们熟知的 Controller 层当然了在实际项目中微服务项目可能还有nginx负载均衡Gateway网管等...总之我想表达的是token 校验是在 请求到达 Controller 控制器层之前完成的public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 获取请求头中的token String token request.getHeader(Authorization); // 2. 获取密钥, 这里需要实现一个获取密钥的方法当然也可以定义在工具类中直接获取 String secret getSecret(); // 3. 使用io.jsonwebtoken依赖包已经提供的parser方法解析JWT指定了JWT的签名算法和密钥。 try { Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); // 4. 获取token载荷中的各种用户信息可以做一系列操作 System.out.println(User ID: claims.get(userId)); System.out.println(User Name: claims.get(userName)); } catch (SignatureException e) { // 5. 如果解析失败说明JWT不合法直接返回false表示拦截请求 System.out.println(Invalid JWT signature.); } // 6. 验证成功之后还可以做一些其他操作比如刷新 token 有效期等... // 7. 返回 return true; }