前言:只是类似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
