分布式电商项目——6.唯一登录实现(redis和数据库一致解决)

会员服务唯一登陆

登陆唯一表设计

CREATE TABLE `meite_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,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2
public interface UserMapper {

	@Insert("INSERT INTO `meite_user` VALUES (null,#{mobile}, #{email}, #{password}, #{userName}, null, null, null, '1', null, null, null);")
	int register(UserDo userDo);

	@Select("SELECT * FROM meite_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 meite_user  WHERE MOBILE=#{0} and password=#{1};")
	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 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 meite_user WHERE user_Id=#{userId}")
	UserDo findByUserId(@Param("userId") Long userId);

}
public interface UserTokenMapper {

	@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 meite_user_token WHERE user_id=#{userId} AND login_type=#{loginType} and is_availability ='0'; ")
	UserTokenDo selectByUserIdAndLoginType(@Param("userId") Long userId, @Param("loginType") String loginType);

	@Update("    update meite_user_token set is_availability ='1',update_time=now()   where user_id=#{userId} and login_type =#{loginType} ")
	int updateTokenAvailability(@Param("userId") Long userId, @Param("loginType") String loginType);

	// INSERT INTO `meite_user_token` VALUES ('2', '1', 'PC', '苹果7p', '1', '1');

	@Insert("    INSERT INTO `meite_user_token` VALUES (null, #{token},#{loginType}, #{deviceInfor}, 0, #{userId} ,now(),null ); ")
	int insertUserToken(UserTokenDo userTokenDo);
}
@Data
public class UserTokenDo extends BaseDo {
	/**
	 * id
	 */
	private Long id;
	/**
	 * 用户token
	 */
	private String token;
	/**
	 * 登陆类型
	 */
	private String loginType;

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

	/**
	 * 注册时间
	 */
	private Date createTime;
	/**
	 * 修改时间
	 *
	 */
	private Date updateTime;

}
@Data
public class BaseDo {
	/**
	 * 注册时间
	 */
	private Date createTime;
	/**
	 * 修改时间
	 *
	 */
	private Date updateTime;
	/**
	 * id
	 */
	private Long id;

	/**
	 * 是否可用 0可用 1不可用
	 */
	private Long isAvailability;
}

GenerateToken(令牌生成工具)

@Component
public class GenerateToken {
	@Autowired
	private RedisUtil redisUtil;

	/**
	 * 生成令牌
	 * 
	 * @param prefix
	 *            令牌key前缀
	 * @param redisValue
	 *            redis存放的值
	 * @return 返回token
	 */
	public String createToken(String keyPrefix, String redisValue) {
		return createToken(keyPrefix, redisValue, null);
	}

	/**
	 * 生成令牌
	 * 
	 * @param prefix
	 *            令牌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);

	}

}

新增常量信息

// token
	String MEMBER_TOKEN_KEYPREFIX = "mayikt.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;

唯一登陆接口实现

@Data
@ApiModel(value = "用户登陆参数")
public class UserLoginInpDTO {
	/**
	 * 手机号码
	 */
	@ApiModelProperty(value = "手机号码")
	private String mobile;
	/**
	 * 密码
	 */
	@ApiModelProperty(value = "密码")
	private String password;

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

}
@Api(tags = "用户登陆服务接口")
public interface MemberLoginService {
	/**
	 * 用户登陆接口
	 * 
	 * @param userEntity
	 * @return
	 */
	@PostMapping("/login")
	@ApiOperation(value = "会员用户登陆信息接口")
	BaseResponse<JSONObject> login(@RequestBody UserLoginInpDTO userLoginInpDTO);

}

package com.mayikt.member.service.impl;

@Autowired
	private UserMapper userMapper;
	@Autowired
	private UserTokenMapper userTokenMapper;

	@Autowired
	private GenerateToken generateToken;

	@Override
	public BaseResponse<JSONObject> login(@RequestBody UserLoginInpDTO 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("设备信息不能为空!");
		}
		String newPassWord = MD5Util.MD5(password);
		// 2.用户名称与密码登陆
		UserDo userDo = userMapper.login(mobile, newPassWord);
		if (userDo == null) {
			return setResultError("用户名称与密码错误!");
		}
		// 3.查询之前是否有过登陆
		Long userId = userDo.getUserId();
		UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType);
		if (userTokenDo != null) {
			// 4.清除之前的token
			String token = userTokenDo.getToken();
			Boolean removeToken = generateToken.removeToken(token);
			if (removeToken) {
				userTokenMapper.updateTokenAvailability(userId, loginType);
			}
		}
		// 5. 生成新的token
		String token = generateToken.createToken(Constants.MEMBER_TOKEN_KEYPREFIX, userId + "",
				Constants.MEMBRE_LOGIN_TOKEN_TIME);
		JSONObject tokenData = new JSONObject();
		tokenData.put("token", token);
		// 6.存入在数据库中
		UserTokenDo userToken = new UserTokenDo();
		userToken.setUserId(userId);
		userToken.setLoginType(userLoginInpDTO.getLoginType());
		userToken.setToken(token);
		userToken.setDeviceInfor(deviceInfor);
		userTokenMapper.insertUserToken(userToken);
		return setResultSuccess(tokenData);
	}

根据Token查询用户信息

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

	@Override
	public BaseResponse<UserOutDTO> getInfo(String token) {
		// 1.参数验证
		if (StringUtils.isEmpty(token)) {
			return setResultError("token不能为空!");
		}
		// 2.使用token向redis中查询userId
		String redisValue = generateToken.getToken(token);
		if (StringUtils.isEmpty(redisValue)) {
			return setResultError("token已经失效或者不正确");
		}
		Long userId = TypeCastHelper.toLong(redisValue);
		// 3.根据userId查询用户信息
		UserDo userDo = userMapper.findByUserId(userId);
		if (userDo == null) {
			return setResultError("用户信息不存在!");
		}
		// 4.将Do转换为Dto
		UserOutDTO doToDto = MeiteBeanUtils.doToDto(userDo, UserOutDTO.class);
		return setResultSuccess(doToDto);
	}

笔记
假设一个用户登陆可以同时在PC、IOS、Android 最终生成三个Token
mayikt.member.login8faa6de4327e4086a666ef1221284dfb –PC登陆
mayikt.member.login8faa6de4327e4086a666ef1221284dfb –redis存放PCtoken

会员登陆模块优化与门户平台整合

BaseApiService新增

// 调用数据库层判断
public Boolean toDaoResult(int result) {
		return result > 0 ? true : false;
}

修改RedisUtil

@Component
public class RedisUtil {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;
	/**
	 * 存放string类型
	 * 
	 * @param key
	 *            key
	 * @param data
	 *            数据
	 * @param timeout
	 *            超时间
	 */
	public void setString(String key, String data, Long timeout) {
		try {

			stringRedisTemplate.opsForValue().set(key, data);
			if (timeout != null) {
				stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
			}

		} catch (Exception e) {

		}

	}
	/**
	 * 开启Redis 事务
	 * 
	 * @param isTransaction
	 */
	public void begin() {
		// 开启Redis 事务权限
		stringRedisTemplate.setEnableTransactionSupport(true);
		// 开启事务
		stringRedisTemplate.multi();

	}
	/**
	 * 提交事务
	 * 
	 * @param isTransaction
	 */
	public void exec() {
		// 成功提交事务
		stringRedisTemplate.exec();
	}

	/**
	 * 回滚Redis 事务
	 */
	public void discard() {
		stringRedisTemplate.discard();
	}
	/**
	 * 存放string类型
	 * 
	 * @param key
	 *            key
	 * @param data
	 *            数据
	 */
	public void setString(String key, String data) {
		setString(key, data, null);
	}

	/**
	 * 根据key查询string类型
	 * 
	 * @param key
	 * @return
	 */
	public String getString(String key) {
		String value = stringRedisTemplate.opsForValue().get(key);
		return value;
	}

	/**
	 * 根据对应的key删除key
	 * 
	 * @param key
	 */
	public Boolean delKey(String key) {
		return stringRedisTemplate.delete(key);

	}
}

RedisDataSoureceTransaction(事务管理工具保持redis和数据库一致)

@Component
@Scope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE)
public class RedisDataSoureceTransaction {
	@Autowired
	private RedisUtil redisUtil;
	/**
	 * 数据源事务管理器
	 */
	@Autowired
	private DataSourceTransactionManager dataSourceTransactionManager;

	/**
	 * 开始事务 采用默认传播行为
	 * 
	 * @return
	 */
	public TransactionStatus begin() {
		// 手动begin数据库事务
		// 1.开启数据库的事务 事务传播行为
		TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
		// 2.开启redis事务
		redisUtil.begin();
		return transaction;
	}

	/**
	 * 提交事务
	 * 
	 * @param transactionStatus
	 *            事务传播行为
	 * @throws Exception
	 */
	public void commit(TransactionStatus transactionStatus) throws Exception {
		if (transactionStatus == null) {
			throw new Exception("transactionStatus is null");
		}
		// 支持Redis与数据库事务同时提交
		dataSourceTransactionManager.commit(transactionStatus);
	}

	/**
	 * 回滚事务
	 * 
	 * @param transactionStatus
	 * @throws Exception
	 */
	public void rollback(TransactionStatus transactionStatus) throws Exception {
		if (transactionStatus == null) {
			throw new Exception("transactionStatus is null");
		}
		// 1.回滚数据库事务 redis事务和数据库的事务同时回滚
		dataSourceTransactionManager.rollback(transactionStatus);
		// // 2.回滚redis事务
		// redisUtil.discard();
	}
	// 如果redis的值与数据库的值保持不一致话

}

登陆接口业务逻辑

@RestController
public class MemberLoginServiceImpl extends BaseApiService<JSONObject> implements MemberLoginService {
	@Autowired
	private UserMapper userMapper;
	@Autowired
	private GenerateToken generateToken;
	@Autowired
	private UserTokenMapper userTokenMapper;
	/**
	 * 手动事务工具类
	 */
	@Autowired
	private RedisDataSoureceTransaction manualTransaction;
	/**
	 * redis 工具类
	 */
	@Autowired
	private RedisUtil redisUtil;

	@Override
	public BaseResponse<JSONObject> login(@RequestBody UserLoginInpDTO 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("用户名称或者密码错误!");
		}
		TransactionStatus transactionStatus = null;
		try {

			// 1.获取用户UserId
			Long userId = userDo.getUserId();
			// 2.生成用户令牌Key
			String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + loginType;
			// 5.根据userId+loginType 查询当前登陆类型账号之前是否有登陆过,如果登陆过 清除之前redistoken
			UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType);
			transactionStatus = manualTransaction.begin();
			// // ####开启手动事务
			if (userTokenDo != null) {
				// 如果登陆过 清除之前redistoken
				String oriToken = userTokenDo.getToken();
				// 移除Token
				generateToken.removeToken(oriToken);
				int updateTokenAvailability = userTokenMapper.updateTokenAvailability(oriToken);
				if (updateTokenAvailability < 0) {
					manualTransaction.rollback(transactionStatus);
					return setResultError("系统错误");
				}
			}

			// 4.将用户生成的令牌插入到Token记录表中
			UserTokenDo userToken = new UserTokenDo();
			userToken.setUserId(userId);
			userToken.setLoginType(userLoginInpDTO.getLoginType());
			String newToken = generateToken.createToken(keyPrefix, userId + "");
			userToken.setToken(newToken);
			userToken.setDeviceInfor(deviceInfor);
			int result = userTokenMapper.insertUserToken(userToken);
			if (!toDaoResult(result)) {
				manualTransaction.rollback(transactionStatus);
				return setResultError("系统错误!");
			}

			// #######提交事务
			JSONObject data = new JSONObject();
			data.put("token", newToken);
			manualTransaction.commit(transactionStatus);
			return setResultSuccess(data);
		} catch (Exception e) {
			try {
				// 回滚事务
				manualTransaction.rollback(transactionStatus);
			} catch (Exception e1) {
			}
			return setResultError("系统错误!");
		}

	}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值