唯一登录、联合登录、单点登录——唯一登录

一、理论基础

(1)

  • 理解redis和token的使用
  • 使用token主要是为了安全,还有就是可以让安卓、IOS、PC等多端登录。用户每一个端登陆成功之后,会对应生成一个token令牌(临时且唯一)存放在redis中作为redis的key ,value 作为userid存储;根据userId+loginType 查询当前登陆类型账号之前是否有登陆过,如果登陆过 清除之前redis token。
  • 如果是PC端,token存放在PC端 的cookie ;如果在安卓 或者IOS端,token 存放在本地文件中。
  • 当前存在那些问题? ——用户如果退出或者修改密码、忘记密码的情况 下,需要对token状态进行标识以下,防止被篡改

(3)@Transactional注解属于声明式事务 还是 编程式事务?

答:声明式事务。加该注解不能控制redis事务,所以涉及到redis的事务要自定义方法,使用编程式事务,在begin和commit之间即需要控制数据库事务也需要控制redis事务;调用begin方式时会同时开始数据库事务和redis事务,commit、callback时也是同时的。(编程式事务相对于声明式事务唯一的优点就是事务粒度更精细)

(2)Redis删除Token与数据库状态Token如何保持一致?

答:redis本身也是支持事务的,只需要让redis事务和数据库事务保持一致,双方都是同步的即可(答案在下文)

二、代码实战

2.1、唯一登录实战

例子:会员登录(防止重复登录)

(1)登陆唯一登录表设计

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,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2

(2)Mapper

public interface UserMapper {

	int register(UserDo userDo);
	UserDo existMobile(@Param("mobile") String mobile);
	UserDo login(@Param("mobile") String mobile, @Param("password") String password);
	UserDo findByUserId(@Param("userId") Long userId);
}
public interface UserTokenMapper {
	UserTokenDo selectByUserIdAndLoginType(@Param("userId") Long userId, @Param("loginType") String loginType);
	int updateTokenAvailability(@Param("userId") Long userId, @Param("loginType") String loginType);
	int insertUserToken(UserTokenDo userTokenDo);
}

(3)Dao

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

(4)生成token的工具类:GenerateToken

根据token获取redis中的value值

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

	}

}

(5)新增常量信息

    // 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;

(6)唯一登陆接口实现

DTO:

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

Service:

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

}

Service的impl: 

@RestController
public class MemberLoginServiceImpl extends BaseApiService<JSONObject> implements MemberLoginService {
	@Autowired
	private UserMapper userMapper;
	@Autowired
	private GenerateToken generateToken;
	@Autowired
	private UserTokenMapper userTokenMapper;
	@Autowired
	private RedisDataSoureceTransaction redisDataSoureceTransaction;
	@Autowired
	private RedisUtil redisUtil;

	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("用户名称或者密码错误!");
		}
		// 用户登陆Token Session 区别
		// 用户每一个端登陆成功之后,会对应生成一个token令牌(临时且唯一)存放在redis中作为rediskey value userid
		TransactionStatus transactionStatus = null;
		try {
			// 4.获取userid
			Long userId = userDo.getUserId();
			// 5.根据userId+loginType 查询当前登陆类型账号之前是否有登陆过,如果登陆过 清除之前redistoken
			UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType);
            //开启以下事务
			transactionStatus = redisDataSoureceTransaction.begin();
			if (userTokenDo != null) {
				// 如果登陆过 清除之前redistoken
				String token = userTokenDo.getToken();
				// 如果开启redis事务的话,删除的时候 方法会返回false
				Boolean removeToken = generateToken.removeToken(token);
				// 把该token的状态改为1
				int updateTokenAvailability = userTokenMapper.updateTokenAvailability(token);
				if (!toDaoResult(updateTokenAvailability)) {
					return setResultError("系统错误!");
				}

			}

			// .生成对应用户令牌存放在redis中

			// 1.插入新的token
			UserTokenDo userToken = new UserTokenDo();
			userToken.setUserId(userId);
			userToken.setLoginType(userLoginInpDTO.getLoginType());

			String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + loginType;
			String newToken = generateToken.createToken(keyPrefix, userId + "");

			userToken.setToken(newToken);
			userToken.setDeviceInfor(deviceInfor);
			int insertUserToken = userTokenMapper.insertUserToken(userToken);
			if (!toDaoResult(insertUserToken)) {
				redisDataSoureceTransaction.rollback(transactionStatus);
				return setResultError("系统错误!");
			}
			JSONObject data = new JSONObject();
			data.put("token", newToken);
            //没有任何问题才commit
			redisDataSoureceTransaction.commit(transactionStatus);
			return setResultSuccess(data);
		} catch (Exception e) {
			try {
                //有问题则回滚
				redisDataSoureceTransaction.rollback(transactionStatus);
			} catch (Exception e2) {
				// TODO: handle exception
			}
			return setResultError("系统错误!");
		}

	}
	// 查询用户信息的话如何实现? redis 与数据库如何保证一致问题

	@Override
	public BaseResponse<JSONObject> delToken(String token) {
		if (StringUtils.isEmpty(token)) {
			return setResultError("token不能为空!");
		}
		Boolean delKey = redisUtil.delKey(token);
		return delKey ? setResultSuccess("删除成功") : setResultError("删除失败!");
	}

	// redis 的值如何与数据库的值保持是一致性问题
	// @Transactional 不能控制redis的事务
	// redis 中是否存在事务 肯定是肯定是存在事务
	// 自定义方法 使用编程事务 begin(既需要控制数据库的事务也需要控制redis) commit

}

