SpringBoot前后台分离项目使用Redis实现SSO单点登录

## SpringBoot前后台分离项目使用Redis实现SSO单点登录

采用web集群进行的服务配置,可以得到较好的处理性能,同时也可以保证高可用的机制,但是有一个最关键性的问题就是在于Session共享与分配,从早期的开发来讲,Session共享是在tomcat中配置的(Tomcat提供了一个扩展的支持)。

### 一、单点登录(SSO)

#### 1、单点登录(SSO)是什么

单点登录(SSO,Single Sign On),是在企业内部多个应用系统(如考勤系统、财务系统、人事系统等)场景下,用户只需要登录一次,就可以访问多个应用系统。

同理用户只需注销一次,就可以从多个应用系统退出登录。

简单来说就是,一次登录,全部登录!一次注销,全部注销!!

#### 2、单点登录的实现原理

单点登录的实现原理说明如下:

1. 用户首次访问系统A时,需要进行登录。
2. 系统A带着用户登录信息重定向给认证系统。
3. 认证系统验证用户登录信息。
4. 验证通过后,返回一个token(Tip:token类似一种内部的通行证,包含了用户身份信息、登录状态和过期时间,在各个系统间共享。)
5. 认证系统带着token重定向给系统A,得知用户是已登录状态。
6. 系统A向用户返回请求的资源。
7. 用户访问系统B时,需要进行登录。
8. 系统B通过共享的token,得知用户是已登录状态。
9. 系统B向用户返回请求的资源。

![33](Redis-3.assets/33.png)

Token是有时效性的,如果用户长时间没有操作,token将会过期。

Token过期后用户再次访问系统A、系统B时,登录状态已失效,需要重新登录。

对于注销场景,与上述流程类似。

用户主动从系统A注销时,系统A调用认证系统,清除token。

此时用户再访问系统A、系统B时,通过共享的token得知用户是已注销状态,需要重新登录。


### 二、Springboot的拦截器和过滤器的区别

Spring Boot中的拦截器(Interceptor)和过滤器(Filter)是用来在请求处理之前或之后进行一些特殊处理的工具。它们的主要区别如下:

- 实现机制不同:Filter 是基于 Servlet 规范实现的,而 Interceptor 是基于 Spring MVC 框架实现的。

- 拦截范围不同:Filter 可以拦截所有的请求,而 Interceptor 只能拦截由 DispatcherServlet 处理的请求。

- 拦截顺序不同:Filter 在 Interceptor 之前调用,并且在所有的拦截器(Interceptor)之后调用。

- 拦截方法不同:Filter 实现了 javax.servlet.Filter 接口,其中有两个主要方法:init() 和 destroy()。Interceptor 实现了 Spring 的 HandlerInterceptor 接口,其中有三个主要方法:preHandle()、postHandle() 和afterCompletion()。

**过滤器示例代码**:

~~~java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;


public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化代码
    }
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        // 在请求处理之前可以进行一些处理
        System.out.println("Filter before processing the request: " + req.getRequestURI());
        chain.doFilter(request, response); // 传递给下一个过滤器或servlet处理
        // 在请求处理之后可以进行一些处理
        System.out.println("Filter after processing the request: " + req.getRequestURI());
    }
 
    @Override
    public void destroy() {
        // 销毁代码
    }
}
~~~

**注册过滤器**

注册过滤器,可以使用`@WebFilter`注解或者在配置类中用`@Bean`注解注册。

~~~java
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {
        FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new MyFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(1);
        return registrationBean;
    }
}
~~~



### 三、SpringBoot使用redis实现单点登录

在Spring Boot中使用拦截器来用户是否登录:

1、登录状态可以保存在redis中

2、创建一个自定义拦截器,在其中检查请求中的登录凭证是否有效。

以下是一个简单的示例:

创建名称为sso-demo的springboot项目

#### 1、pom.xml依赖配置

~~~xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version> <!-- 请使用最新的版本号 -->
</dependency>
~~~

