Spring Session Redis实现简单的SSO

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

问题

之前实现使用过一次Spring Session集中会话管理《Spring Session和拦截器集成做简单Restful接口登录超时验证》

现在需要在这个集中会话管理的基础上面加上SSO单点登录即可。

思路

会话拦截器仍旧负责会话的登录状态检查。只是这次在登录的时候需要检查当前用户的所有会话然后把其他会话统统删除只保留当前登录成功的有效会话。这样就实现了SSO。有效会话的记录仍旧保留在redis中。

步骤

Maven依赖

pom.xml

<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>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>

Spring配置

application.yml

spring:
  session:
    redis:
      flush-mode: on_save
      namespace: xxx:session
    timeout: P30D
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: xxxxx
      database: 0

这里主要是配置怎么连redis以及会话过期时间为30天。

会话认证拦截器实现

SessionTimeOutInterceptor.java


import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.c2.comm.Result;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.PrintWriter;

@Component
public class SessionTimeOutInterceptor implements HandlerInterceptor {

    public static final String USER_AUTH_KEY = "user";

    @Resource
    private ObjectMapper mapper;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        Object userId = session.getAttribute(USER_AUTH_KEY);
        if (userId != null){
            return HandlerInterceptor.super.preHandle(request, response, handler);
        } else {
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            PrintWriter out = response.getWriter();
            out.print(mapper.writeValueAsString(Result.builder().code("200").message("请先登录").build()));
            out.flush();
            return false;
        }

    }
}

这里只要检查到session中存在user的属性就表示这个session是登录成功的。

登录实现

UserController.java



import com.xxx.c2.services.UserService;
import com.xxx.c2.vo.UserReq;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @Resource
    private UserService userService;
    @GetMapping("/index")
    public String index() {
        return "Greetings from Spring Boot!";
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(HttpSession session, @RequestBody UserReq userReq) {
        return userService.login(session, userReq);
    }

    @PostMapping("/logout")
    public ResponseEntity<?> logout(HttpSession session) {
        session.invalidate();
        return ResponseEntity.ok().build();
    }
}

这里主要体现了退出时使其当前会话失效。具体登录处理都在userService.login(session, userReq);中接下来看看具体是如何实现。
定义如下UserService接口

UserService.java

public interface UserService {

    void ssoLogin(HttpSession session, User user);

    /**
     * 登录
     */
    ResponseEntity<?> login(HttpSession session, UserReq userReq);
}

接口实现如下

UserServiceImp.java

import com.xxx.c2.comm.Result;
import com.xxx.c2.interceptor.SessionTimeOutInterceptor;
import com.xxx.c2.model.User;
import com.xxx.c2.services.UserService;
import com.xxx.c2.vo.UserReq;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpSession;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

@Service
public class UserServiceImp implements UserService {
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private SessionRepository sessionRepository;

    @Override
    public void ssoLogin(HttpSession session, User user) {
        String key = "user";
        boolean add = true;
        String currentSessionId = session.getId();
        String userId = user.getId().toString();
        HashOperations ops = redisTemplate.opsForHash();
        List<String> sessionIdList = new ArrayList<>();

        if (ops.hasKey(key, userId)){
            sessionIdList = (List<String>) ops.get(key, userId);
            if (!CollectionUtils.isEmpty(sessionIdList)) {
                Iterator<String> iterable = sessionIdList.iterator();
                while (iterable.hasNext()) {
                    String sessionId = iterable.next();
                    if (currentSessionId.equals(sessionId)){
                        add = false;
                    } else {
                        sessionRepository.deleteById(sessionId);
                        iterable.remove();
                    }
                }
                ops.put(key, userId, sessionIdList);

            }
        }
        if (add) {
            sessionIdList.add(currentSessionId);
            ops.put(key, userId, sessionIdList);
        }
    }

    @Override
    public ResponseEntity<?> login(HttpSession session, UserReq userReq) {
        // TODO 判断是否登录成功
        this.ssoLogin(session, User.builder().id(Long.parseLong("1")).build());
        session.setAttribute(SessionTimeOutInterceptor.USER_AUTH_KEY, "x");
        return ResponseEntity.ok(Result.builder().message("登录成功").build());
    }
}

这里主要就是sso关键实现逻辑代码根据用户id查询是否存在当前用户的有效会话如果不存在有效会话就记录当前会话如果存在就先踢掉老会话记录新会话即可。接下来还需要配置一下redis和配置拦截器。

redis配置

RedisConfig.java

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(objectMapper, Object.class);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

这里不要使用Spring默认的ObjectMapper实例因为这里会对ObjectMapper进行个性化修改。

拦截器配置

WebMvcConfig.java

import com.xxx.SessionTimeOutInterceptor;
import jakarta.annotation.Resource;
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 WebMvcConfig implements WebMvcConfigurer {
    @Resource
    private SessionTimeOutInterceptor sessionTimeOutInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(sessionTimeOutInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

这里主要配置会话认证拦截器对所有请求生效除了登录接口。

总结

到这里Spring Session的SSO简单实现就这样了。这个部分还缺少用户认证的实现也没有使用DB来记录历史会话使用的redis进行简单记录。

参考

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Springredis

“Spring Session Redis实现简单的SSO” 的相关文章