ユーザー登録とSMS認証コードによるログイン、実用的

個人プロジェクト: ソーシャルペイメントプロジェクト (リトルボス)

作者:三男(j3code.cn )

ドキュメンテーションシステム:amdem.j3code.cn/note

前回の記事 では、SMS を送信するためのツール クラスを実装するのに 44 元を費やしました。そのため、この記事は、ユーザー登録を作成し、SMS 認証コードを介してログインするのに役立ちます。

開始する前に、その後のコード記述を容易にするために、基本的な SpringBoot プロジェクトを構築する必要があります (ただし、詳細は説明します)。

1. テーブルフィールド分析

まず、user テーブルの関連フィールドを分析しましょう。

ユーザーID

電話: 電話

パスワード: パスワード

後で、ログインするためにコードをスキャンする必要がある場合があるため、ログインするためにコードをスキャンするためのいくつかのフィールドが予約されています。

オープンID

セッションキー

労働組合

さらに、ユーザーを識別するために適切なパーソナライゼーションを備えたフィールドをいくつか追加します。

アバター: avatar_url

ニックネーム:nick_name

性別: 性別

はじめに: イントロ

作成時間: create_time

変更時刻: update_time

さて、ユーザーテーブルの作成SQLは次のようになります。

CREATE TABLE `sb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `phone` varchar(15) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '电话',
  `password` varchar(200) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '密码',
  `avatar_url` varchar(500) COLLATE utf8mb4_german2_ci DEFAULT 'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132' COMMENT '头像',
  `nick_name` varchar(50) COLLATE utf8mb4_german2_ci DEFAULT '默认用户' COMMENT '昵称',
  `openid` varchar(200) COLLATE utf8mb4_german2_ci DEFAULT NULL,
  `session_key` varchar(200) COLLATE utf8mb4_german2_ci DEFAULT NULL,
  `unionid` varchar(200) COLLATE utf8mb4_german2_ci DEFAULT NULL,
  `sex` tinyint(1) DEFAULT '1' COMMENT '性别,0女1男',
  `intro` varchar(256) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '简介',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_1` (`phone`),
  UNIQUE KEY `un_2` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

ユーザー テーブルを設計したら、SMS レコード テーブルの設計を開始しましょう。

なぜこのような表があるのか​​というと、オペレータがSMSの発行数をカウントし、その後の運用戦略を立てるのに便利であることが一番大きいと思います。もちろん開発者にとっても同様で、記録はログに相当し、問題があれば開発者が確認できるため、SMSの記録を一覧表にすることは非常に有意義です。

このテーブルには多くのフィールドを記録する必要はなく、次のフィールドだけを記録する必要があります。

ID

電話: 電話

送信ステータス: ステータス

備考: 備考

作成時間: create_time

対応するテーブル SQL は次のとおりです。

