淘东电商项目(20) -会员唯一登录

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 淘东电商项目(20) -会员唯一登录

引言

在上一节《淘东电商项目(19) -日志打印》,主要讲解「淘东项目」slf4j日志框架的基本使用方法。

本文代码已提交至Github(版本号:e2b2700c36fdaef2636819743bd2d396ff641911),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop

本文主要简单的讲解会员服务如何实现唯一登录。

本文目录结构:

l____引言

l____ 1. 什么是唯一登录?

l____ 2. 会员唯一登录的实现思路

l____ 3. 功能实现

l________ 3.1 数据库设计

l________ 3.2 代码实现

l________________ 3.2.1 用户登录

l________________ 3.2.2 获取用户信息

l____ 4. 测试

l________ 4.1 三端唯一登录测试

l________ 4.2 根据token获取用户信息

l____总结

1. 什么是唯一登录?

平时我们常用过QQ、微信、钉钉等社交应用,他们都支持在PC端、Android端或者IOS端登录,这些应用都保证一个用户在某端只允许登录成功一次,这就是本文要讲的 「唯一登录」,以会员服务为例子。

2. 会员唯一登录的实现思路

登录代码流程图:

获取用户信息流程图:

3. 功能实现

3.1 数据库设计

在会员数据库(taodong-member)创建表,脚本如下:

CREATE TABLE `user_token` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `token` varchar(255) DEFAULT NULL,
  `login_type` varchar(255) CHARACTER SET utf8 DEFAULT NULL,
  `device_infor` varchar(255) DEFAULT NULL,
  `is_availability` int(2) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `create_time` date DEFAULT NULL,
  `update_time` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2

创建成功:

对应的UserTokenMapper:

/**
 * description: 用户TokenMapper
 * create by: YangLinWei
 * create time: 2020/3/3 4:42 下午
 */
public interface UserTokenMapper {
  /**
   * 根据userid+loginType +is_availability=0 进行查询
   * 
   * @param userId
   * @param loginType
   * @return
   */
  @Select("SELECT id as id ,token as token ,login_type as LoginType, device_infor as deviceInfor ,is_availability as isAvailability,user_id as userId"
      + "" + ""
      + " , create_time as createTime,update_time as updateTime   FROM user_token WHERE user_id=#{userId} AND login_type=#{loginType} and is_availability ='0'; ")
  UserTokenDo selectByUserIdAndLoginType(@Param("userId") Long userId, @Param("loginType") String loginType);
  /**
   * 根据userId+loginType token的状态修改为不可用
   * 
   * @param token
   * @return
   */
  @Update(" update user_token set is_availability  ='1', update_time=now() where token=#{token}")
  int updateTokenAvailability(@Param("token") String token);
  /**
   * token记录表中插入一条记录
   * 
   * @param userTokenDo
   * @return
   */
  @Insert("    INSERT INTO `user_token` VALUES (null, #{token},#{loginType}, #{deviceInfor}, 0, #{userId} ,now(),null ); ")
  int insertUserToken(UserTokenDo userTokenDo);
}

3.2 代码实现

3.2.1 用户登录

1.定义token生成工具类:

/**
 * description: Token生成工具类
 * create by: YangLinWei
 * create time: 2020/3/3 4:31 下午
 */
@Component
public class GenerateToken {
    @Autowired
    private RedisUtil redisUtil;
    /**
     * 生成令牌
     *
     * @param keyPrefix  令牌key前缀
     * @param redisValue redis存放的值
     * @return 返回token
     */
    public String createToken(String keyPrefix, String redisValue) {
        return createToken(keyPrefix, redisValue, null);
    }
    /**
     * 生成令牌
     *
     * @param keyPrefix  令牌key前缀
     * @param redisValue redis存放的值
     * @param time       有效期
     * @return 返回token
     */
    public String createToken(String keyPrefix, String redisValue, Long time) {
        if (StringUtils.isEmpty(redisValue)) {
            new Exception("redisValue Not nul");
        }
        String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
        redisUtil.setString(token, redisValue, time);
        return token;
    }
    /**
     * 根据token获取redis中的value值
     *
     * @param token
     * @return
     */
    public String getToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        String value = redisUtil.getString(token);
        return value;
    }
    /**
     * 移除token
     *
     * @param token
     * @return
     */
    public Boolean removeToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        return redisUtil.delKey(token);
    }
}

