## 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)。