#### 2、redis连接池

使用lettuce的连接池,springmvc需要导入commons-pool2依赖,但springboot自动集成,无需再次导入

~~~xml
<!-- redis连接池lettuce客户端-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
~~~

application.yml的配置

~~~yml
spring:
  data:
    redis:
      database: 0
      host: 127.0.0.1
      port: 6379
      password: 123456
      lettuce:
        pool:
          #最大连接数
          max-active: 8
          #最大阻塞等待时间(负数表示没限制)
          max-wait: -1
          #最大空闲
          max-idle: 8
          #最小空闲
          min-idle: 0
      #连接超时时间
      timeout: 10000
~~~

#### 3、创建实体类User,创建LoginUser的Vo类

User类

~~~java
package cn.hxzy.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author mengshujun
 * @create 2024/5/5 20:52
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    private Long userId;
    private String userName;
    private String userNo;
    private String userPwd;
}

~~~

LoginUser实体类

~~~java
package cn.hxzy.domain.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author mengshujun
 * @create 2024/5/5 20:58
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser  implements Serializable {
    private String userNo;
    private String userPwd;
}

~~~

**引入RedisConfig配置类**

~~~java
package cn.hxzy.config;

/**
 * @author mengshujun
 * @create 2024/5/5 21:06
 */
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean("redis")
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        //自定义Jackson序列化配置
        Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jsonRedisSerializer.setObjectMapper(objectMapper);

        //key使用String的序列化方式
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也是用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value的key使用jackson的序列化方式
        template.setValueSerializer(jsonRedisSerializer);
        //hash的value也是用jackson的序列化方式
        template.setHashValueSerializer(jsonRedisSerializer);
        template.afterPropertiesSet();
        return template;

    }

}
~~~

登录的业务实现

~~~java
package cn.hxzy.controller;

import cn.hxzy.domain.User;
import cn.hxzy.domain.vo.LoginUserVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    //登录业务
    @PostMapping("/login")
    public String login(@RequestBody LoginUserVo loginUserVo, HttpServletResponse response) {
        //连接数据库
        if (loginUserVo.getUserNo().equals("admin") && loginUserVo.getUserPwd().equals("123")) {
            //1.保存状态在redis
            //1.1 用户信息
            User user = new User();
            user.setUserId(1001L);
            user.setUserName("张三");
            user.setUserNo(loginUserVo.getUserNo());
            user.setUserPwd(loginUserVo.getUserPwd());

            //2.返回token
            //2.1 生成token令牌
            String token = UUID.randomUUID().toString().replace("-", "");
            //把用户信息保存在redis中,有效期为1小时,key改为token
            redisTemplate.opsForValue().set(token, user,3600, TimeUnit.SECONDS);
            //2.2 把令牌加入到响应头中
            response.setHeader("Authorization",token);
            return token;
        }
        return "fail";
    }
}

~~~





在控制器中实现拦截判断的方法

~~~java
package cn.hxzy.controller;

import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/news")
public class NewsController {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @GetMapping("/list")
    public String getNewsList(@RequestHeader("Authorization") String token) {
        //业务:
        //1.判断token是否为空,如果为空则不往下执行业务
        if (StringUtil.isNullOrEmpty(token)) {
            return "token为空";
        }
        //2. 在redis中去查看是否存在此用户
        Object o = redisTemplate.opsForValue().get(token);
        System.out.println("o的值:"+o);
        if (o == null) {
            return "登录过期,请重新登录";
        }
        return "资讯列表信息...";
    }
}

~~~



#### 4、创建一个拦截器类实现`HandlerInterceptor`接口

~~~java
package cn.hxzy.handler;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author mengshujun
 * @create 2024/5/5 20:43
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization"); // 假设token在请求头中
        if (token != null && redisTemplate.hasKey(token)) {
            // Token存在于Redis中,登录有效
            return true;
        }
        // Token无效或不存在,返回未登录
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return false;
    }
}

~~~



#### 5、在Spring Boot的配置类中注册拦截器

