Redis实现单点登录(并且只能在一台设备登录)
1、Redis配置
依赖注入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
全局配置
spring:
redis:
host: 127.0.0.1
port: 6379
timeout: 30000
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0
Redis工具配置
因为官方给的工具类太难用了,所以我们选择自己写一个工具
RedisConfig
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
RedisUtils
以下举几个例子,更多方法请在Redis的文档里查看
@Component
public class RedisUtils {
@Autowired
RedisTemplate<String,Object> redisTemplate;
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time,
TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public boolean exist(String key){
return redisTemplate.hasKey(key);
}
public void remove(String key){
System.out.println(key);
redisTemplate.delete(key);
}
/**
* 读取缓存
*
* @param key
* @return
*/
public String get(final String key) {
return String.valueOf(redisTemplate.opsForValue().get(key));
}
}
2、拦截器配置
实现只有登录过的session才能访问某些接口
1、编写redis拦截器
public class RedisSessionInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtils redisUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{
String sid = request.getSession().getId();
if(redisUtils.exist(sid))
return true;
response401(response);
return false;
}
private void response401(HttpServletResponse response){
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try
{
response.setStatus(StatusCode.NEED_LOGIN);
response.getWriter().print(JSON.toJSONString(new Result<String>(StatusCode.NEED_LOGIN,"","用户未登录"))) ;
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
2、将拦截器注册到拦截器配置类中
@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
//添加拦截器,并设置拦截的接口、忽略的接口
registry.addInterceptor(redisSessionInterceptor())
.addPathPatterns("/api/user/**")
.excludePathPatterns("/api/user/login")
.excludePathPatterns("/swagger-ui.html/**")
.excludePathPatterns("/api/user/register");
}
@Bean
public RedisSessionInterceptor redisSessionInterceptor(){
return new RedisSessionInterceptor();
}
}
此处注意,拦截器一定要在配置文件中注入,如此处在配置类中使用@Bean注解注入(这样才能保证先执行拦截器中Autowired自动注入的对象,否则Utils会注入失败)
3、登录逻辑编写
检查sessionid逻辑
前端每次打开网页时,应调用此接口,检查session是否存在于redis中,如果存在,则可直接跳过登录的步骤
public Result<String> setRedisResult(HttpServletRequest request){
//第一次登录
//1. 取出当期客户端的sessionId
String sId=request.getSession().getId();
//2. 查询该sessionId 是否存在于redis
boolean exists = redisUtils.exist(sId);
if (!exists){
return new Result<String>(StatusCode.NEED_LOGIN,"","用户需登录");
}else {
//2.2 已经登录过,则存入redis中刷新过期时间,再直接返回成功页面
redisUtils.set(sId,"login success",1000);
return new Result<String>(StatusCode.SUCCESS,"","sid验证通过");
}
}
登录逻辑
此处省略了用户User类与Mybatis-plus mapper层
@Autowired
RedisUtils redisUtils;
@Autowired
UserMapper userMapper;
public Result<String> login(HttpServletRequest request, Long id, String password){
User user = userMapper.selectById(id);
if(user==null){
return new Result<String>(StatusCode.LOGIN_MATCH_ERROR,"","用户不存在");
}else if(!user.getPassword().equals(password)){
return new Result<String>(StatusCode.LOGIN_MATCH_ERROR,"","密码错误");
}else {
if(redisUtils.exist(id.toString())){
redisUtils.remove(redisUtils.get(id.toString()));
redisUtils.remove(id.toString());
}
redisUtils.set(request.getSession().getId(),"login success",1000);//有效期为1000s
redisUtils.set(id.toString(),request.getSession().getId(),1000);
//如果通过后,写入session域进行共享,即使是负载不同端口,sessionId不会发生变化
return new Result<String>(StatusCode.SUCCESS,getToken(user),"登录成功!");
}
}
因为要实现只有一个设备能同时登录本账号,所以登录验证通过后,第一步是检查该id是否最近登录过。如果登录过则将Redis中存的两条记录先删除
if(redisUtils.exist(id.toString())){
redisUtils.remove(redisUtils.get(id.toString()));
redisUtils.remove(id.toString());
}
然后再向redis中存入两条新的缓存数据
一条key:sessionid value:login success
另一条 key: id(账号) value: sessionid
通过这两条记录,就可以判断当前设备是否登录,当前账号是否登录,并可通过登录账号的id查到登录设备的sessionid
退出登录
public Result<String> userLoginOut(HttpServletRequest request){
String sId=request.getSession().getId();
redisUtils.remove(sId);
return new Result<String>(StatusCode.SUCCESS,"","已退出登录!");
}
退出登录时,获取下sid删了就行
此处虽然redis中 key为id的记录并未删除,但是并未影响功能
如果想实现把key为id的记录也删了,可以通过登录时获取token,在退出登录时通过header中获取的token解析出id再删除