文章目录


引言

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

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

1.什么是唯一登录?

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

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

登录代码流程图:
《果然新鲜》电商项目(25)- 会员唯一登录_java
获取用户信息流程图:

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

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

创建成功:
《果然新鲜》电商项目(25)- 会员唯一登录_java_03
对应的​​​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));
}

4. 测试

4.1 三端唯一登录测试

目前数据库存在的用户如下,一共两位用户:
《果然新鲜》电商项目(25)- 会员唯一登录_java_04
现在测试使用登录,启动会员项目后,使用swagger访问登录接口:

请求内容
《果然新鲜》电商项目(25)- 会员唯一登录_spring_05
《果然新鲜》电商项目(25)- 会员唯一登录_eclipse_06

查看redis和数据库:
《果然新鲜》电商项目(25)- 会员唯一登录_hibernate_07
再次访问接口,我们看看redis和数据库:
《果然新鲜》电商项目(25)- 会员唯一登录_java_08
《果然新鲜》电商项目(25)- 会员唯一登录_java_09
再访问多几次,可以看到数据库只保持一条数据可用:
《果然新鲜》电商项目(25)- 会员唯一登录_eclipse_10
当然,Redis一个用户最多只有三条数据:
《果然新鲜》电商项目(25)- 会员唯一登录_hibernate_11
《果然新鲜》电商项目(25)- 会员唯一登录_tomcat_12

4.2 根据token获取用户信息

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

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

IOS
《果然新鲜》电商项目(25)- 会员唯一登录_spring_15
《果然新鲜》电商项目(25)- 会员唯一登录_tomcat_16
PC
《果然新鲜》电商项目(25)- 会员唯一登录_spring_17
《果然新鲜》电商项目(25)- 会员唯一登录_eclipse_18

5.总结

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