~~~java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class WebConfig implements WebMvcConfigurer {
 
    @Autowired
    private LoginInterceptor loginInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/login", "/error"); // 排除登录和错误处理路径
    }
}
~~~



#### 6、UserController登录和登出控制器

~~~java
package cn.hxzy.controller;

import cn.hxzy.domain.vo.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author mengshujun
 * @create 2024/5/5 20:51
 */
@RestController
public class UserController {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostMapping("/login")
    public String login(@RequestBody LoginUser user, HttpServletResponse response) {
        // 登录逻辑,验证用户名密码等,连接数据库等操作略
        if(user.getUserNo().equals("admin")&&user.getUserPwd().equals("123")){
            //此处可以做数据校验
            String token = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(token, user, 3600, TimeUnit.SECONDS);
            response.addHeader("Authorization", token);
            System.out.println(token);
            return "登录成功!";
        }
        return "登录失败!";


    }

    @PostMapping("/logout")
    public String logout(@RequestHeader("Authorization") String token) {
        redisTemplate.delete(token);
        return "登出成功!";
    }
}

~~~

在这个简化的例子中,我们是通过自定义的拦截器来处理登录,并使用Redis来存储用户的登录状态。用户登录后,生成一个唯一的

token,并将用户信息和token作为键值对存储在Redis中,过期时间可以设置。每次请求时,通过检查token来验证用户是否登录。登

出操作则是通过删除token对应的数据来实现登出。

### 四、Springboot的跨域配置

在Spring Boot中配置跨域可以通过实现`WebMvcConfigurer`接口,并覆盖`addCorsMappings`方法来完成。以下是一个配置示例:

~~~java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class CorsConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 允许跨域的路径
                .allowedOrigins("*") // 允许跨域请求的域名
                .allowedMethods("GET", "POST", "PUT", "DELETE","PATCH") // 允许的请求方法
                .allowedHeaders("*") // 允许的请求头
                .allowCredentials(true); // 是否允许证书(cookies)
    }
}
~~~

这段代码创建了一个配置类`CorsConfig`,实现了`WebMvcConfigurer`接口,并覆盖了`addCorsMappings`方法。在这个方法中,我们使用`registry.addMapping`指定了路由,并设置了允许所有来源的跨域请求(`allowedOrigins("*")`),同时还配置了允许的HTTP方法和请求头。最后,`allowCredentials`设置为`true`表示允许跨域请求包含认证信息(如cookies)。
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个用于快速开发Java应用程序的开源框架,Shiro是一个强大且易于使用的Java安全框架,Redis是一个开源的内存数据库。结合使用这些技术可以实现单点登录功能。 在Spring Boot使用Shiro来处理认证和授权,可以通过配置Shiro的Realm来实现用户的登录认证和权限控制。将用户的信息存储在Redis中,利用Redis的持久化特性来实现用户登录状态的共享和存储。 首先,在Spring Boot项目的配置文件中配置Redis的连接信息,以便连接到Redis数据库。 然后,创建一个自定义的Shiro的Realm,在其中重写认证和授权的方法。在认证方法中,将用户的登录信息存储到Redis中,以便其他服务可以进行验证。在授权方法中,根据用户的角色和权限进行相应的授权操作。 接着,在Spring Boot项目的配置类中配置Shiro的相关设置,包括Realm、Session管理器、Cookie管理器等。 最后,可以在Controller层中使用Shiro的注解来标记需要进行认证和授权的接口,以确保只有登录后且具备相应权限的用户才能访问这些接口。 总的来说,通过使用Spring Boot、Shiro和Redis的组合,可以实现单点登录的功能。用户在登录后,将登录信息存储到Redis中,其他服务可以通过验证Redis中的数据来判断用户的登录状态。同时,Shiro提供了强大的认证和授权功能,可以确保只有具备相应权限的用户才能访问受保护的接口。这些功能的具体实现可以通过深入研究Spring Boot、Shiro和Redis的源码来了解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值