《果然新鲜》电商项目(25)- 会员唯一登录

Posted IT老刘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《果然新鲜》电商项目(25)- 会员唯一登录相关的知识,希望对你有一定的参考价值。

引言

在上一节《果然新鲜》电商项目(24)- 日志打印,主要讲解slf4j日志框架的基本使用方法。

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

1.什么是唯一登录?

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

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

登录代码流程图:

获取用户信息流程图:

3. 功能实现

3.1 数据库设计

在会员数据库(guoranxinxian-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

package com.guoranxinxian.entity;

import lombok.Data;

@Data
public class UserTokenDo 
    /**
     * id
     */
    private Long id;
    /**
     * 用户token
     */
    private String token;
    /**
     * 登陆类型
     */
    private String loginType;

    /**
     * 设备信息
     */
    private String deviceInfor;
    /**
     * 用户userId
     */
    private Long userId;




package com.guoranxinxian.mapper;

import com.guoranxinxian.entity.UserTokenDo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * description: 用户TokenMapper
 */
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生成工具类:

package com.guoranxinxian.util;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * description: Token生成工具类
 */
@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 = "guoranxinxian.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.用户登录接口:

UserLoginInDTO

package com.guoranxinxian.member.dto.input;


import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * description: 用户登录请求参数
 */
@Data
@ApiModel(value = "用户登录参数")
public class UserLoginInDTO 
    /**
     * 手机号码
     */
    @ApiModelProperty(value = "手机号码")
    private String mobile;
    /**
     * 密码
     */
    @ApiModelProperty(value = "密码")
    private String password;

    /**
     * 登陆类型 PC、Android 、IOS
     */
    @ApiModelProperty(value = "登陆类型")
    private String loginType;
    /**
     * 设备信息
     */
    @ApiModelProperty(value = "设备信息")
    private String deviceInfor;



package com.guoranxinxian.service;

import com.alibaba.fastjson.JSONObject;
import com.guoranxinxian.api.BaseResponse;
import com.guoranxinxian.member.dto.input.UserLoginInDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * 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.用户登录接口实现:

package com.guoranxinxian.mapper;

import com.guoranxinxian.entity.UserDo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

/**
 * description: 用户mapper
 */
public interface UserMapper 

    @Insert("INSERT INTO `user` VALUES (null,#mobile, #email, #password, #userName, null, null, null, '1', null, null, null);")
    int register(UserDo userEntity);

    @Select("SELECT * FROM user WHERE MOBILE=#mobile;")
    UserDo existMobile(@Param("mobile") String mobile);

    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE,EMAIL AS EMAIL,PASSWORD AS PASSWORD, USER_NAME AS USER_NAME ,SEX AS SEX ,AGE AS AGE ,CREATE_TIME AS CREATETIME,IS_AVALIBLE AS ISAVALIBLE,PIC_IMG AS PICIMG,QQ_OPENID AS QQOPENID,WX_OPENID AS WXOPENID "
            + "  FROM user  WHERE MOBILE=#mobile and password=#password;")
    UserDo login(@Param("mobile") String mobile, @Param("password") String password);

    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE,EMAIL AS EMAIL,PASSWORD AS PASSWORD, USER_NAME AS userName ,SEX AS SEX ,AGE AS AGE ,CREATE_TIME AS CREATETIME,IS_AVALIBLE AS ISAVALIBLE,PIC_IMG AS PICIMG,QQ_OPENID AS QQOPENID,WX_OPENID AS WXOPENID"
            + " FROM user WHERE user_Id=#userId")
    UserDo findByUserId(@Param("userId") Long userId);



package com.guoranxinxian.impl;

import com.alibaba.fastjson.JSONObject;
import com.guoranxinxian.api.BaseResponse;
import com.guoranxinxian.constants.Constants;
import com.guoranxinxian.entity.BaseApiService;
import com.guoranxinxian.entity.UserDo;
import com.guoranxinxian.entity.UserTokenDo;
import com.guoranxinxian.mapper.UserMapper;
import com.guoranxinxian.mapper.UserTokenMapper;
import com.guoranxinxian.member.dto.input.UserLoginInDTO;
import com.guoranxinxian.service.MemberLoginService;
import com.guoranxinxian.util.GenerateToken;
import com.guoranxinxian.util.MD5Util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@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.实现接口:


  @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 token=#token and is_availability ='0'; ")
  UserTokenDo selectByToken(String token);
@Override
public BaseResponse<UserOutDTO> getInfo(String token) 
    UserTokenDo userTokenDo = userTokenMapper.selectByToken(token);
    if(userTokenDo == null)
        return setResultError("该用户没有登录!");
    

    UserDo userDo = userMapper.findByUserId(userTokenDo.getUserId());
    if (userDo == null) 
        return setResultError("用户不存在!");
    
    // 下节课将 转换代码放入在BaseApiService
    return setResultSuccess(BeanUtils.doToDto(userDo, UserOutDTO.class以上是关于《果然新鲜》电商项目(25)- 会员唯一登录的主要内容,如果未能解决你的问题,请参考以下文章

《果然新鲜》电商项目(40)- SSO单点登录(退出功能)

《果然新鲜》电商项目(21)- 会员注册功能

《果然新鲜》电商项目(39)- SSO单点登录(登录功能完善)

《果然新鲜》电商项目(22)- DTO接口细分

《果然新鲜》电商项目(37)-SSO单点登录(改造SSO认证服务登录界面)

《果然新鲜》电商项目(36)-SSO单点登录(集成SSO认证服务)