zoukankan      html  css  js  c++  java
  • SpringSecurity3.2.10 + SpringBoot2.1.11 + ConcurrentSession(分布式会话)+ redis

    注意:SpringBoot2.1.11 应该搭配更高版本的SpringSecurity. 

    1、引入maven依赖

    本项目中用的SpringBoot2.1.11,引入自带的 spring-boot-starter-security 版本为 5.1.7,但是由于是老项目需要兼容旧版本,所以使用了低版本的 3 个SpringSecurity包:

         <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-core</artifactId>
                <version>3.2.10.RELEASE</version> 
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-web</artifactId>
                <version>3.2.10.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-config</artifactId>
                <version>3.2.10.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <!-- <version>1.5.20.RELEASE</version> -->
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.security</groupId>
                        <artifactId>spring-security-core</artifactId>  <!-- default version is 5.1.7 -->
                    </exclusion>
                    <exclusion>
                        <groupId>org.springframework.security</groupId>
                        <artifactId>spring-security-web</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.springframework.security</groupId>
                        <artifactId>spring-security-config</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>

    当应用多节点分布式部署的时候,SpringSecurity本身是不能控制分布式会话的,所以需要第三方介质的介入,这里选择 redis,导入redis依赖:

            <dependency>
                 <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>1.5.22.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.session</groupId>
                <artifactId>spring-session-data-redis</artifactId>  <!-- 注意,这个包必须引入,它可以支持SpringBoot的内置tomcat通过redis同步所有session -->
            </dependency>    

    2、自定义分布式会话控制类

    这里先贴代码,后面再说怎么注入到SpringBoot 和SpringSecurity

    import com.test.MyUser;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.ApplicationListener;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.security.core.session.SessionDestroyedEvent;
    import org.springframework.security.core.session.SessionInformation;
    import org.springframework.security.core.session.SessionRegistry;
    import org.springframework.stereotype.Component;
    
    import java.io.*;
    import java.util.*;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    import java.util.concurrent.TimeUnit;
    
    @Component
    public class MySessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> {
    
        private static final Logger logger = LoggerFactory.getLogger(MySessionRegistryImpl.class);
    
        // 以下两个集合的代码作用,参考Spring原生实现类: org.springframework.security.core.session.SessionRegistryImpl
         
        /** <principal:Object,SessionIdSet> */
        private final ConcurrentMap<Object, Set<String>> principals = new ConcurrentHashMap<Object,Set<String>>();
        /** <sessionId:Object,SessionInformation> */
        private final Map<String, SessionInformation> sessionIds = new ConcurrentHashMap<String, SessionInformation>();
    
        @Autowired
        @Qualifier("redisTemplate")
        RedisTemplate redisTemplate;
    
        @Value("${session.timeout.minutes}")
        private Integer sessionTimeoutMinutes;
    
        @Override
        public void registerNewSession(String sessionId, Object principal) {
            MyUser myUser = (MyUser) principal;
            try {
    
                // put login user to local collection
                sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
                logger.info("put login user to local collection success, username={}, sessionId={}", myUser.getUsername(), sessionId);
    
                // put login user to redis
                byte[] bytes = myUserToBytes(myUser);
                redisTemplate.opsForValue().set(sessionId, bytes, sessionTimeoutMinutes, TimeUnit.MINUTES);
                myUser.toString().getBytes();
                logger.info("put login user to redis success, username={}, sessionId={}, bytes.length={}", myUser.getUsername(), sessionId, bytes.length);
    
            } catch (IOException e) {
                logger.error("register new sessionId[{}] to redis is fail, username={}", sessionId, myUser.getUsername(), e);
            }
        }
    
        /**
         * 这里参考kafka的优秀思想,存储数据都使用byte[]数组 
         * object to byte[]
         */
        public byte[] myUserToBytes(MyUser myUser) throws IOException {
            try(
                    ByteArrayOutputStream out = new ByteArrayOutputStream();
                    ObjectOutputStream sOut = new ObjectOutputStream(out);
            ){
                sOut.writeObject(myUser);
                sOut.flush();
                byte[] bytes = out.toByteArray();
                return bytes;
            }
        }
    
        /**
         * byte[] to object
         */
        public MyUser bytesToMyUser(byte[] bytes) throws IOException, ClassNotFoundException {
            try(
                    ByteArrayInputStream in = new ByteArrayInputStream(bytes);
                    ObjectInputStream sIn = new ObjectInputStream(in);
            ){
                return (MyUser) sIn.readObject();
            }
        }
    
        @Override
        public SessionInformation getSessionInformation(String sessionId) {
    
            // get login user from local collection , 优先从本地集合取值
            SessionInformation sessionInformation = sessionIds.get(sessionId);
            if(null != sessionInformation){
                MyUser myUser = (MyUser) sessionInformation.getPrincipal();
                logger.info("get login user from local collection by sessionId success, username={}, sessionId={}", myUser.getUsername(), sessionId);
    
                return sessionInformation;
            }
    
            // get login user from redis
            Object sessionValue = redisTemplate.opsForValue().get(sessionId);
            if(null == sessionValue){
                logger.info("can't find login user from redis by sessionId[{}]", sessionId);
                return null;
            }
    
            try {
                byte[] bytes = (byte[]) sessionValue;
                logger.info("get login user from redis by sessionId success, bytes.length={}", bytes.length);
    
                MyUser myUser = bytesToMyUser(bytes);
                logger.info("get login user from redis by sessionId success, username={}, sessionId={}, bytes.length={}", myUser.getUsername(), sessionId, bytes.length);
    
                SessionInformation sessionInfo = new SessionInformation(myUser, sessionId, new Date());
                return sessionInfo;
    
            } catch (ClassNotFoundException | IOException e) {
                logger.error("get myUser from redis by session[{}] is fail", sessionId, e);
            }
            return null;
        }
    
        @Override
        public void removeSessionInformation(String sessionId) {
            boolean isDelete = redisTemplate.delete(sessionId);
            logger.info("remove sessionId from redis is sucess. isDelete={}", isDelete);
        }
    
        @Override
        public List<Object> getAllPrincipals() {
            return new ArrayList<Object>(principals.keySet());
        }
    
        @Override
        public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
            final Set<String> sessionsUsedByPrincipal = principals.get(principal);
    
            if (sessionsUsedByPrincipal == null) {
                return Collections.emptyList();
            }
    
            List<SessionInformation> list = new ArrayList<SessionInformation>(sessionsUsedByPrincipal.size());
    
            for (String sessionId : sessionsUsedByPrincipal) {
                SessionInformation sessionInformation = getSessionInformation(sessionId);
    
                if (sessionInformation == null) {
                    continue;
                }
    
                if (includeExpiredSessions || !sessionInformation.isExpired()) {
                    list.add(sessionInformation);
                }
            }
    
            return list;
        }
    
        @Override
        public void onApplicationEvent(SessionDestroyedEvent event) {
            String sessionId = event.getId();
            removeSessionInformation(sessionId);
        }
    
        @Override
        public void refreshLastRequest(String sessionId) {
            SessionInformation info = getSessionInformation(sessionId);
    
            if (info != null) {
                info.refreshLastRequest();
            }
        }
    }

    3、MySessionRegistryImpl 以SpringBoot的方式注入SpringSecurity

     这部分由于我使用了低版本的SpringSecurity,所以参考spring官网地址是: https://docs.spring.io/spring-security/site/docs/3.1.x/reference/session-mgmt.html 《12. Session Management Prev Part III. Web Application Security》

    SpringSecurity其他高版本的官方文档地址参考: https://docs.spring.io/spring-security/site/docs/

        @Autowired
        MySessionRegistryImpl mySessionRegistryImpl; // 自定义分布式会话控制类
    
        @Bean("concurrentSessionControlStrategy")
        public org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy getConcurrentSessionControlStrategy(){
            return new ConcurrentSessionControlStrategy(mySessionRegistryImpl);  // 注意,ConcurrentSessionControlStrategy 这个类在SpringSecurity 3.2.10 以上已过时
        }
    
        @Bean("authenticationFilter")
        public MyAuthenticationFilter getMyAuthenticationFilter(
                @Qualifier("authenticationManager") ProviderManager authenticationManager
                ,@Qualifier("successHandler") MyAuthenticationSuccessHandler successHandler
                ,@Qualifier("failureHandler") MyAuthenticationFailureHandler failureHandler
                ,@Qualifier("concurrentSessionControlStrategy") ConcurrentSessionControlStrategy concurrentSessionControlStrategy
        ){
            MyAuthenticationFilter filter = new MyAuthenticationFilter();
            filter.setAuthenticationManager(authenticationManager); // spring原生类 org.springframework.security.authentication.ProviderManager
            filter.setAuthenticationSuccessHandler(successHandler); // 自定义类需 extends org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
            filter.setAuthenticationFailureHandler(failureHandler); // 自定义类需 implements org.springframework.security.web.authentication.AuthenticationFailureHandler
            filter.setFilterProcessesUrl("/login/loginUser.xhtml");
            filter.setAllowSessionCreation(false);
    
            // SpringSecurity + SpringBoot + ConcurrentSession + redis
            // refer doc from https://docs.spring.io/spring-security/site/docs/3.1.x/reference/session-mgmt.html
            filter.setSessionAuthenticationStrategy(concurrentSessionControlStrategy); // 重要!!!这里把自定义分布式session会话控制类加入SpringSecurity
    
            return filter;
        }
    
    
        @Bean("securityFilter")
        public FilterChainProxy getFilterChainProxy(
                @Qualifier("securityContextPersistenceFilter") SecurityContextPersistenceFilter securityContextPersistenceFilter
                ,@Qualifier("logoutFilter") LogoutFilter logoutFilter
                ,@Qualifier("authenticationFilter") MyAuthenticationFilter authenticationFilter
                ,@Qualifier("securityContextHolderAwareRequestFilter") SecurityContextHolderAwareRequestFilter securityContextHolderAwareRequestFilter
                ,@Qualifier("exceptionTranslationFilter") ExceptionTranslationFilter exceptionTranslationFilter
                ,@Qualifier("concurrentSessionFilter") ConcurrentSessionFilter concurrentSessionFilter
        ){
            PrefixUriRequestMatcher requestMatcher = new PrefixUriRequestMatcher();
            requestMatcher.setPrefixUris("/admin-portal/,/xxxx/");
    
            SecurityFilterChain filterChain = new DefaultSecurityFilterChain(
                    requestMatcher
                    ,securityContextPersistenceFilter // spring原生类 org.springframework.security.web.context.SecurityContextPersistenceFilter
                    ,logoutFilter                     // spring原生类 org.springframework.security.web.authentication.logout.LogoutFilter
                    ,authenticationFilter             // 自定义类需 extends org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter, 可选择性 implements org.springframework.context.ApplicationContextAware
                    ,securityContextHolderAwareRequestFilter  // spring原生类 org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
                    ,exceptionTranslationFilter    // spring原生类 org.springframework.security.web.access.ExceptionTranslationFilter
                    ,concurrentSessionFilter       // 重要!!!只有这里加入自定义分布式session会话控制类,SpringSecurity才会执行这个Filter
            );
    
            FilterChainProxy proxy = new FilterChainProxy(filterChain);
            return proxy;
        }

     4、从Session中获取 login user,而不是从 SecurityContextHolder.getContext().getAuthentication() 这里获取登陆用户

    感谢这位老兄: http://www.manongjc.com/article/97630.html 《SecurityContextHolder.getContext().getAuthentication()为null的情况》

    原理描述:

    因为无论你给SpringSecurity定义了多少个Filter过滤链,最后SpringSecurity都会执行 SecurityContextHolder.clearContext(); 而把SecurityContextHolder清空,所以会得到 SecurityContextHolder.getContext().getAuthentication() = null 的情况。

    所以如果想获得当前用户,必须在spring security过滤器执行中执行获取login user的办法。

    我的解决方案:

    我这里选择在MySessionRegistryImpl 自定义分布式会话控制器里存储login user到redis。

    再定义一个后台登陆过滤器 LogonFilter,在到达任何Controller请求之前,获取redis里的login user 放到session里,而spring容器因为引入了spring-session-data-redis依赖,已经通过redis将session同步到连接到此redis的所有SpringBoot节点。

    5、获取request的小窍门

     org.springframework.web.context.request.ServletRequestAttributes holder = (ServletRequestAttributes) org.springframework.web.context.request.RequestContextHolder;
     javax.servlet.http.HttpServletRequest request = holder.getRequest();
     javax.servlet.http.HttpSession session = request.getSession();

    end.

  • 相关阅读:
    oracle 自动备份
    oracle 常用操作语句
    数据库创建及使用注意事项
    oracle 导入 导出 备份
    http://blog.sina.com.cn/s/blog_5fc8b3810100iw9n.html
    利用普通工具编译的第一个Servlet
    对java:comp/env的研究(转)
    MyEclipse配置tomcat、jdk和发布第一个web项目
    构建 SSH 框架(转)
    Java Project和Web Project
  • 原文地址:https://www.cnblogs.com/zhuwenjoyce/p/13287038.html
Copyright © 2011-2022 走看看