SpringSecurity3.2.10 + SpringBoot2.1.11+ConcurrentSession(分散セッション)+redis

32584 ワード

注意:SpringBoot 2.1.11より高いバージョンのSpringSecurityを組み合わせるべきです. 
1、maven依存を導入する
本プロジェクトで使用するSpringBoot 2.1.11では、付属のspring-boot-starter-securityバージョンを導入して5.1.7となっていますが、古いプロジェクトで古いバージョンと互換性が必要なため、低バージョンの3つのSpringSecurityパッケージを使用しています.
     <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-coreartifactId>
            <version>3.2.10.RELEASEversion> 
        dependency>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-webartifactId>
            <version>3.2.10.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-configartifactId>
            <version>3.2.10.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
            
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.securitygroupId>
                    <artifactId>spring-security-coreartifactId>  
                exclusion>
                <exclusion>
                    <groupId>org.springframework.securitygroupId>
                    <artifactId>spring-security-webartifactId>
                exclusion>
                <exclusion>
                    <groupId>org.springframework.securitygroupId>
                    <artifactId>spring-security-configartifactId>
                exclusion>
            exclusions>
        dependency>

マルチノード分散配置を適用する場合、SpringSecurity自体は分散セッションを制御できないため、サードパーティ製メディアの介入が必要です.ここでredisを選択し、redis依存をインポートします.
        <dependency>
             <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
            <version>1.5.22.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframework.sessiongroupId>
            <artifactId>spring-session-data-redisartifactId>  
        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 {

    private static final Logger logger = LoggerFactory.getLogger(MySessionRegistryImpl.class);

    //            ,  Spring     : org.springframework.security.core.session.SessionRegistryImpl
     
    /**  */
    private final ConcurrentMap> principals = new ConcurrentHashMap>();
    /**  */
    private final Map sessionIds = new ConcurrentHashMap();

    @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 getAllPrincipals() {
        return new ArrayList(principals.keySet());
    }

    @Override
    public List getAllSessions(Object principal, boolean includeExpiredSessions) {
        final Set sessionsUsedByPrincipal = principals.get(principal);

        if (sessionsUsedByPrincipal == null) {
            return Collections.emptyList();
        }

        List list = new ArrayList(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、SecurityContextHolderではなく、Sessionからlogin userを取得する.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.