CREATE TABLE `sb_sms_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `phone` varchar(11) COLLATE utf8_unicode_ci NOT NULL COMMENT '号码',
    `status` int(11) NOT NULL COMMENT '发送状态',
    `remark` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '备注',
    `create_time` datetime DEFAULT NULL COMMENT '发送时间',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

データベースにテーブルを作成した後、IDEA のプラグインを使用して、MyBatisX対応するエンティティ、サービス、マッパー、その他のクラスまたは XML ファイルを生成します。

1)MyBatisXプラグインをインストールします(誰もが行うと思います)

2) プロジェクトは MySQL データベースに接続します

  • Snipaste_2023-06-27_16-29-38.png

    データベースへの接続を開始します

    Snipaste_2023-06-27_16-34-08.png

3) プラグインを使用してコードを生成する

  • 1 つのテーブルと 1 つのテーブルに対してのみコードを生成できます。手順は図に示すとおりです。

    Snipaste_2023-06-27_16-40-08.png

2. SMS認証コード登録解析

现在,发挥你们大脑的时刻到了,如果让你来考虑一下用户注册的流程,你会如何做呢!

因为我们这是短信验证码方式注册,所以肯定要考虑如下的问题:

  1. 短信不能随随便便的发送
  2. 发送短信前,需要通过严格的校验
  3. 做好短信发送记录

开始分析每一步的流程,这里只是大致的流程,具体的细节要到代码中去考量。

1、注册页面回显一个图形验证码,该验证码可以通过按钮触发更换

2、用户获取短信验证码,必须携带电话 + 图形验证码,才可获取短信验证码

3、注册时需提交如下信息:

  • 电话
  • 短信验证码
  • 密码

4、一系列的校验流程,成功之后才能进入后续步骤

5、生成用户信息,注册成功

大致的流程出来了,那么为了加深一下印象,我们再来画个时序图:

Snipaste_2023-06-27_17-30-05.png

现在清晰多了,那么开始编码。

3、编码实现注册功能

3.1 获取图形验证码

别嫌我啰嗦,获取图形验证码的流程我们还是要再来分析分析。

先搞清楚一点,为啥需要这个。

目的:防止用户刷发送短信接口,只有发送短信接口中携带的图形验证码正确才能触发后续的短信发送功能,否则一律不发送验证码。

现在知道这个图形验证码作用了吧!

那下面我画一个代码实现的基本流程图,确保代码的每一步都清清楚楚:

Snipaste_2023-06-27_18-01-51.png

编码实现:

前提:先创建好如下几个类:

  1. SmsAuthController:短信认证相关控制器
  2. AuthService 和 AuthServiceImpl:认证业务接口和实现类

1)controller 接口方法编写

@Slf4j
@ResponseResult
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.V1 + "/sms/auth")
public class SmsAuthController {

    private final AuthService service;
    /**
     * 用户登录或者注册获取图形验证码
     *
     * @return 图片 Base64 字符串
     */
    @GetMapping("/getCaptcha")
    public String getCaptcha() {
        return service.getCaptcha();
    }
}

2)service 方法实现

public interface AuthService {
    String getCaptcha();
}

@Slf4j
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public String getCaptcha() {
        // 生成 图形验证码
        RandomGenerator randomGenerator = new RandomGenerator("abcdefghjkmnopqrstuvwxyz023456789", 4);
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(100, 42, 4, 60);
        lineCaptcha.setGenerator(randomGenerator);
        lineCaptcha.createCode();

        // 获取 base64 字符串 和 code 值
        String base64 = "data:image/png;base64," + lineCaptcha.getImageBase64();
        String code = lineCaptcha.getCode();

        log.info("图形验证码内容:{}", code);
        log.debug("图形验证码base64:{}", base64);

        // 根据 code ,生成 redis 的 key,登录或注册时需要验证
        String key = SbUtil.captchaRedisKey(code);
        // 存入 redis(key,value,超时时间)
        redisTemplate.opsForValue().set(key, code, ServerNameConstants.CAPTCHA_KEY_EXPIRED_TIME);

        return base64;
    }
}

注:SbUtil 和 ServerNameConstants 类是系统封装的工具类和常量类,代码就不贴了,你们可以根据业务灵活实现。

3.2 获取短信验证码

这节就来实现如何发送短信验证码了。

那还是来分析一下,这个流程需要考虑那些问题:

  1. 校验请求是否合格,不合格的一律不发送短信验证码
  2. 需存储发送记录

考虑的问题虽然不多,但是校验这块却是本次的重点,只要校验做的好,升职加薪少不了。

写代码之前,还是来画画流程图:

Snipaste_2023-06-27_21-48-08.jpg

注意图中的几个校验,其中有一个地方会存在并发安全问题,不知道你们会不会发现?

编码实现:

1)controller 编写

@Slf4j
@ResponseResult
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.V1 + "/sms/auth")
public class SmsAuthController {

    private final AuthService service;
    /**
     * 发送短信验证码
     *
     * @param phone 电话号码
     * @param code  图形验证码 code
     */
    @GetMapping("/sendSms")
    public void sendSms(@RequestParam("phone") String phone, @RequestParam("code") String code) {
        service.sendSms(phone, code);
    }
}

2)service 方法实现

public interface AuthService {
    void sendSms(String phone, String code);
}

@Slf4j
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final UserService userService;
    private final RedisTemplate<String, Object> redisTemplate;
    private final ApplicationContext applicationContext;
    private final SendSmsConfig sendSmsConfig;
    private final SmsLogService smsLogService;

    @Override
    public void sendSms(String phone, String code) {
        // 校验
        checkPhoneAndRegisterAndKey(phone, SbUtil.captchaRedisKey(code));

        // 开始发送短信验证码

        // 生成 6 位数字验证码
        String smsCode = RandomUtil.randomNumbers(6);

        // 记录
        SmsLog smsLog = new SmsLog()
            .setPhone(phone)
            .setRemark(String.format("用户注册短信验证码发送:%s", smsCode));

        // 封装短信发送参数
        final var smsRequest = new SendSmsUtil.SendSmsRequest()
            .setSmsSdkAppId(sendSmsConfig.getSmsSdkAppId())
            .setSecretId(sendSmsConfig.getSecretId())
            .setSecretKey(sendSmsConfig.getSecretKey())
            .setSignName(sendSmsConfig.getSignName())
            .setTemplateId(sendSmsConfig.getTemplateId())
            .setPhone(phone)
            .setTemplateParamSet(new String[]{smsCode, "" + ServerNameConstants.SMS_KEY_EXPIRED_TIME.get(ChronoUnit.SECONDS) / 60});

        // 发送短信验证码并获取结果
        smsLog.setStatus(SendSmsUtil.sendSms(smsRequest) ? SuccessFailStatusEnum.SUCCESS : SuccessFailStatusEnum.FAIL);

        try {
            // 保存发送日志
            smsLogService.save(smsLog);
        } catch (Exception e) {
            log.error("保存短信发送记录失败:", e);
        }

        if (SuccessFailStatusEnum.FAIL.equals(smsLog.getStatus())) {
            throw new SendSmsAuthException("短信发送失败!");
        }

        // 存入 redis,校验用户的注册逻辑
        redisTemplate.opsForValue().set(SbUtil.smsRedisKey(phone + ":" + smsCode), smsCode, ServerNameConstants.SMS_KEY_EXPIRED_TIME);
    }

    /**
     * 加分布式锁,确保此功能原子
     *
     * @param key code 再 redis 中对应的 key
     */
    @DistributedLock(key = "check-code-{1}", lockFail = true, failMessage = "请求频繁,稍后重试!")
    public void checkey(String key) {
        // 校验图形验证码是否正确
        if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
            throw new SendSmsAuthException("验证码不正确或已过期!");
        }
        // 证码成功,删除 key,防止多次提交,多次发送短信
        redisTemplate.delete(key);
    }

    /**
     * 校验电话号码是否合格,是否已经注册,key 是否正确
     *
     * @param phone
     * @param key
     */
    public void checkPhoneAndRegisterAndKey(String phone, String key) {
        // 校验电话号码
        if (Boolean.FALSE.equals(SbUtil.checkPhone(phone))) {
            throw new SendSmsAuthException("请正确输入电话号码!");
        }

        // 是否注册
        User user = userService.lambdaQuery()
            .eq(User::getPhone, phone)
            .one();
        if (Objects.nonNull(user)) {
            throw new RegisterAuthException("电话号码已存在,请直接登录!");
        }

        // key 是否存在
        applicationContext.getBean(AuthServiceImpl.class).checkey(key);
    }

}

SendSmsConfig 配置代码:

@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "send.sms")
public class SendSmsConfig {
    /**
     * 短信签名内容,必须填写已审核通过的签名
     */
    private String signName;

    /**
     * 模板 ID: 必须填写已审核通过的模板 ID
     */
    private String templateId;

    /**
     * 应用id
     */
    private String smsSdkAppId;
    /**
     * api密钥中的secretId
     */
    private String secretId;

    /**
     * api密钥中的应用密钥
     */
    private String secretKey;
}

分析一下上面的代码:

  1. 先做了三个校验:校验电话号码、校验号码是否已经注册、校验图形验证码是否正确

    这里要重点说一下:校验图形验证码是否正确

    大家试想一下,如果我们从 Redis 中校验了图形验证码合格,然后不去删除此验证码,是不是在验证码过期之内,用户有可能一直携带这个有效验证码进行刷接口,导致我们的短信不停的发送啊!

    所以我们这里的做法肯定是两步:

    1. 校验验证码是否合格
    2. 合格,移除验证码

    但是新的问题又来了,如果用户一次性进行大量的请求(并发)都打到第一步,而第一步同样是符合要求任然会进入到第二步,导致发送多条短信,所以必须让 1、2 两步原子的执行。

    解决:

    1. 分布式锁

      推荐我的手写分布式锁视频:www.bilibili.com/video/BV1d4…

    2. Lua 脚本执行 redis

    那么,分析到这里大家就知道我为啥使用了一个分布式锁了。至于 Lua 脚本,后续我也会在优化版本中将代码写出来。

  2. 生成 6 位短信验证码,封装发送短信参数,短信发送

  3. 保存短信记录

  4. 短信验证码存入 Redis,用户注册时进行校验

3.3 短信验证码实现注册

经过了上面两个步骤,终于是要真正的开始注册用户数据了,那流程走到这里,我们要明确下面这几件事:

  1. 给用户发过短信了,注册时需携带此短信验证码
  2. 让用户输入密码,后续可以通过短信登录或者电话 + 密码方式登录

ok,再来画一画此功能的消息流程图:

Snipaste_2023-06-28_14-03-38.png

好,现在开始编码。

1)controller 编写

@Slf4j
@ResponseResult
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.V1 + "/sms/auth")
public class SmsAuthController {

    private final AuthService service;

    /**
     * 用户注册
     *
     * @param request 请求数据
     */
    @PostMapping("/register")
    public void register(@Validated @RequestBody RegisterRequest request) {
        service.register(request);
    }
}

RegisterRequest 请求体:

@Data
public class RegisterRequest {

    /**
     * 电话
     */
    @NotBlank(message = "电话号码不为空")
    private String phone;

    /**
     * 短信验证码
     */
    @NotBlank(message = "短信验证码不为空")
    private String smsCode;

    /**
     * 密码
     */
    @NotBlank(message = "密码不为空")
    private String password;
}

2)service 编写

public interface AuthService {
    void register(RegisterRequest request);
}

@Slf4j
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final UserService userService;
    private final RedisTemplate<String, Object> redisTemplate;
    private final ApplicationContext applicationContext;
    private final SendSmsConfig sendSmsConfig;
    private final SmsLogService smsLogService;

    @Override
    public void register(RegisterRequest request) {
        // 校验
        checkPhoneAndRegisterAndKey(request.getPhone(), SbUtil.smsRedisKey(request.getPhone() + ":" + request.getSmsCode()));

        // 创建并保存用户对象
        boolean isSave = userService.save(new User()
                                          .setPhone(request.getPhone())
                                          .setPassword(request.getPassword()));

        if (Boolean.FALSE.equals(isSave)) {
            throw new RegisterAuthException("注册失败,请联系管理员!");
        }
    }
}

注:有些方法在 3.2 节中已经写过了,就不贴出来了

接着,我们对 User 对象内部做了一些调整,提供了下面两个方法:

/**
 * 设置密码,自动加密
 * @param password
 * @return
 */
public User setPassword(String password) {
    if (StringUtils.isBlank(password)) {
        throw new PasswordException("密码不能为空!");
    }
    this.password = MD5.create().digestHex(password);
    return this;
}

/**
 * 判断当前对象密码与传入密码是否相等
 * @param password
 * @return
 */
public Boolean passwordEqual(String password) {
    if (StringUtils.isBlank(password)){
        return Boolean.FALSE;
    }
    /**
         * 这里,进行了两次 MD5,因为存入的时候对原值 MD5 了一次,
         * 然后取出来调用 set 的时候又 MD5 了一次,所以要进行两次 MD5 进行比较
         */
    String hex = MD5.create().digestHex(password);
    String hex02 = MD5.create().digestHex(hex);
    return StringUtils.isBlank(this.password) ? Boolean.FALSE : this.password.equals(hex02);
}

这样做的目的是为了高内聚,因为密码是用户的东西,所以关于密码的操作都内聚到用户类中。

4、编码实现登录功能

登録機能の実現によれば、システムがユーザーの電話番号とパスワードをすでに持っていることがわかっているため、SMS ログイン、電話 + パスワード ログインの 2 つのログイン方法を提供する必要があります。

4.1 SMS認証コードによるログイン

注意深く分析した結果、この機能の実装プロセスは登録機能と似ており、次のステップがあることがわかります。

  1. グラフィック検証コードを取得する
  2. SMS認証コードを取得する
  3. ログイン用のSMS認証コード

したがって、ここでコードを繰り返さないようにするために、上記のセクション 3.2 の関数の実装にいくつかの小さな変更を加えます。

3.1 機能はまったく同じであるため変更はありませんが、3.2 には送信レコードがあり、SMS 送信テンプレートを変更する必要があるため、セクション 3.2 のみを変更します。

3.2 調整内容は次のとおりです。

1. SMS 送信タイプの列挙を追加します。これにより、後で SMS 送信タイプを柔軟に追加できます

@Getter
public enum SendSmsTypeEnum {

    LOGIN("LOGIN", "登录", "1847057"),

    REGISTER("REGISTER", "注册", "1843226"),

    ;

    private String value;

    private String description;

    /**
     * 短信模板id
     */
    private String templateId;

    SendSmsTypeEnum(String value, String description, String templateId) {
        this.value = value;
        this.description = description;
        this.templateId = templateId;
    }
}

2. コントローラーの送信SMSインターフェースの定義を変更します。

/**
 * 发送短信验证码
 *
 * @param request 发送短信请求参数
 */
@PostMapping("/sendSms")
public void sendSms(@Validated @RequestBody SendSmsRequest request) {
    service.sendSms(request);
}

3. SendSmsRequest リクエストボディを定義する

@Data
public class SendSmsRequest {
    /**
     * 电话
     */
    @NotBlank(message = "电话不为空")
    private String phone;
    /**
     * 验证码
     */
    @NotBlank(message = "验证码不为空")
    private String code;
    /**
     * 类型
     */
    @NotNull(message = "类型不为空")
    private SendSmsTypeEnum sendSmsType;
}

4. サービス内のSMS送信方法を変更する

@Override
public void sendSms(SendSmsRequest request) {
    if (SendSmsTypeEnum.REGISTER.equals(request.getSendSmsType())) {
        // 校验注册
        checkPhoneAndRegisterAndKey(request.getPhone(), SbUtil.captchaRedisKey(request.getCode()));
    } else if (SendSmsTypeEnum.LOGIN.equals(request.getSendSmsType())) {
        // 校验登录
        checkPhoneAndLoginAndKey(request.getPhone(), SbUtil.captchaRedisKey(request.getCode()));
    }

    // 开始发送短信验证码

    // 生成 6 位数字验证码
    String smsCode = RandomUtil.randomNumbers(6);

    // 记录
    SmsLog smsLog = new SmsLog()
        .setPhone(request.getPhone())
        .setRemark(String.format("用户" + request.getSendSmsType().getDescription() + "短信验证码发送:%s", smsCode));

    // 封装短信发送参数
    final var smsRequest = new SendSmsUtil.SendSmsRequest()
        .setSmsSdkAppId(sendSmsConfig.getSmsSdkAppId())
        .setSecretId(sendSmsConfig.getSecretId())
        .setSecretKey(sendSmsConfig.getSecretKey())
        .setSignName(sendSmsConfig.getSignName())
        .setTemplateId(request.getSendSmsType().getTemplateId())
        .setPhone(request.getPhone())
        // 设置验证码、过期时间,因为短信模板有2个占位符
        .setTemplateParamSet(new String[]{smsCode, "" + ServerNameConstants.SMS_KEY_EXPIRED_TIME.get(ChronoUnit.SECONDS) / 60});

    // 发送短信验证码并获取结果
    smsLog.setStatus(SendSmsUtil.sendSms(smsRequest) ? SuccessFailStatusEnum.SUCCESS : SuccessFailStatusEnum.FAIL);

    try {
        // 保存发送日志
        smsLogService.save(smsLog);
    } catch (Exception e) {
        log.error("保存短信发送记录失败:", e);
    }

    if (SuccessFailStatusEnum.FAIL.equals(smsLog.getStatus())) {
        throw new SendSmsAuthException("短信发送失败!");
    }

    // 存入 redis,校验用户的注册逻辑
    redisTemplate.opsForValue().set(SbUtil.smsRedisKey(request.getPhone() + ":" + smsCode), smsCode, ServerNameConstants.SMS_KEY_EXPIRED_TIME);
}


/**
     * 校验电话号码是否合格,是否已经注册,key 是否正确
     *
     * @param phone
     * @param key
     */
public void checkPhoneAndRegisterAndKey(String phone, String key) {
    // 校验电话号码
    if (Boolean.FALSE.equals(SbUtil.checkPhone(phone))) {
        throw new SendSmsAuthException("请正确输入电话号码!");
    }

    // 是否注册
    User user = userService.lambdaQuery()
        .eq(User::getPhone, phone)
        .one();
    if (Objects.nonNull(user)) {
        throw new RegisterAuthException("电话号码已存在,请直接登录!");
    }

    // key 是否存在
    applicationContext.getBean(AuthServiceImpl.class).checkey(key);
}

/**
     * 校验电话号码是否合格,是否未注册,key 是否正确
     *
     * @param phone
     * @param key
     */
public void checkPhoneAndLoginAndKey(String phone, String key) {
    // 校验电话号码
    if (Boolean.FALSE.equals(SbUtil.checkPhone(phone))) {
        throw new SendSmsAuthException("请正确输入电话号码!");
    }

    // 是否注册
    User user = userService.lambdaQuery()
        .eq(User::getPhone, phone)
        .one();
    if (Objects.isNull(user)) {
        throw new LoginAuthException("电话号码不存在,请注册!");
    }

    // key 是否存在
    applicationContext.getBean(AuthServiceImpl.class).checkey(key);
}

それ以来、SMS 送信関数が変換され、SendSmsTypeEnum 列挙で定義されたあらゆる種類の SMS 送信に適用できるようになりました。次に、SMS ログイン機能を実装しましょう。

4.1.1 SMSログイン

SMS ログインの場合は、まずいくつかの事前条件を決定する必要があります。

  1. 携帯電話 + SMS 認証コード

まあ、これだけで、残りは、ユーザーが存在するかどうかに関係なく、バックエンドの検証コードが正しいかどうかです。その後も同じです。SMS ログインのフローチャートを描きます。

Snipaste_2023-06-28_16-09-57.png

コードの作成を開始します。

1) コントローラの実装

/**
 * 短信登录
 *
 * @param phone   电话
 * @param smsCode 短信验证码
 * @return token
 */
@GetMapping("/loginBySms")
public String loginBySms(@RequestParam("phone") String phone, @RequestParam("smsCode") String smsCode) {
    return service.loginBySms(phone, smsCode);
}

2) 書き込みサービス

public interface AuthService {
    String loginBySms(String phone, String smsCode);
}

@Slf4j
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final UserService userService;
    private final RedisTemplate<String, Object> redisTemplate;
    private final ApplicationContext applicationContext;

    @Override
    public String loginBySms(String phone, String smsCode) {
        // 校验电话号码
        if (Boolean.FALSE.equals(SbUtil.checkPhone(phone))) {
            throw new SendSmsAuthException("请正确输入电话号码!");
        }

        // 是否注册
        User user = userService.lambdaQuery()
            .eq(User::getPhone, phone)
            .one();
        if (Objects.isNull(user)) {
            throw new LoginAuthException("电话号码不存在,请注册!");
        }

        // key 是否存在
        applicationContext.getBean(AuthServiceImpl.class).checkey(SbUtil.smsRedisKey(phone + ":" + smsCode));

        // 生成 token,并存入 redis(带过期时间)
        String token = SbUtil.getTokenStr();
        redisTemplate.opsForValue().set(SbUtil.loginRedisKey(token), JSON.toJSONString(user), ServerNameConstants.AUTH_KEY_EXPIRED_TIME);

        // 返回 token
        return token;
    }
}

ここまででSMSログイン機能は完成しました、もちろん解決していると思いますので以下の機能をコピペして少し修正するだけでOKですので操作させてください。

4.2 電話番号 + パスワードによるログイン

この機能の前提条件:

  1. フロントエンドには、ログインするための電話番号とパスワードが含まれています。

フローチャートは描かず、SMS ログインと同様に、コーディングを開始するだけです。

1) コントローラの書き込み

/**
 * 密码登录
 *
 * @param phone    电话
 * @param password 密码
 * @return token
 */
@GetMapping("/loginByPassword")
public String loginByPassword(@RequestParam("phone") String phone, @RequestParam("password") String password) {
    return service.loginByPassword(phone, password);
}

2) サービスライティング

public interface AuthService {
    String loginByPassword(String phone, String password);
}

@Slf4j
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final UserService userService;
    private final RedisTemplate<String, Object> redisTemplate;
    private final ApplicationContext applicationContext;

    @Override
    public String loginByPassword(String phone, String password) {
        // 校验电话号码
        if (Boolean.FALSE.equals(SbUtil.checkPhone(phone))) {
            throw new SendSmsAuthException("请正确输入电话号码!");
        }

        // 是否注册
        User user = userService.lambdaQuery()
            .eq(User::getPhone, phone)
            .one();
        if (Objects.isNull(user)) {
            throw new LoginAuthException("电话号码不存在,请注册!");
        }

        // 校验密码
        if (Boolean.FALSE.equals(user.passwordEqual(password))){
            throw new LoginAuthException("密码不正确,请重新输入!");
        }

        // 生成 token,并存入 redis(带过期时间)
        String token = SbUtil.getTokenStr();
        redisTemplate.opsForValue().set(SbUtil.loginRedisKey(token), JSON.toJSONString(user), ServerNameConstants.AUTH_KEY_EXPIRED_TIME);

        // 返回 token
        return token;
    }
}

それ以来、ログイン関連の機能は完了しました。「なぜログアウトしないの?」という人もいるかもしれませんが、この機能を実装する前に、優先的に実装すべき機能がいくつかあり、その後、ログアウト機能を開発することができます。

最初にトリックを紹介します。機能については次の章に譲りましょう。

さて、今日の内容は終わりました、フォローしてください、すぐに素晴らしい記事が来るでしょう。

おすすめ

転載: juejin.im/post/7253736317934600249
おすすめ