Springboot+vue+redis实现一个账户只能在一处登录

前言:只是类似QQ,这里实现的效果是,后登录的会把前登录界面挤出,但需要前登录者发送请求,才能对前登录者进行退登处理,跟QQ及时性还是没法比。这里我配合了一套商品的增删改查使用,大家只需关注单点登录是如何实现的,代码无需照搬,理解后能写出更优的代码

搭建springboot项目:

1.选择jdk1.8,我的版本(2.3.7)

2.然后确保在pom.xml导入了以下依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

3.把application.properties改成application.yml后在文件中配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/goods?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT-8&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  jpa:
    database: mysql
    show-sql: true
  redis:
    host: 175.178.235.245
    port: 2468
    password: zxyzjc323621
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s
    database: 2

server:
  port: 8087
  servlet:
    context-path: /goods

项目结构:

 我们只需重点关注与User和Login有关的

建类建包:

1.建实体类,我们用密码和邮箱登录:

User.java

@Data
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String password;

    private String email;
}

2.UserController.java的登录方法,这里我使用了统一返回接口,也可使用其他:

@RestController
@CrossOrigin
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private UserService service;

    @PostMapping
    public Result loginUser(@RequestBody User user){
        return service.getUserReplay(user);
    }
}

2.0:看自身情况参考,Result统一接口:

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

3.1.UserService:

public interface UserService {

    Result getUserReplay(User user);
}

3.2.UserServiceImpl(重点关注):

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository repository;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //登录方法
    @Override
    public Result getUserReplay(User user) {
        User byEmailAndPassword = repository.findByEmailAndPassword(user.getEmail(), user.getPassword());
        if (byEmailAndPassword==null){
            return Result.fail("邮箱或密码错误");
        }
        //加密密码
        String key = MD5.create().digestHex(user.getPassword());
        //生成token随机数
        String token = UUID.randomUUID().toString();
        
        //把token和pwd封装为一个实体类一起存入redis(也可以只存token)
        TokenUtil tokenUtil =new TokenUtil();
        tokenUtil.setPwd(key);
        tokenUtil.setToken(token);

        //密码为key存入redis
        stringRedisTemplate.opsForValue().set(key,token);
        //返回正确(把数据返回给前端,前端存入session)
        return Result.ok(tokenUtil);
    }
}

3.3.上面代码中的TokenUtil:

@Data
public class TokenUtil {

    private String pwd;

    private String token;
}

4.用户接口的登录方法UserRepository :

@Repository
public interface UserRepository extends JpaRepository<User,Integer> {

    User findByEmailAndPassword(String email,String password);
}

 5.LoginInterceptor (重点关注):

当收到前端发的请求头后,拿到请求头的token,如果为null,说明没有登录,返回错误码401

如果不为空,并且拿到的token和redis里存的token一致,说明是同一个页面的请求,也可以说是后登录者的请求,直接放行。

不一致就说明这个用户已经有两处地方登录了,因为在service层已经把旧token用新token覆盖了,

就可以把这个请求作为过期请求来看,直接返回错误码402,前端直接让前登录者的页面退出即可

public class LoginInterceptor implements HandlerInterceptor {

    private final 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();
       
        if(token==null){
            //没有登录
            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;
        }
        
        response.setStatus(402);
        return false;
    }
}

6.最后配置MvcConfig,把刚写的LoginInterceptor配进去,除了登录请求,其他请求都会经过LoginInterceptor 进行验证 :

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

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

    }
}

后端代码大致就是这些

前端vue最主要的就是配置一个全局请求,响应拦截器,可直接写进main.js

main.js:

//全局请求拦截器(统一发送请求头,把session中的token发送)
axios.interceptors.request.use(config=>{
  let token = sessionStorage.getItem("token");
  if(token){
    config.headers["token"]=token;
  }
  return config;
})

//全局响应拦截器(处理后端返回的错误码)
axios.interceptors.response.use(
    re=>{
      return re
    },
    err=>{
      if(err.response.status===401){
        return Promise.reject("没有登录")
      }
      if(err.response.status===520){
        //跳转到login页面
        router.push("/Login")
        alert("您的账号在别处登录!")
      }
      return Promise.reject("服务器异常")
    }
)

然后就是在登录请求成功后,把后端返回的token存入session

sessionStorage.setItem("token",JSON.stringify(re.data.data))

后面写页面,跟以前一模一样,可以写个小项目练习练习,如果有什么遗漏可以私信我OvO

 


版权声明:本文为xiaoxiao1927原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。