バックエンドのみが関係します。すべてのディレクトリ、コード、ドキュメント、およびインターフェイス パスについては、一番上の列を参照してください。
[リリショップモール] B2B2Cモールシステムの学習ノートを記録〜
記事全体では、インターフェースクラスとビジネスクラスを含む設計ロジックに焦点を当てたビジネス紹介と、特定のソースコード分析を組み合わせます。ソースコードは複雑ではありません〜
注意: ソースコード内のコメントには、間違っているもの、まったく逆の意味のコメント、正しくないコメントがあります. 読み取り過程で更新し、わからないところに新しいコメントを追加しました. ソースコードを読むときは注意してください. !
目次
C2. ユーザー名とパスワードのログインインターフェイスの開発
C4 .モバイル アプリ/ミニ プログラム スキャン QR コード ログインインターフェイスの開発⭐
A1.会員ログインモジュール
A5.セキュリティフレームワーク(Spring Security) No2-2.ソフトウェアアーキテクチャ構築の決定 の 記事で 、アカウント認可と認証の開発アーキテクチャについてはすでに学習しました。
- アカウントのログインが成功すると、バックエンドから返された accesstoken と refreshtoken を含むトークン データを受け取ります。
- アカウントはバックエンドのインターフェースにアクセスするためのアクセストークンを持っており、フィルタによってインターセプトされ、アクセストークンからアカウント情報を取得し、承認されていると判断してインターフェースを実行します。
なので、ここのメンバーログインモジュールは上記の1.だけで、最後は確実にTokenをゲットできますよ〜
2.については各エンドのAPIコードでBasicAuthenticationFilterから継承したフィルターを確認できますロジックはNo2-2のA5なのでここでは割愛します〜
B1. メンバーコントローラー
C1. プラットフォーム登録会員インターフェース開発
プラットフォームには登録メンバー用のインターフェイスが 1 つしかなく、メンバーを作成するには、ユーザー名、パスワード、携帯電話番号、SMS 認証コードが必要です。また、インターフェイスはアカウント ログインのトークンを直接返しますが、登録が成功した後、PC 側のフロント エンドはトークンを使用しないことに注意してください。登録が成功した後、ユーザーは手動でログインする必要があります。
ビジネスの論理:
ビジネスロジックを導入する場合は、他のコード構造が関係します. 説明が必要な場合は、緑色の網掛けでマークし、次のコードロジックで詳しく紹介します.
コントローラ種別:MemberBuyerController
- 入力パラメーターを受け取ったら、まずSMS 検証コードを検証します [検証はパブリック SmsUtil 操作によっても実行されます]
- 検証コードの検証に問題がある場合、 ServiceException がスローされます.例外の種類: SMS 検証コード エラーです。再検証してください。[例外クラスはカスタマイズされており、グローバル例外処理クラスでキャッチして返す必要があります。詳細は No2-* ソフトウェア アーキテクチャに追加されます。具体的なコードは次のとおりです: GlobalControllerExceptionHandler]
- 検証に問題がなければ、メンバーのビジネスクラスの登録メソッドを呼び出してトークンを取得し、レスポンス値ResultMessageを返します。
サービス クラス: mybatis-plus のみを使用し、カスタム マッパーは使用しません
- まず会員情報に含まれるユーザー名と携帯電話番号が既に存在するかどうかを確認し、存在する場合は ServiceException をスローします. 例外の種類: ユーザー名または携帯電話番号が既に登録されています.
- パラメータをユーザー エンティティ クラス Member に変換し、hutool ツールキットのスノーフレーク アルゴリズムを使用して id を設定し、IService の save メソッドを呼び出してユーザーをデータベースに保存します。
- 会員登録の小さなイベントを処理します: 新規メンバーがポイントを提供し、新規メンバーがクーポンを提供し、新規メンバーが経験値を提供します。[ここでのロジックは SpringEvent と RocketMQ によって処理されます。SpringEventはメッセージを発行するために使用され、その機能はプログラムを分離することです。RocketMQ はメッセージを受信した後に特定のビジネスを処理します]
- 最後にMemberTokenGenerateを生成してトークンを生成し、値を返す [MemberTokenGenerateはNo2-2 A5を参照]
コード ロジック:
//cn.lili.controller.passport.MemberBuyerController
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {
@Autowired
private MemberService memberService;
@Autowired
private SmsUtil smsUtil;
@ApiOperation(value = "注册用户")
@PostMapping("/register")
public ResultMessage<Object> register(@NotNull(message = "用户名不能为空") @RequestParam String username,
@NotNull(message = "密码不能为空") @RequestParam String password,
@NotNull(message = "手机号为空") @RequestParam String mobilePhone,
@RequestHeader String uuid,
@NotNull(message = "验证码不能为空") @RequestParam String code) {
if (smsUtil.verifyCode(mobilePhone, VerificationEnums.REGISTER, uuid, code)) {
return ResultUtil.data(memberService.register(username, password, mobilePhone));
} else {
throw new ServiceException(ResultCode.VERIFICATION_SMS_CHECKED_ERROR);
}
}
...
}
//cn.lili.modules.member.serviceimpl.MemberServiceImpl
@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {
/**
* 会员token
*/
@Autowired
private MemberTokenGenerate memberTokenGenerate;
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Override
@Transactional
public Token register(String userName, String password, String mobilePhone) {
//检测会员信息
checkMember(userName, mobilePhone);
//设置会员信息
Member member = new Member(userName, new BCryptPasswordEncoder().encode(password), mobilePhone);
//进行用户注册处理。抽象出一个方法
this.registerHandler(member);
return memberTokenGenerate.createToken(member, false);
}
/**
* 注册方法抽象出来:会员注册、第三方授权自动注册、员工账号注册登都需要改逻辑~
*
* @param member
*/
@Transactional
public void registerHandler(Member member) {
//hutool工具包 中的雪花算法
member.setId(SnowFlake.getIdStr());
//保存会员
this.save(member);
//处理会员保存成功后的小事件:监听event,发送mp消息。最终在 mq 里面处理小事件
applicationEventPublisher.publishEvent(new TransactionCommitSendMQEvent("new member register", rocketmqCustomProperties.getMemberTopic(), MemberTagsEnum.MEMBER_REGISTER.name(), member));
}
/**
* 检测会员
*
* @param userName 会员名称
* @param mobilePhone 手机号
*/
private void checkMember(String userName, String mobilePhone) {
//判断手机号是否存在
if (this.findMember(mobilePhone, userName) > 0) {
throw new ServiceException(ResultCode.USER_EXIST);
}
}
。。。
}
1.パブリック SmsUtil 操作
詳細については、cn.lili.modules.sms.SmsUtil を参照してください。これには、SMS 検証コードを送信するためのメソッドと、SMS 検証コードを検証するためのメソッドが含まれています。これらはすべて公開されています。
SMS認証コードを送信する方法では、携帯電話番号に送信後、認証コードは最終的にredisに保存されます。そのため、SMS認証コードの認証方法では、登録されている携帯電話番号のEnumの認証コードもredisから取得し、比較・認証します。
SMS 送信ロジックはサード パーティ (アリババ クラウド) によって使用され、アリババ クラウドが提供するツールに従って使用されます。
もう 1 つのポイントは、SmsUtil は公開されているため、会員登録、ログイン、パスワードの取得など、さまざまな種類の使用方法があることです。さまざまな種類の SMS 確認コード テンプレートが異なります (Alibaba Cloud ではテンプレート コードも必要です。差別化された) であるため、区別するにはさまざまなタイプが必要です。
そのため、タイプをマークするための VerificationEnums 列挙型クラスがあり、application.yml もテンプレート情報の構成に使用されます
//cn.lili.modules.verification.entity.enums.VerificationEnums
public enum VerificationEnums {
/**
* 登录
* 注册
* 找回用户
* 修改密码
* 支付钱包密码
*/
LOGIN,
REGISTER,
FIND_USER,
UPDATE_PASSWORD,
WALLET_PASSWORD;
}
//cn.lili.modules.sms.impl.SmsUtilAliImplService
@Component
@Slf4j
public class SmsUtilAliImplService implements SmsUtil, AliSmsUtil {
@Autowired
private Cache cache;
@Autowired
private SettingService settingService;
@Autowired
private MemberService memberService;
@Autowired
private SmsTemplateProperties smsTemplateProperties;
@Autowired
private SystemSettingProperties systemSettingProperties;
@Override
public void sendSmsCode(String mobile, VerificationEnums verificationEnums, String uuid) {
。。。
//缓存中写入要验证的信息
cache.put(cacheKey(verificationEnums, mobile, uuid), code, 300L);
}
@Override
public boolean verifyCode(String mobile, VerificationEnums verificationEnums, String uuid, String code) {
Object result = cache.get(cacheKey(verificationEnums, mobile, uuid));
if (code.equals(result) || code.equals("0")) {
//校验之后,删除
cache.remove(cacheKey(verificationEnums, mobile, uuid));
return true;
} else {
return false;
}
}
/**
* 生成缓存key
*
* @param verificationEnums 验证场景
* @param mobile 手机号码
* @param uuid 用户标识 uuid
* @return
*/
static String cacheKey(VerificationEnums verificationEnums, String mobile, String uuid) {
return CachePrefix.SMS_CODE.getPrefix() + verificationEnums.name() + uuid + mobile;
}
。。。
}
# /lilishop-master/common-api/src/main/resources/application.yml
lili:
#短信模版配置
sms:
#登录
LOGIN: SMS_205755300
#注册
REGISTER: SMS_205755298
#找回密码
FIND_USER: SMS_205755301
#设置密码
UPDATE_PASSWORD: SMS_205755297
#支付密码
WALLET_PASSWORD: SMS_205755301
//使用例子
smsUtil.verifyCode(mobilePhone, VerificationEnums.REGISTER, uuid, code)
2. ServiceException 例外、例外タイプは、グローバル例外処理クラス
ServiceException はグローバルなビジネス例外クラスであり、そのほとんどが例外であり、多くの種類の例外があるため、さまざまな種類の例外情報を示すために例外の種類の列挙クラスを用意する必要があります。情報には、コード、メッセージ (一部のシステム国際化されたメッセージが使用されますが、このシステムでは使用されません)。
最後にスローされた ServiceException は、GlobalControllerExceptionHandler によってキャッチされ、処理するコードとメッセージを取得します。
注: 列挙型クラスに例外の種類を格納するのは、構成ファイルのように便利ではありません. 例外の種類の情報を変更したい場合は、コードを変更して再起動する必要があります.
//cn.lili.common.enums.ResultCode
/**
* 返回状态码
* 第一位 1:商品;2:用户;3:交易,4:促销,5:店铺,6:页面,7:设置,8:其他
*
* @author Chopper
* @since 2020/4/8 1:36 下午
*/
public enum ResultCode {
/**
* 成功状态码
*/
SUCCESS(200, "成功"),
/**
* 失败返回码
*/
ERROR(400, "服务器繁忙,请稍后重试"),
。。。
private final Integer code;
private final String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer code() {
return this.code;
}
public String message() {
return this.message;
}
}
//cn.lili.common.exception.ServiceException
/**
* 全局业务异常类
*
* @author Chopper
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 3447728300174142127L;
public static final String DEFAULT_MESSAGE = "网络错误,请稍后重试!";
/**
* 异常消息
*/
private String msg = DEFAULT_MESSAGE;
/**
* 错误码
*/
private ResultCode resultCode;
public ServiceException(String msg) {
this.resultCode = ResultCode.ERROR;
this.msg = msg;
}
public ServiceException() {
super();
}
public ServiceException(ResultCode resultCode) {
this.resultCode = resultCode;
}
public ServiceException(ResultCode resultCode, String message) {
this.resultCode = resultCode;
this.msg = message;
}
}
//cn.lili.common.exception.GlobalControllerExceptionHandler
/**
* 异常处理
*
* @author Chopper
*/
@RestControllerAdvice
@Slf4j
public class GlobalControllerExceptionHandler {
/**
* 如果超过长度,则前后段交互体验不佳,使用默认错误消息
*/
static Integer MAX_LENGTH = 200;
/**
* 自定义异常
*
* @param e
* @return
*/
@ExceptionHandler(ServiceException.class)
//设置响应状态码code
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ResultMessage<Object> handleServiceException(HttpServletRequest request, final Exception e, HttpServletResponse response) {
//如果是自定义异常,则获取异常,返回自定义错误消息
if (e instanceof ServiceException) {
ServiceException serviceException = ((ServiceException) e);
ResultCode resultCode = serviceException.getResultCode();
Integer code = null;
String message = null;
if (resultCode != null) {
code = resultCode.code();
message = resultCode.message();
}
//如果有扩展消息,则输出异常中,跟随补充异常
if (!serviceException.getMsg().equals(ServiceException.DEFAULT_MESSAGE)) {
message += ":" + serviceException.getMsg();
}
log.error("全局异常[ServiceException]:{}-{}", serviceException.getResultCode().code(), serviceException.getResultCode().message(), e);
return ResultUtil.error(code, message);
} else {
log.error("全局异常[ServiceException]:", e);
}
//默认错误消息
String errorMsg = "服务器异常,请稍后重试";
if (e != null && e.getMessage() != null && e.getMessage().length() < MAX_LENGTH) {
errorMsg = e.getMessage();
}
return ResultUtil.error(ResultCode.ERROR.code(), errorMsg);
}
。。。
}
//使用例子
throw new ServiceException(ResultCode.VERIFICATION_SMS_CHECKED_ERROR);
3. hutool ツールキットでの Snowflake アルゴリズム設定 ID
The shop platform uses SnowFlake, which uses the hutool tool class. 特定の分散学習については、この記事Distributed Global Unique ID (Learning Summary --- From Getting Started to Deepening) を 参照してください- CSDN ブログ
//cn.lili.common.utils.SnowFlake
/**
* 雪花分布式id获取
*
* @author Chopper
*/
@Slf4j
public class SnowFlake {
//静态
private static Snowflake snowflake;
/**
* 初始化配置
*
* @param workerId
* @param datacenterId
*/
public static void initialize(long workerId, long datacenterId) {
snowflake = IdUtil.getSnowflake(workerId, datacenterId);
}
public static long getId() {
return snowflake.nextId();
}
/**
* 生成字符,带有前缀的id。例如,订单编号 O202103301376882313039708161
*
* @param prefix
* @return
*/
public static String createStr(String prefix) {
return prefix + DateUtil.toString(new Date(), "yyyyMMdd") + SnowFlake.getId();
}
public static String getIdStr() {
return snowflake.nextId() + "";
}
}
//cn.lili.common.utils.SnowflakeInitiator
@Component
@Slf4j
public class SnowflakeInitiator {
/**
* 缓存前缀
*/
private static final String KEY = "{Snowflake}";
@Autowired
private Cache cache;
/**
* 尝试初始化
*
* @return
*/
//Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法
@PostConstruct
public void init() {
//从 redis 里面获取到自增长的主键
Long num = cache.incr(KEY);
long dataCenter = num / 32;
long workedId = num % 32;
//如果数据中心大于32,则抹除缓存,从头开始
if (dataCenter >= 32) {
cache.remove(KEY);
num = cache.incr(KEY);
dataCenter = num / 32;
workedId = num % 32;
}
//初始化
SnowFlake.initialize(workedId, dataCenter);
}
public static void main(String[] args) {
SnowFlake.initialize(0, 8);
System.out.println(SnowFlake.getId());
}
}
//使用例子
member.setId(SnowFlake.getIdStr());
order.setSn(SnowFlake.createStr("G"));
4.SpringEvent、RocketMQ
サービスへの会員登録が成功した後、クーポンやポイントなどの業務を後から会員に発行する必要があるため、プログラム結合を避けるためにSpringEventメソッド、つまりTransactionCommitSendMQListenerイベントリスナーとTransactionCommitSendMQEventイベントを利用する. 次に、イベントリスナーで、メッセージを送信するためにrocketMQTemplateが呼び出され、最後にロケットリスナーで処理されます〜
ぶっちゃけて言えばSpringEventとRocketMQを組み合わせて使っている.RocketMQを直接使うのは面倒じゃないから一緒に使う理由がわからない.SpringEvent自体の一番重要な役割はビジネスストリッピングとプログラムデカップリングである. RocketMQ の役割も果たします。[他のモジュールは RocketMQ を直接使用]
SpringEventのクラス名とアノテーションを見るまでは、トランザクション投入後にmqイベントが発生する、トランザクション投入リスナー、@TransactionalEventListenerアノテーションが使われているので、このクラスはトランザクション投入後に関連する業務を処理するためだけに使われていると思います。
//cn.lili.common.event.TransactionCommitSendMQEvent
/**
* 事务提交后发生mq事件
*
* @author paulG
* @since 2022/1/19
**/
public class TransactionCommitSendMQEvent extends ApplicationEvent {
private static final long serialVersionUID = 5885956821347953071L;
@Getter
private final String topic;
@Getter
private final String tag;
@Getter
private final Object message;
public TransactionCommitSendMQEvent(Object source, String topic, String tag, Object message) {
super(source);
this.topic = topic;
this.tag = tag;
this.message = message;
}
}
//cn.lili.common.listener.TransactionCommitSendMQListener
/**
* 事务提交监听器
*
* @author paulG
* @since 2022/1/19
**/
@Component
@Slf4j
public class TransactionCommitSendMQListener {
/**
* rocketMq
*/
@Autowired
private RocketMQTemplate rocketMQTemplate;
//在事务提交后再触发某一事件
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void send(TransactionCommitSendMQEvent event) {
log.info("事务提交,发送mq信息!{}", event);
String destination = event.getTopic() + ":" + event.getTag();
//发送订单变更mq消息
rocketMQTemplate.asyncSend(destination, event.getMessage(), RocketmqSendCallbackBuilder.commonCallback());
}
}
//使用例子:
//处理会员保存成功后的小事件:监听event,发送mp消息。最终在 mq 里面处理小事件
applicationEventPublisher.publishEvent(new TransactionCommitSendMQEvent("new member register", rocketmqCustomProperties.getMemberTopic(), MemberTagsEnum.MEMBER_REGISTER.name(), member));
C2. ユーザー名とパスワードのログイン インターフェイスの開発
これもインターフェースは 1 つだけで、最後にユーザー名とパスワードに基づいてユーザー ログイン用のトークンを取得します。
ビジネスの論理:
ビジネスロジックを導入する場合は、他のコード構造が関係します. 説明が必要な場合は、緑色の網掛けでマークし、次のコードロジックで詳しく紹介します.
コントローラ種別:MemberBuyerController
- 入力パラメーターを受け取った後、まず画像検証コードを検証します [検証は VerificationService 操作によって行われます]
- 検証コードの検証に問題がある場合は、 ServiceException がスローされます. 例外の種類: 検証コードの有効期限が切れています.再検証してください.
- 検証に問題がなければ、メンバーのビジネスクラスのユーザー名とパスワードのログインメソッドを呼び出し、トークンを取得し、レスポンス値ResultMessageを返します。
サービス クラス: mybatis-plus のみを使用し、カスタム マッパーは使用しません
- 最初にユーザー名または携帯電話番号に対応するアカウント情報を取得し、存在しない場合は ServiceException をスローします (例外の種類: ユーザーが存在しません)。
- ユーザーが存在する場合は、パスワードが正しく入力されているかどうかを判定し、間違っている場合は ServiceException をスローします (パスワードは登録時に BCryptPasswordEncoder によって保存され、当然復号化に使用されるため)
- 取得したメンバー アカウント情報に従って、トークンを生成し、それを返します。
コード ロジック:
//cn.lili.controller.passport.MemberBuyerController
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {
@Autowired
private MemberService memberService;
@Autowired
private VerificationService verificationService;
@ApiOperation(value = "用户名密码登录接口")
@PostMapping("/userLogin")
public ResultMessage<Object> userLogin(@NotNull(message = "用户名不能为空") @RequestParam String username,
@NotNull(message = "密码不能为空") @RequestParam String password,
@RequestHeader String uuid) {
verificationService.check(uuid, VerificationEnums.LOGIN);
return ResultUtil.data(this.memberService.usernameLogin(username, password));
}
。。。
}
//cn.lili.modules.member.serviceimpl.MemberServiceImpl
/**
* 会员接口业务层实现
*
* @author Chopper
* @since 2021-03-29 14:10:16
*/
@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {
/**
* 会员token
*/
@Autowired
private MemberTokenGenerate memberTokenGenerate;
@Override
public Token usernameLogin(String username, String password) {
//获取用户名或手机号码对应的帐号信息
Member member = this.findMember(username);
//判断用户是否存在
if (member == null || !member.getDisabled()) {
throw new ServiceException(ResultCode.USER_NOT_EXIST);
}
//判断密码是否输入正确
if (!new BCryptPasswordEncoder().matches(password, member.getPassword())) {
throw new ServiceException(ResultCode.USER_PASSWORD_ERROR);
}
//成功登录,则检测cookie中的信息,进行会员绑定。但是我发现前端并没有操作对应的cookies,所以暂时是没有用的
this.loginBindUser(member);
//根据会员账号信息,生成token
return memberTokenGenerate.createToken(member, false);
}
...
}
1.VerificationService の運用
[Lilishop Mall] No3-2. モジュールの詳細設計 A4 スライダー認証コードの詳細設計 写真 - CSDN ブログでは、 ユーザーがスライダー認証コードを使用してログインする際のプロセスについて説明しています。
スライダーの検証プロセス:
1. バックエンドはベースマップとスライダー画像を base64 に変換して返し、同時に正しいシャドウ X 軸位置を redis に格納します (キーには、後で getkey を検証するためにフロントエンドから渡された uuid が含まれます)。フロントエンド展示に戻します。
2. フロント エンドは base64 を取得して画像表示に変換し、スライドの動的な効果を実現します。ユーザーはスライダーを見た後、特定の位置にスライダーをスライドさせます. このときのスライダーの位置は入力パラメーターです. 手放した後, キャリブレーション スライダー インターフェイスを呼び出して, redis から正しい X 軸位置を取得し, と比較します.このときのスライダーの位置. パスした後、キャッシュの検証が再び成功し (キーには今の uuid も含まれています)、成功を返します。
3. フロントエンドは、スライダーの検証が成功したことを確認すると、ログイン インターフェイスを呼び出します. ログイン インターフェイスでは、まずキャッシュから検証成功を取得し、検証が成功した場合にログインします.
上記の 3 つの手順は、検証コード モジュールのインターフェイスに対応し、すべて非常に理解しやすい.2 つの型を redis に格納し、検証する必要があるコンテンツをキャッシュし、検証結果をキャッシュする必要があることを覚えておいてください。 .
//cn.lili.modules.verification.service.impl.VerificationServiceImpl
/**
* 验证码认证处理类
*
* @author Chopper
* @version v1.0
* 2020-11-17 14:59
*/
@Slf4j
@Component
public class VerificationServiceImpl implements VerificationService {
@Autowired
private VerificationSourceService verificationSourceService;
@Autowired
private VerificationCodeProperties verificationCodeProperties;
@Autowired
private Cache cache;
/**
* 创建校验
* @param uuid 前端传过来的的标识
* @return 验证码参数
*/
@Override
public Map<String, Object> createVerification(VerificationEnums verificationEnums, String uuid) {
if (uuid == null) {
throw new ServiceException(ResultCode.ILLEGAL_REQUEST_ERROR);
}
。。。
try {
。。。
//⭐重点,生成验证码数据
Map<String, Object> resultMap = SliderImageUtil.pictureTemplatesCut(
sliderFile, interfereSliderFile, originalFile,
verificationCodeProperties.getWatermark(), verificationCodeProperties.getInterfereNum());
//生成验证参数 有效时间 默认600秒,可以自行配置,存储到redis
cache.put(cacheKey(verificationEnums, uuid), resultMap.get("randomX"), verificationCodeProperties.getEffectiveTime());
resultMap.put("key", cacheKey(verificationEnums, uuid));
resultMap.put("effectiveTime", verificationCodeProperties.getEffectiveTime());
//移除横坐标移动距离,不能返回给用户哦
resultMap.remove("randomX");
return resultMap;
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("生成验证码失败", e);
throw new ServiceException(ResultCode.ERROR);
}
}
/**
* 根据网络地址,获取源文件
* 这里简单说一下,这里是将不可序列化的inputstream序列化对象,存入redis缓存
*
* @param originalResource
* @return
*/
private SerializableStream getInputStream(String originalResource) throws Exception {
Object object = cache.get(CachePrefix.VERIFICATION_IMAGE.getPrefix() + originalResource);
if (object != null) {
return (SerializableStream) object;
}
if (StringUtils.isNotEmpty(originalResource)) {
URL url = new URL(originalResource);
InputStream inputStream = url.openStream();
SerializableStream serializableStream = new SerializableStream(inputStream);
cache.put(CachePrefix.VERIFICATION_IMAGE.getPrefix() + originalResource, serializableStream);
return serializableStream;
}
return null;
}
/**
* 预校验图片 用于前端回显
*
* @param xPos X轴移动距离
* @param verificationEnums 验证key
* @return 验证是否成功
*/
@Override
public boolean preCheck(Integer xPos, String uuid, VerificationEnums verificationEnums) {
Integer randomX = (Integer) cache.get(cacheKey(verificationEnums, uuid));
if (randomX == null) {
throw new ServiceException(ResultCode.VERIFICATION_CODE_INVALID);
}
log.debug("{}{}", randomX, xPos);
//验证结果正确 && 删除标记成功
if (Math.abs(randomX - xPos) < verificationCodeProperties.getFaultTolerant() && cache.remove(cacheKey(verificationEnums, uuid))) {
//验证成功,则记录验证结果 验证有效时间与验证码创建有效时间一致
cache.put(cacheResult(verificationEnums, uuid), true, verificationCodeProperties.getEffectiveTime());
return true;
}
throw new ServiceException(ResultCode.VERIFICATION_ERROR);
}
/**
* 验证码校验
*
* @param uuid 用户标识
* @param verificationEnums 验证key
* @return 验证是否成功
*/
@Override
public boolean check(String uuid, VerificationEnums verificationEnums) {
//如果有校验标记,则返回校验结果
if (Boolean.TRUE.equals(cache.remove(this.cacheResult(verificationEnums, uuid)))) {
return true;
}
throw new ServiceException(ResultCode.VERIFICATION_CODE_INVALID);
}
/**
* 生成缓存key 记录缓存需要验证的内容
*
* @param verificationEnums 验证码枚举
* @param uuid 用户uuid
* @return 缓存key
*/
public static String cacheKey(VerificationEnums verificationEnums, String uuid) {
return CachePrefix.VERIFICATION_KEY.getPrefix() + verificationEnums.name() + uuid;
}
/**
* 生成缓存key 记录缓存验证的结果
*
* @param verificationEnums 验证码枚举
* @param uuid 用户uuid
* @return 缓存key
*/
public static String cacheResult(VerificationEnums verificationEnums, String uuid) {
return CachePrefix.VERIFICATION_RESULT.getPrefix() + verificationEnums.name() + uuid;
}
}
//使用例子
verificationService.createVerification(verificationEnums, uuid)
verificationService.preCheck(xPos, uuid, verificationEnums)
verificationService.check(uuid, VerificationEnums.LOGIN)
C3. SMS ログイン インターフェイスの開発
ビジネスの論理:
ビジネスロジックを導入する場合は、他のコード構造が関係します. 説明が必要な場合は、緑色の網掛けでマークし、次のコードロジックで詳しく紹介します.
コントローラ種別:MemberBuyerController
- 入力パラメータを受け取ったら、まずSMS 確認コードを確認します
- 検証コードの検証に問題がある場合、ServiceException がスローされます。
- 認証に問題がなければ、メンバーシップビジネスクラスの携帯電話番号認証コードログインメソッドを呼び出し、トークンを取得し、レスポンス値ResultMessageを返します。
サービス クラス: mybatis-plus のみを使用し、カスタム マッパーは使用しません
- 電話番号でユーザーを取得する
- 携帯電話番号が存在しない場合、抽象化されたメソッド this.registerHandler(member) を使用して、ユーザーが自動的に登録されます。
- 会員のアカウント情報をもとにトークンを生成して返却
コード ロジック:
この部分には複雑なロジックはありません。携帯電話番号に従って登録されたアカウント、ユーザー名が携帯電話番号であることを覚えておいてください。
注:具体的な使用方法を直接入力するだけです.結局、ソースコードから学ぶ必要があり、ここにコードを投稿するのは不便です.焦点は思考とロジックです〜
//cn.lili.controller.passport.MemberBuyerController#smsLogin
@PostMapping("/smsLogin")
public ResultMessage<Object> smsLogin(@NotNull(message = "手机号为空") @RequestParam String mobile,
@NotNull(message = "验证码为空") @RequestParam String code,
@RequestHeader String uuid) {
if (smsUtil.verifyCode(mobile, VerificationEnums.LOGIN, uuid, code)) {
return ResultUtil.data(memberService.mobilePhoneLogin(mobile));
} else {
throw new ServiceException(ResultCode.VERIFICATION_SMS_CHECKED_ERROR);
}
}
//cn.lili.modules.member.serviceimpl.MemberServiceImpl#mobilePhoneLogin
@Override
@Transactional
public Token mobilePhoneLogin(String mobilePhone) {
QueryWrapper<Member> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("mobile", mobilePhone);
//根据手机号码获取用户。疑问,不是有 findMember(String userNameOrMobile) 方法吗?为啥不用呢?因为有可能会有用户名为该手机号码的,所以不可以使用哦
Member member = this.baseMapper.selectOne(queryWrapper);
//如果手机号不存在则自动注册用户
if (member == null) {
member = new Member(mobilePhone, UuidUtils.getUUID(), mobilePhone);
//使用注册抽象出来的方法
this.registerHandler(member);
}
this.loginBindUser(member);
//根据会员账号信息,生成token
return memberTokenGenerate.createToken(member, false);
}
C4.モバイルアプリ/スモールプログラムスキャンQRコードログインインターフェースの開発
これには 4 番目のインターフェイスがあり、2 つは PC 側にあり、2 つは携帯電話のアプレット/APP 側にあります。
焦点は、プロセス全体を実行する 4 番目の長期トレーニング検証 QR コード インターフェイスです。
- まず、ユーザーが PC のフロント エンドでスキャン コードをクリックしてログインすると、バック エンドが呼び出されて QR コード インターフェイスが取得され、バック エンドは QR コードの情報を返します。トークン、有効期限、その他の情報が含まれます。コード表示。
- ユーザーはアプリ/小さなプログラムでコードをスキャンします.コードをスキャンした後、QR コードでトークンを取得し、バックエンド スキャン コード インターフェイスを呼び出し、トークンを入力パラメーターとして使用して、トークンに従ってQRコード情報をキャッシュし、コードをスキャンした状態に変更し、QRコード情報を再キャッシュし、この時点でスキャンされたQRコードのステータスに戻ります。
- フロントエンドはスキャンしたコードのステータスを取得した後、認可確認ページを開き、認可確認/拒否ボタンをクリックし、バックエンドで QR コード ログイン確認インターフェイスを呼び出し、トークンと認可ステータスを次のように使用します。パラメータを入力し、トークンのQRコード情報に基づいてキャッシュを取得し、コードのスキャンを確認/拒否するステータスを変更します. 確認された場合、useridは現在のログインユーザーに設定され、QRコード情報再キャッシュされ、成功を返します。
- 1.でQRコード情報を取得した後、フロントエンドはトークンと待機ステータスを入力パラメーターとして受け取り、バックエンドのロングラウンドトレーニングを呼び出してQRコードインターフェースを検証します.インターフェースはQRコードが正しいかどうかを判断します.毎秒有効かどうか 接続結果を返す機能。ユーザーが 2. のコード スキャン インターフェイスを呼び出すと、ポーリング インターフェイスはスキャンされた QR コード情報を返します。次に、フロントエンドがスキャンされたコード ステータスの応答を受信した後、トークンとスキャンされたステータスを入力パラメータとして再度受け取り、バックエンドのロングラウンド トレーニングを呼び出して QR コード インターフェースを検証します。ユーザーが 3. の確認/拒否インターフェイスを呼び出すと、ポーリング インターフェイスはステータスが確認/拒否の QR コード情報を返し、返されたステータスが確認の場合はログイン トークン情報を含めます。フロントエンドは、確認ステータスのレスポンスを受信すると、トークンに応じてログイン成功メソッドを実行し、拒否ステータスのレスポンスを受信すると、QR コードを更新するメソッドを呼び出します。
ビジネスの論理:
ビジネスロジックは上で見ることができ、非常に明確に説明されています〜
ポイントは4. インターフェースでのポーリング判定は、業務に応じて、このメソッドの戻り値にあるQRコードのログイン結果情報のステータスは、1:スキャン済み、2:同意済み、3. : 拒否、4: 期限切れ。
1 を返すのは、スキャンした QR コードのステータスをフロント エンドに表示する必要があるためです。
メソッドによって返されたステータスが 1 になった後、フロントエンドはこの長いポーリング メソッドを再度呼び出す必要があります。これは、まだ 2: 同意/3: 拒否のステータスを取得する必要があるためですが、現在のトークンについてはステータスを返すことができないためです。再度 1、それ以外の場合はフロントエンド このインターフェースは無許可の期間中連続して呼び出されます! ! ! 対応する判断を追加する必要があります! ! !
そのため、このメソッドの入力パラメータに beforeSessionStatus パラメータを追加して、最後に記録されたセッション ステータスを示します. フロント エンドが最初に呼び出したとき、値は 0: コードのスキャンを待っており、バック エンドが 1 を返したとき:コードをスキャンした後、新しい 1: スキャン済みのコードを beforeSessionStatus パラメータに割り当てます。その後、バックエンドは判断後に最終的な認証結果を返します~~~
バックエンドはどのように判断しますか? 以下のコードロジックを見てください~~~
コード ロジック:
//cn.lili.controller.passport.MemberBuyerController
/**
* 买家端,会员接口
*
* @author Chopper
* @since 2020/11/16 10:07 下午
*/
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {
@Autowired
private MemberService memberService;
@ApiOperation(value = "web-获取手机App/小程序登录二维码")
@PostMapping(value = "/pc_session", produces = "application/json;charset=UTF-8")
public ResultMessage<Object> createPcSession() {
return ResultUtil.data(memberService.createPcSession());
}
/**
* 长轮询:参考nacos
*
* 此方法的返回值中的二维码登录结果信息的状态可以是 1,2,3,4,返回 1 是因为需要在前端展示该二维码已经扫码的的状态,
* 返回 1 然后前端会再次调用此长轮询方法,并且之后(针对当前token来说)就不能再次返回 1 了,不然前端就会在未授权期间不断调用此接口了!
* 所以为了增加token状态的判断,我们在入参中添加了 beforeSessionStatus 参数,表示上次记录的session状态
*
* @param token
* @param beforeSessionStatus 上次记录的session状态,前端只可能传递 0 或 1
* @return
*/
@ApiOperation(value = "web-二维码长轮训校验登录")
@PostMapping(value = "/session_login/{token}", produces = "application/json;charset=UTF-8")
public Object loginWithSession(@PathVariable("token") String token, Integer beforeSessionStatus) {
log.info("receive login with session key {}", token);
//ResponseEntity继承了HttpEntity类,HttpEntity代表一个http请求或者响应实体
ResponseEntity<ResultMessage<Object>> timeoutResponseEntity =
new ResponseEntity<>(ResultUtil.error(ResultCode.ERROR), HttpStatus.OK);
int timeoutSecond = 20;
//建立一次连接,让他们等待尽可能长的时间。这样同时如果有新的数据到达服务器,服务器可以直接返回响应
DeferredResult<ResponseEntity<Object>> deferredResult = new DeferredResult<>(timeoutSecond * 1000L, timeoutResponseEntity);
//异步执行
CompletableFuture.runAsync(() -> {
try {
int i = 0;
while (i < timeoutSecond) {
//根据二维码 token 获取二维码登录结果信息
QRLoginResultVo queryResult = memberService.loginWithSession(token);
int status = queryResult.getStatus();
//为了满足接口调用,此处借助于 beforeSessionStatus 来判断。
//但是源代码里面写的是下面这个逻辑,我觉得不太好理解,于是按照此方法的使用流程写了自己的思考(其实就是将他的判断反转了一下,但是这个思维更好理解点,我觉得好理解了)
// if (status == beforeSessionStatus
// && (QRCodeLoginSessionStatusEnum.WAIT_SCANNING.getCode() == status
// || QRCodeLoginSessionStatusEnum.SCANNING.getCode() == status)) {
//如果status是等待扫描, 并且 beforeSessionStatus 是等待扫描,则(true || false && true) = true
//如果status是已经扫描/同意/拒绝/过期,并且 beforeSessionStatus 是等待扫描,则( false || T/F && false) = false
//如果status是已经扫描, 并且 beforeSessionStatus 是已经扫描,则( false || true && true) = true
//如果status是同意/拒绝/过期, 并且 beforeSessionStatus 是已经扫描,则( false || false && false) = false
if (QRCodeLoginSessionStatusEnum.WAIT_SCANNING.getCode() == status
|| (QRCodeLoginSessionStatusEnum.SCANNING.getCode() == status)
&& status == beforeSessionStatus) {
//睡眠一秒种,继续等待结果
TimeUnit.SECONDS.sleep(1);
} else {
//设置长轮询的返回值
deferredResult.setResult(new ResponseEntity<>(ResultUtil.data(queryResult), HttpStatus.OK));
break;
}
i++;
}
} catch (Exception e) {
log.error("获取登录状态异常,", e);
deferredResult.setResult(new ResponseEntity<>(ResultUtil.error(ResultCode.ERROR), HttpStatus.OK));
Thread.currentThread().interrupt();
}
}, Executors.newCachedThreadPool());
//返回长轮询
return deferredResult;
}
@ApiOperation(value = "App/小程序扫码")
@PostMapping(value = "/app_scanner", produces = "application/json;charset=UTF-8")
public ResultMessage<Object> appScanner(String token) {
return ResultUtil.data(memberService.appScanner(token));
}
@ApiOperation(value = "app扫码-登录确认:同意/拒绝")
@ApiImplicitParams({
@ApiImplicitParam(name = "token", value = "sessionToken", required = true, paramType = "query"),
@ApiImplicitParam(name = "code", value = "操作:0拒绝登录,1同意登录", required = true, paramType = "query")
})
@PostMapping(value = "/app_confirm", produces = "application/json;charset=UTF-8")
public ResultMessage<Object> appSConfirm(String token, Integer code) {
boolean flag = memberService.appSConfirm(token, code);
return flag ? ResultUtil.success() : ResultUtil.error(ResultCode.ERROR);
}
...
}
//cn.lili.modules.member.serviceimpl.MemberServiceImpl
@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {
@Override
public QRCodeLoginSessionVo createPcSession() {
//创建二维码信息
QRCodeLoginSessionVo session = new QRCodeLoginSessionVo();
//设置二维码状态:等待扫码
session.setStatus(QRCodeLoginSessionStatusEnum.WAIT_SCANNING.getCode());
//过期时间,20s
Long duration = 20 * 1000L;
session.setDuration(duration);
String token = CachePrefix.QR_CODE_LOGIN_SESSION.name() + SnowFlake.getIdStr();
session.setToken(token);
//将二维码信息缓存起来
cache.put(token, session, duration, TimeUnit.MILLISECONDS);
return session;
}
@Override
public Object appScanner(String token) {
//获取当前登录用户。这里也没用到,其实可以去掉,或者先存到二维码结果里面
AuthUser tokenUser = UserContext.getCurrentUser();
if (tokenUser == null) {
throw new ServiceException(ResultCode.USER_NOT_LOGIN);
}
//根据token获取二维码信息
QRCodeLoginSessionVo session = (QRCodeLoginSessionVo) cache.get(token);
if (session == null) {
//没有二维码或者二维码已过期,则返回二维码不存在/或者已经过期状态的二维码信息
return QRCodeLoginSessionStatusEnum.NO_EXIST.getCode();
}
//将拿到的二维码状态修改:已经扫码
session.setStatus(QRCodeLoginSessionStatusEnum.SCANNING.getCode());
//然后重新缓存二维码信息
cache.put(token, session, session.getDuration(), TimeUnit.MILLISECONDS);
//返回二维码状态
return QRCodeLoginSessionStatusEnum.SCANNING.getCode();
}
@Override
public boolean appSConfirm(String token, Integer code) {
//获取当前登录用户。
AuthUser tokenUser = UserContext.getCurrentUser();
if (tokenUser == null) {
throw new ServiceException(ResultCode.USER_NOT_LOGIN);
}
//根据 token 获取二维码信息
QRCodeLoginSessionVo session = (QRCodeLoginSessionVo) cache.get(token);
if (session == null) {
//没有二维码或者二维码已过期,则返回二维码不存在/或者已经过期状态的二维码信息
return false;
}
if (code == 1) {
//若登录状态是同意,则修改状态:确认登录
session.setStatus(QRCodeLoginSessionStatusEnum.VERIFIED.getCode());
//并且设置用户id
session.setUserId(Long.parseLong(tokenUser.getId()));
} else {
//若登录状态是拒绝,则修改状态:取消登录
session.setStatus(QRCodeLoginSessionStatusEnum.CANCELED.getCode());
}
//然后重新缓存二维码信息
cache.put(token, session, session.getDuration(), TimeUnit.MILLISECONDS);
return true;
}
@Override
public QRLoginResultVo loginWithSession(String sessionToken) {
//创建二维码登录结果对象
QRLoginResultVo result = new QRLoginResultVo();
result.setStatus(QRCodeLoginSessionStatusEnum.NO_EXIST.getCode());
//获取根据token获取缓存里的二维码信息
QRCodeLoginSessionVo session = (QRCodeLoginSessionVo) cache.get(sessionToken);
if (session == null) {
//没有二维码或者二维码已过期,则返回二维码不存在/或者已经过期状态的二维码信息
return result;
}
result.setStatus(session.getStatus());
//若存在二维码,则校验状态是否是:确认登录,是的话会修改二维码登录结果状态
if (QRCodeLoginSessionStatusEnum.VERIFIED.getCode().equals(session.getStatus())) {
//若是,则根据二维码里面的会员id拿到帐号信息
Member member = this.getById(session.getUserId());
if (member == null) {
throw new ServiceException(ResultCode.USER_NOT_EXIST);
} else {
//拿到帐号信息后,生成token
Token token = memberTokenGenerate.createToken(member, false);
//将token添加到二维码登录结果
result.setToken(token);
//删除缓存里面的二维码信息
cache.vagueDel(sessionToken);
}
}
//返回二维码登录结果
return result;
}
...
}
前端的部分代码 /lilishop-ui-master/buyer/src/pages/Login.vue
//调用web-二维码长轮训校验登录
async qrLogin() {
if(!this.qrSessionToken) return;
sCLogin(this.qrSessionToken,{beforeSessionStatus:this.scannerCodeLoginStatus}).then(response=>{
if (response.success) {
//拿到响应里面的二维码结果状态,并设置给 scannerCodeLoginStatus ,再下次调用此方法时会传递
this.scannerCodeLoginStatus = response.result.status;
switch (response.result.status) {
case 0:
case 1:
//已经扫码状态,继续调用web-二维码长轮训校验登录接口
this.qrLogin();break;
case 2:
//已经授权状态,调用登录成功方法
this.loginSuccess(response.result.token.accessToken,response.result.token.refreshToken);
break;
case 3:
//拒绝授权状态,调用刷新二维码方法
this.createPCLoginSession();
break;
default:
this.clearQRLoginInfo();
break
}
} else{
this.clearQRLoginInfo();
}
});
},