一、步驟說明
登錄流程
分佈式系統使用 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("系統錯誤!");
}
}
}