实现的效果就是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整个项目框架 ,新建项目过程就省略了
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>
大家如果有别的解决方案,欢迎评论区留言,想要源码请私聊