疑问:查询用户信息的话如何实现? redis 与数据库如何保证一致问题?

参考:https://blog.csdn.net/RuiKe1400360107/article/details/103706472

以下代码其实可以封装成一个注解:

            //开启一下事务
            transactionStatus = redisDataSoureceTransaction.begin();

           ......

          //没有任何问题才commit
            redisDataSoureceTransaction.commit(transactionStatus);
            return setResultSuccess(data);
        } catch (Exception e) {
            try {
                //有问题则回滚
                redisDataSoureceTransaction.rollback(transactionStatus);
            } catch (Exception e2) {
                // TODO: handle exception
            }
            return setResultError("系统错误!");

Redis与 DataSource 事务封装:

@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的值与数据库的值保持不一致话

}

(7)根据Token查询用户信息

使用token向redis中查询userId

@RestController
public class MemberServiceImpl extends BaseApiService<UserOutDTO> implements MemberService {
	@Autowired
	private UserMapper userMapper;
	@Autowired
	private GenerateToken generateToken;

	@Override
	public BaseResponse<UserOutDTO> existMobile(String mobile) {
		// 1.验证参数
		if (StringUtils.isEmpty(mobile)) {
			return setResultError("手机号码不能为空!");
		}

		// 2.根据手机号码查询用户信息 单独定义code 表示是用户信息不存在把
		UserDo userEntity = userMapper.existMobile(mobile);
		if (userEntity == null) {
			return setResultError(Constants.HTTP_RES_CODE_EXISTMOBILE_203, "用户信息不存在!");
		}

		// 3.将do转换成dto
		return setResultSuccess(MeiteBeanUtils.doToDto(userEntity, UserOutDTO.class));
	}

	@Override
	public BaseResponse<UserOutDTO> getInfo(String token) {
		// 1.验证token参数
		if (StringUtils.isEmpty(token)) {
			return setResultError("token不能为空!");
		}
		// 2.使用token查询redis 中的userId
		String redisUserId = generateToken.getToken(token);
		if (StringUtils.isEmpty(redisUserId)) {
			return setResultError("token已经失效或者token错误!");
		}
		// 3.使用userID查询 数据库用户信息
		Long userId = TypeCastHelper.toLong(redisUserId);
		UserDo userDo = userMapper.findByUserId(userId);
		if (userDo == null) {
			return setResultError("用户不存在!");
		}
		// 可重构,将代码放入在BaseApiService
		return setResultSuccess(MeiteBeanUtils.doToDto(userDo, UserOutDTO.class));
	}
	// token存放在PC端 cookie token 存放在安卓 或者IOS端 存放在本地文件中
	// 当前存在那些问题? 用户如果退出或者修改密码、忘记密码的情况 对token状态进行标识
	// token 如何防止伪造 真正其实很难防御伪造 尽量实现在安全体系 xss 只能在一些某些业务模块上加上必须验证本人操作
}

 

 

 

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要实现SpringCloud单点登录,首先需要创建一个SpringBoot模块sso-server,并在Eureka上注册。在pom.xml文件中引入相关依赖,包括spring-boot-starter-thymeleaf、spring-boot-starter-data-redis等。 还需要在pom.xml文件中引入sso_base_util依赖以及spring-boot-starter-web、spring-boot-starter-test和spring-cloud-starter-netflix-eureka-client等依赖。 具体实现单点登录的步骤可以参考以往的文章,比如《SpringBoot——简单整合Redis实例》、《SpringCloud入门——Feign服务调用》和《SpringCloud入门——Zuul路由配置》。 通过以上步骤,你就可以实现SpringCloud的单点登录功能了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [SpringCloud入门 —— SSO 单点登录](https://blog.csdn.net/qq_34383510/article/details/121610477)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [SpringCloud简单的单点登录](https://blog.csdn.net/June_FlyingFrost/article/details/90384405)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值