实现的效果就是A在A点登陆在浏览器登陆了A账户,之后B又在B点登陆了A账户,此时,B可以正常登陆账户,A点的A账户也不会立即掉线,一旦A点的A账户执行操作也就是向后端发送请求时,A点A账户会在页面收到弹窗提示(账号已在别处登陆)点击登陆后,则会跳到登陆页面

1.整体思路    

       采用拦截器校验缓存的策略。当用户登陆的时候,首先校验用户名和密码是否正确,如果错误则直接向前端返回用户名或者密码错误.如果正确则生成一个UUID的token,以用户名为键,token为值,存入到redis!同时封装一个tokenUtil类,只存储String类型的用户名,还有String类型的token。将当前用户名和token封装到这个类,返回给前端将收到的tokenUtil封装到请求的请求头中,保证除了登陆请求除外的所有请求都会携带一个tokenUtil,里面包含了用户的用户名还有一个token值。然后后端编写拦截器,获取请求头中的token,通过工具类转化成TokenUtil类的对象,然后通过对象的中的用户名字段去获取redis中的值,这时候拿redis里面拿到的token值,和tokenUtil里面的token做比较,如果不一致,则意味着有人又登陆了这个账户,以这个账户为键,重新生成了一个token存入到了redis,所以匹配不上,这时候进行拦截,并且返回前端相对应的状态码,前端做出相对应提示。

2.代码

       有了思路,写代码就容易了。下面我把的代码展示一下,一些踩过的坑也会标注出来。

     2.1整个项目框架   ,新建项目过程就省略了

Vue实现Redis订阅消息 vue调用redis_redis

  2.2依赖导入 

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>SSOLogin</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SSOLogin</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

hutool是一个非常好用的工具包,大家可以尝试使用一下

2.3核心代码

@RestController
@CrossOrigin
public class UserController {
    @Autowired
    private UserService userService;


    @PostMapping("/login")
    public Result checkLogin(String account,String password){
        return userService.checkLogin(account,password);
    }

    @GetMapping("/test")
    public Result test(){
        return Result.ok();
    }
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired(required = false)
    private StringRedisTemplate redisTemplate;

    /**
     * 处理登陆,实现只能一处登陆的效果
     *
     * @param account
     * @param password
     * @return
     */
    //Redis 是一种菲关系型数据库  Map类型的数据结构  读写速度 每秒百万级别
    @Override
    public Result checkLogin(String account, String password) {
        //1.TODO 判断用户名密码是否正确
        LambdaQueryWrapper<User> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getAccount,account)
                .eq(User::getPassword,password);
        User user = userMapper.selectOne(queryWrapper);
        if(user==null){
            //2.TODO  如果错误,直接后端返回fasle
            return Result.fail("用户名或者密码错误");
        }
        //3.TODO  如果正确 以账户的MD5加密格式为键 存储一个TOken进入redis
        //TODO 并且把账户的加密格式以及随机的Token封装成对象返回给前端;
        //TODO 前端每次请求携带在请求头中
        String key = MD5.create().digestHex(password);
        String token = UUID.randomUUID().toString();
        TokenUtil tokenUtil =new TokenUtil();
        tokenUtil.setPwd(key);
        tokenUtil.setToken(token);
        redisTemplate.opsForValue().set(key,token);
        //4.TODO 返回正确
        return Result.ok(tokenUtil);

        //拦截器里携带tokenUtil,用pwd匹配redis获取token,匹配不上,已经登陆过了
    }
}

我这里封装的是密码,大家改成用户名就好了,这里的mapper就是继承了baseMapper没有新增任何接口,就是单纯的查询用户信息的。这里就不展示了

拦截器

/**
 * @Description:
 * @author: RainMoon
 * @date: 2022年11月24日 22:58
 */
public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate redisTemplate;

    public LoginInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if(HttpMethod.OPTIONS.toString().equals(request.getMethod())){
            System.out.println("OPTIONS请求,放行");
            return true;
        }
        String token = request.getHeader("token");
        String requestURI = request.getRequestURI();
        System.out.println("requestURI = " + requestURI);
        System.out.println("token = " + token);
        if(StringUtils.isBlank(token)){
            response.setStatus(401);
            System.out.println("拦截 401");
            return false;
        }
        TokenUtil tokenUtil = JSONUtil.toBean(token, TokenUtil.class);
        String tokenValue = redisTemplate.opsForValue().get(tokenUtil.getPwd());
        System.out.println("tokenValue = " + tokenValue);
        if(tokenValue!=null&&tokenValue.equals(tokenUtil.getToken())){
            System.out.println("token正确,放行");
            return true;
        }
        System.out.println("token错误,拦截 520");
        response.setStatus(520);
        return false;
    }
}

这里要注意一下,因为拦截器的加载优先级比较高,所以不能直接注入RedisTemplate,所以给他配成属性,生成一个有参构造方法,然后在配置类里面注入RedisTemplate,添加拦截器的时候从构造器赋值

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired(required = false)
    private StringRedisTemplate redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(redisTemplate))
                .excludePathPatterns("/login");
    }
}

然后是对应实体类,tokenUtil类,返回工具类,TokenUtil类中的pwd换成用户的账号就好了

@Data
@TableName("t_user")
public class User {
    //用户ID
    private Integer id;
    //用户名字
    private String username;
    //密码
    private String password;
    //账户
    private String account;

}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Boolean success;
    private String errorMsg;
    private Object data;

    public static Result ok(){
        return new Result(true, null, null);
    }
    public static Result ok(Object  data){
        return new Result(true, null, data);
    }
    public static Result fail(String errorMsg){
        return new Result(false, errorMsg, null);
    }
}
@Data
public class TokenUtil {

    private String pwd;

    private String token;
}

然后是前端代码,就是两个请求,大家也可以把axios的请求拦截器和响应拦截器封装到main函数里面或者单独封装axios工具然后全局引用

登陆组件

<template>
<div>
  <h1>SSO单点登陆测试</h1>
    账户:<input type="text" v-model="account"> <br>
    密码:<input type="text" v-model="password"> <br>
    <el-button @click="login"  >登陆</el-button>
</div>
</template>

<script>
export default {
  name: "LoginTest",
  data(){
    return{
      url:' http://localhost:8086',
      account:'',
      password:''
    }
  },
  methods:{
    login(){
      this.$axios({
        headers:{},
        url:this.url+"/login",
        method:'post',
        params:{account:this.account,password:this.password},
        data:{}
      }).then(re=>{
        console.log(re.data.data)
        sessionStorage.setItem("token",JSON.stringify(re.data.data))
        this.$router.push("/TestRequest")
      })
    }
  },
  destroyed() {

  }
}
</script>

<style scoped>

</style>

测试组件

<template>
<h1>
  <el-button @click="test">拿到数据</el-button>
</h1>
</template>

<script>
export default {
  name: "TestRequest",
  data(){
    return{
      url:' http://localhost:8086',
    }
  },
  methods:{
    test(){
      var token = sessionStorage.getItem("token");
      //token=JSON.parse(token)
      this.$axios({
        headers:{token:token},
        url:this.url+"/test",
        method:'get',
      }).then(re=>{
        this.$message.success(re.data)
      }).catch(re=>{
        if(re.request.status===401){
          alert("请先登陆")
        }else if(re.request.status===520){
          alert("您的账号已在别处登陆,请重新登陆")
          this.$router.push("/LoginTest")
        }
      })
    }
  }
}
</script>

<style scoped>

</style>

大家如果有别的解决方案,欢迎评论区留言,想要源码请私聊