2.新增常量定义(其实不应该都只写到通用的Constants,应该每个微服务对应一个Constants。还有不会放到Apollo里,因为这些常量不经常变):

// token
String MEMBER_TOKEN_KEYPREFIX = "taodong.member.login";
// 安卓的登陆类型
String MEMBER_LOGIN_TYPE_ANDROID = "Android";
// IOS的登陆类型
String MEMBER_LOGIN_TYPE_IOS = "IOS";
// PC的登陆类型
String MEMBER_LOGIN_TYPE_PC = "PC";
// 登陆超时时间 有效期 90天
Long MEMBRE_LOGIN_TOKEN_TIME = 77776000L;

3.用户登录接口:

/**
 * description: 用户登录接口服务
 * create by: YangLinWei
 * create time: 2020/3/3 4:35 下午
 */
@Api(tags = "用户登录服务接口")
public interface MemberLoginService {
    /**
     * 用户登录接口
     *
     * @param userLoginInDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation(value = "会员用户登陆信息接口")
    BaseResponse<JSONObject> login(@RequestBody UserLoginInDTO userLoginInDTO);
}

4.用户登录接口实现:

@RestController
public class MemberLoginServiceImpl extends BaseApiService<JSONObject> implements MemberLoginService {
  @Autowired
  private UserMapper userMapper;
  @Autowired
  private GenerateToken generateToken;
  @Autowired
  private UserTokenMapper userTokenMapper;
  @Override
  public BaseResponse<JSONObject> login(@RequestBody UserLoginInDTO userLoginInpDTO) {
    // 1.验证参数
    String mobile = userLoginInpDTO.getMobile();
    if (StringUtils.isEmpty(mobile)) {
      return setResultError("手机号码不能为空!");
    }
    String password = userLoginInpDTO.getPassword();
    if (StringUtils.isEmpty(password)) {
      return setResultError("密码不能为空!");
    }
    // 判断登陆类型
    String loginType = userLoginInpDTO.getLoginType();
    if (StringUtils.isEmpty(loginType)) {
      return setResultError("登陆类型不能为空!");
    }
    // 目的是限制范围
    if (!(loginType.equals(Constants.MEMBER_LOGIN_TYPE_ANDROID) || loginType.equals(Constants.MEMBER_LOGIN_TYPE_IOS)
        || loginType.equals(Constants.MEMBER_LOGIN_TYPE_PC))) {
      return setResultError("登陆类型出现错误!");
    }
    // 设备信息
    String deviceInfor = userLoginInpDTO.getDeviceInfor();
    if (StringUtils.isEmpty(deviceInfor)) {
      return setResultError("设备信息不能为空!");
    }
    // 2.对登陆密码实现加密
    String newPassWord = MD5Util.MD5(password);
    // 3.使用手机号码+密码查询数据库 ,判断用户是否存在
    UserDo userDo = userMapper.login(mobile, newPassWord);
    if (userDo == null) {
      return setResultError("用户名称或者密码错误!");
    }
    // 用户登陆Token Session 区别
    // 用户每一个端登陆成功之后,会对应生成一个token令牌(临时且唯一)存放在redis中作为rediskey value userid
    // 4.获取userid
    Long userId = userDo.getUserId();
    // 5.根据userId+loginType 查询当前登陆类型账号之前是否有登陆过,如果登陆过 清除之前redistoken
    UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType);
    if (userTokenDo != null) {
      // 如果登陆过 清除之前redistoken
      String token = userTokenDo.getToken();
      Boolean isremoveToken = generateToken.removeToken(token);
      if (isremoveToken) {
       // 把该token的状态改为1
       userTokenMapper.updateTokenAvailability(token);
      }
    }
    // .生成对应用户令牌存放在redis中
    String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + loginType;
    String newToken = generateToken.createToken(keyPrefix, userId + "");
    // 1.插入新的token
    UserTokenDo userToken = new UserTokenDo();
    userToken.setUserId(userId);
    userToken.setLoginType(userLoginInpDTO.getLoginType());
    userToken.setToken(newToken);
    userToken.setDeviceInfor(deviceInfor);
    userTokenMapper.insertUserToken(userToken);
    JSONObject data = new JSONObject();
    data.put("token", newToken);
    return setResultSuccess(data);
  }
}
3.2.2 获取用户信息

1.新增获取用户信息接口:

/**
     * 根据token查询用户信息
     *
     * @param token
     * @return
     */
    @GetMapping("/getUserInfo")
    @ApiOperation(value = "/getUserInfo")
    BaseResponse<UserOutDTO> getInfo(@RequestParam("token") String token);

2.实现接口:

@Override
    public BaseResponse<UserOutDTO> getInfo(String token) {
        // 1.验证token参数
        if (StringUtils.isEmpty(token)) {
            return setResultError("token不能为空!");
        }
        // 2.使用token查询redis 中的userId
        String redisUserId = generateToken.getToken(token);
        if (StringUtils.isEmpty(redisUserId)) {
            return setResultError("token已经失效或者token错误!");
        }
        // 3.使用userID查询 数据库用户信息
        Long userId = TypeCastHelper.toLong(redisUserId);
        UserDo userDo = userMapper.findByUserId(userId);
        if (userDo == null) {
            return setResultError("用户不存在!");
        }
        // 下节课将 转换代码放入在BaseApiService
        return setResultSuccess(MeiteBeanUtils.doToDto(userDo, UserOutDTO.class));
    }

4. 测试

4. 4.1 三端唯一登录测试

目前数据库存在的用户如下,一共两位用户:

现在测试使用登录,启动会员项目后,使用swagger访问登录接口:

请求内容 返回结果

查看redis和数据库:

redis 数据库

再次访问接口,我们看看redis和数据库:

redis 数据库

再访问多几次,可以看到数据库只保持一条数据可用:

使用其它设备看看,发现其它设备也是只有一条数据可用:

当然,Redis一个用户最多只有三条数据:

4. 4.2 根据token获取用户信息

使用Swagger根据token获取用户信息(点击图片可以放大看结果)

Android IOS PC

总结

本文主要讲解会员使用Android、IOS和PC来实现唯一登录,并通过token来获取用户信息。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6天前
|
容器
会员管理系统实战开发教程07-会员消费
会员管理系统实战开发教程07-会员消费
|
6天前
|
新零售 供应链 数据挖掘
多商户商城入驻系统案例|方案设计|详情版
新零售的最大趋势是线上线下相结合,电商与线下实体商业,应该由原先的独立、冲突,走向混合、融合,通过精准化
|
6月前
|
前端开发 JavaScript Java
淘东电商项目(24) -获取验证码功能
淘东电商项目(24) -获取验证码功能
38 0
|
6天前
|
JavaScript 前端开发 索引
会员管理系统实战开发教程04-会员开卡
会员管理系统实战开发教程04-会员开卡
|
6天前
|
容器
会员管理系统实战开发教程06-会员充值
会员管理系统实战开发教程06-会员充值
|
9月前
|
SQL Java 数据库连接
用户注册【项目 商城】2
用户注册【项目 商城】2
40 0
|
6月前
|
关系型数据库 MySQL 数据库
淘东电商项目(16) -会员注册功能
淘东电商项目(16) -会员注册功能
45 0
|
6月前
|
前端开发 NoSQL 数据库
淘东电商项目(26) -门户登录功能
淘东电商项目(26) -门户登录功能
24 0
|
6月前
|
JSON 前端开发 NoSQL
淘东电商项目(27) -门户登出功能
淘东电商项目(27) -门户登出功能
28 0
|
6月前
|
前端开发 NoSQL 数据库
淘东电商项目(25) -门户注册功能
淘东电商项目(25) -门户注册功能
23 0