用戶登錄設計之唯一登錄

一、步驟說明

登錄流程

分佈式系統使用 redis 緩存用戶登錄信息 token = redis 的key, userId = redis 的 value ,單點系統可以使用session


用戶登錄 --> 1.參數驗證 --> 2、密碼md5加密  -- 3、 調用數據庫驗證 -->   4、登錄成功生成toket 保存到redis -->  返回toket

用戶查詢--> 1.驗證token參數是否爲空   -->  2、通過toket查詢userId  --> 3、調用數據庫查詢userId用戶 -->   返回用戶數據

登錄流程

在原登錄流程上添加數據表:

id
toket            令牌
userId           用戶id
login_type       登錄類型(pc/ ios / 安卓)
state            狀態(0,最後/當前登錄,1、登錄已失效)
time             登錄時間
登錄地址/ip等等
 

在這裏插入圖片描述
1、判斷type 字段是否爲正確值(pc/ ios / 安卓)
2、查詢該表是否存在該用戶登錄信息,存在,修改狀態,爲已失效,並刪除toket
3、添加對應登錄信息

後登錄用戶會把先登錄的用戶擠下線

二、登錄實現

1、令牌工具類 GenerateToken

令牌 臨時且唯一

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

	/**
	 * 生成令牌
	 * 
	 * @param prefix
	 *            令牌key前綴
	 * @param redisValue
	 *            redis存放的值
	 * @return 返回token
	 */
	public String createToken(String keyPrefix, String redisValue) {
		if (StringUtils.isEmpty(redisValue)) {
			new Exception("redisValue Not nul");
		}
		String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
		redisUtil.setString(token, redisValue);
		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、用戶登陸參數接收類

@Data
@ApiModel(value = "用戶登陸")
public class UserLoginInpDto {
	/**
	 * 手機號碼
	 */
	@ApiModelProperty(value = "手機號碼")
	private String mobile;
	/**
	 * 密碼
	 */
	@ApiModelProperty(value = "密碼")
	private String password;

	/**
	 * 登陸類型 PC端 移動端 安卓 IOS 平板
	 */
	@ApiModelProperty(value = "登陸類型")
	private String loginType;

}

3、登陸接口實現

	/**
	 * 用戶登陸接口
	 * 
	 * @param userEntity
	 * @return
	 */
	@PostMapping("/login")
	@ApiOperation(value = "會員用戶登陸信息接口")
	BaseResponse<JSONObject> login(@RequestBody UserLoginInpDto userLoginInpDto);

	@Autowired
	private UserMapper userMapper;
	
	@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("密碼不能爲空!");
		}
		// 2.調用數據庫驗證
		String newPassWord = MD5Util.MD5(password);
		UserDo userDo = userMapper.login(mobile, newPassWord);
		if (userDo == null) {
			return setResultError("用戶名稱或者密碼錯誤!");
		}
		// 獲取userId,將userId存放在redis中 key爲生成的令牌 value 爲userid
		String userId = userDo.getUserId() + "";
		String token = generateToken.createToken(Constants.MEMBER_LOGIN_TOKEN_KEYPREFIX, userId);
		JSONObject data = new JSONObject();
		data.put("token", token);
		return setResultSuccess(data);
	}

4、通過Token查詢用戶信息

	/**
	 * 根據token查詢用戶信息
	 * 
	 * @param userEntity
	 * @return
	 */
	@PostMapping("/getUserInfo")
	@ApiOperation(value = "/getUserInfo")
	BaseResponse<UserOutDto> getInfo(@Param("token") String token)
	@Override
	public BaseResponse<UserOutDto> getInfo(String token) {
		// 1.驗證token參數是否爲空
		if (StringUtils.isEmpty(token)) {
			return setResultError("token不能爲空!");
		}
		// 2. 根據token查詢用戶userId
		String redisUserId = generateToken.getToken(token);
		if (StringUtils.isEmpty(redisUserId)) {
			return setResultError("用戶會話已經失效或者token無效!");
		}
		// 3.從數據庫中查詢redisUserId信息
		Long userId = ConverterUtils.toLong(redisUserId);
		UserDo userDo = userMapper.findByUserId(userId);
		if (userDo == null) {
			return setResultError("用戶信息不存在");
		}
		// 4.將do轉換爲dto
		UserOutDto userOutDto = MiteBeanUtils.doToDto(userDo, UserOutDto.class);
		return setResultSuccess(userOutDto);
	}

5、打印MySQL日誌

####打印MyBatias日誌    
logging:
  level:
   com.mayikt.member.mapper: DEBUG

6、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 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=#{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);

三、redis 與數據庫數據同步(事務)

1、BaseApiService新增


// 調用數據庫層判斷
public Boolean toDaoResult(int result) {
		return result > 0 ? true : false;
}

2、redis 工具類RedisUtil 添加事務控制api

@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);

	}
}

3、redis+數據庫事務控制工具類-RedisDataSoureceTransaction

@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的值與數據庫的值保持不一致話

}

4、登陸接口業務邏輯事務控制

manualTransaction.begin 開啓事務,
manualTransaction.commit 提交事務
manualTransaction.rollback 回滾事務

@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("系統錯誤!");
		}

	}

}
發佈了198 篇原創文章 · 獲贊 30 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章