7.Spring security中的会话管理

文章目录

    • *会话管理*
      • *7.1会话简介*
      • *7.2会话并发管理*
        • *7.2.1实战*
        • *7.2.2原理分析*
          • *`SessionInformation`*
          • *`SessionRegistry`*
          • *`SessionAuthenticationStrategy`*
            • *`ConcurrentSessionControlAuthenticationStrategy`*
            • *`RegisterSessionAuthenticationStrategy`*
            • *`CompositeSessionAuthenticationStrategy`*
          • *`SessionManagementFilter`*
          • *`ConcurrentSessionFilter`*
          • *Session创建时机*
          • *`SessionManagementConfigurer`*
          • *`AbstractAuthenticationFilterConfigurer`*
      • *7.3会话固定攻击与防御*
        • *7.3.1什么是会话固定攻击*
        • *7.3.2会话固定攻击防御策略*
      • *7.4Session共享*
        • *7.4.1集群会话方案*
        • *7.4.2实战*

会话管理

7.1会话简介

当用户通过浏览器登录成功之后,用户和系统之间就会保持一个会话(session),通过这个会话,系统可以确定出访问用户的身份。Spring security中和会话相关的功能由SessionManagementFilterSessionAuthenticationStrategy接口的组合来处理,过滤器委托该接口对会话进行处理,比较典型的用法有防止会话固定攻击、配置会话并发数等。

7.2会话并发管理

7.2.1实战
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("javaboy")
                .password("{noop}123")
                .roles("admin");
    }

     @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                // 开启会话配置
                .sessionManagement()
                // 设置会话并发数为1,类似于挤下线的效果
                .maximumSessions(1)
                // 自定义会话销毁后的行为,重定向到/login页面
                // .expiredUrl("/login")
                // 如果是前后端分离的项目,就不需要页面跳转了,直接返回一段JSON提示即可
                .expiredSessionStrategy(event -> {
                    HttpServletResponse response = event.getResponse();
                    response.setContentType("application/json;charset=utf-8");
                    Map<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已经失效, 请重新登录");
                    response.getWriter().print(new ObjectMapper().writeValueAsString(result));
                    response.flushBuffer();
                })
                // 禁止后来者无法使用相同的用户登录,直到当前用户主动注销登录
                .maxSessionsPreventsLogin(true);
    }

    /**
     * 提供HttpSessionEventPublisher实例。Spring security中通过一个Map集合来维护当前的HttpSession记录,进而实现
     * 会话的并发管理。当用户登录成功时,就向集合中添加一条HttpSession记录;当会话销毁时,就从集合中移除一条HttpSession
     * 记录。HttpSessionEventPublisher实现了HttpSessionListener接口,可以监听到HttpSession的创建和销毁事件,并将
     * HttpSession的创建/销毁事件发布出去,这样,当有HttpSession销毁时,spring security就可以感知到该事件了
     */
    @Bean
    HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}
7.2.2原理分析
SessionInformation

主要用作spring security框架内的会话记录:

public class SessionInformation implements Serializable {
    // 最近一次请求的时间
    private Date lastRequest;

    // 会话对应的主体(用户)
    private final Object principal;

    // 会话id
    private final String sessionId;

    // 会话是否过期
    private boolean expired = false;

    // 更新最近一次请求的时间
    public void refreshLastRequest() {
        this.lastRequest = new Date();
    }
    // 省略getter/setter
}
SessionRegistry

主要用来维护SessionInformation实例。该接口只有一个实现类SessionRegistryImpl

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {
    // ,保存当前登录主体(用户)和sessionId之间的关系,
    // 由于用对象作为key,因此自定义用户类时需要重写equals和hashCode方法
    private final ConcurrentMap<Object, Set<String>> principals;

    // ,保存sessionId和SessionInformation之间的关系
    private final Map<String, SessionInformation> sessionIds;

    public SessionRegistryImpl() {
        this.principals = new ConcurrentHashMap<>();
        this.sessionIds = new ConcurrentHashMap<>();
    }

    public SessionRegistryImpl(ConcurrentMap<Object, Set<String>> principals,
            Map<String, SessionInformation> sessionIds) {
        this.principals = principals;
        this.sessionIds = sessionIds;
    }

    // 重写ApplicationListener接口中的方法,接收HttpSession相关的事件
    @Override
    public void onApplicationEvent(AbstractSessionEvent event) {
        // 如果是SessionDestroyedEvent事件,则移除掉HttpSession的记录
        if (event instanceof SessionDestroyedEvent) {
            SessionDestroyedEvent sessionDestroyedEvent = (SessionDestroyedEvent) event;
            String sessionId = sessionDestroyedEvent.getId();
            removeSessionInformation(sessionId);
        }
        // 如果是SessionIdChangedEvent事件,则更新HttpSession的记录
        else if (event instanceof SessionIdChangedEvent) {
            SessionIdChangedEvent sessionIdChangedEvent = (SessionIdChangedEvent) event;
            String oldSessionId = sessionIdChangedEvent.getOldSessionId();
            if (this.sessionIds.containsKey(oldSessionId)) {
                Object principal = this.sessionIds.get(oldSessionId).getPrincipal();
                removeSessionInformation(oldSessionId);
                registerNewSession(sessionIdChangedEvent.getNewSessionId(), principal);
            }
        }
    }

    // 返回所有的用户登录对象
    @Override
    public List<Object> getAllPrincipals() {
        return new ArrayList<>(this.principals.keySet());
    }

    // 返回某一个用户所对应的所有SessionInformation,第二个参数表示是否包含已经过期的session
    @Override
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        Set<String> sessionsUsedByPrincipal = this.principals.get(principal);
        if (sessionsUsedByPrincipal == null) {
            return Collections.emptyList();
        }
        List<SessionInformation> 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;
    }

    // 根据sessionId获取SessionInformation
    @Override
    public SessionInformation getSessionInformation(String sessionId) {
        return this.sessionIds.get(sessionId);
    }

    @Override
    public void refreshLastRequest(String sessionId) {
        SessionInformation info = getSessionInformation(sessionId);
        if (info != null) {
            info.refreshLastRequest();
        }
    }

    // 会话保存
    @Override
    public void registerNewSession(String sessionId, Object principal) {
        // 如果sessionId存在,则先将其移除
        if (getSessionInformation(sessionId) != null) {
            removeSessionInformation(sessionId);
        }

        this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
        // 第一个参数是当前登录主体,第二个参数则进行计算。如果当前登录主体在principals中已经有对应的value,
        // 则在value的基础上继续添加一个sessionId;如果没有对应的value,则新建一个sessionsUsedByPrincipal对象,
        // 然后再将sessionId添加进去
        this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
            if (sessionsUsedByPrincipal == null) {
                sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
            }
            sessionsUsedByPrincipal.add(sessionId);
            return sessionsUsedByPrincipal;
        });
    }

    // 会话移除
    @Override
    public void removeSessionInformation(String sessionId) {
        SessionInformation info = getSessionInformation(sessionId);
        if (info == null) {
            return;
        }

        this.sessionIds.remove(sessionId);
        // 移除value中对应的sessionId
        this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
            sessionsUsedByPrincipal.remove(sessionId);
            if (sessionsUsedByPrincipal.isEmpty()) {
                // No need to keep object in principals Map anymore
                sessionsUsedByPrincipal = null;
            }
            return sessionsUsedByPrincipal;
        });
    }
}
SessionAuthenticationStrategy

主要在用户登录成功后,对HttpSession进行处理:

public interface SessionAuthenticationStrategy {
    void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response)
            throws SessionAuthenticationException;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhYCkzyz-1646809765498)(en-resource://database/643:1)]

  • CsrfAuthenticationStrategy:和CSRF攻击有关,该类主要负责在身份验证后删除旧的CsrfToken并生成一个新的CsrfToken
  • ConcurrentSessionControlAuthenticationStrategy:该类主要用来处理session并发问题,例如并发数量控制就是通过该类来完成的。
  • RegisterSessionAuthenticationStrategy:该类用于在认证成功后将HttpSession信息记录到SessionRegistry中。
  • CompositeSessionAuthenticationStrategy:这是一个复合策略,它里边维护了一个集合,其中保存了多个不同的SessionAuthenticationStrategy对象,相当于该类代理了多个SessionAuthenticationStrategy对,大部分情况下,在spring security框架中直接使用的也是该类的实例。
  • NullAuthenticatedSessionStrategy:这是一个空的实现,未做任何处理。
  • AbstractSessionFixationProtectionStrategy:处理会话固定攻击的基类。
  • ChangeSessionIdAuthenticationStrategy:通过修改sessionId来防止会话固定攻击。
  • SessionFixationProtectionStrategy:通过创建一个新的会话来防止会话固定攻击。
ConcurrentSessionControlAuthenticationStrategy

在前面的案例中,起主要作用的是ConcurrentSessionControlAuthenticationStrategy,因此先对该类进行重点分析:

@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
        HttpServletResponse response) {
    // 获取session最大并发数,如果值为-1,则没有限制
    int allowedSessions = getMaximumSessionsForThisUser(authentication);
    if (allowedSessions == -1) {
        return;
    }
    // 获取当前用户的所有未失效的SessionInformation实例
    List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
    
    // 如果获取到的SessionInformation实例数小于当前项目允许的最大session数,说明当前登录没问题,直接返回即可
    int sessionCount = sessions.size();
    if (sessionCount < allowedSessions) {
        return;
    }
    if (sessionCount == allowedSessions) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            // 仅当此请求与已注册的会话之一关联时才允许
            for (SessionInformation si : sessions) {
                if (si.getSessionId().equals(session.getId())) {
                    return;
                }
            }
        }
    }

	// 如果在前面的判断中没有return,说明当前用户登录的并发数已经超过允许的并发数了,
	// 进入到allowableSessionsExceeded方法中进行处理
    allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
}

protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
        SessionRegistry registry) throws SessionAuthenticationException {
    // exceptionIfMaximumExceeded的值由maxSeesionsPreventsLogin方法配置,即禁止后来者登录
    if (this.exceptionIfMaximumExceeded || (sessions == null)) {
        throw new SessionAuthenticationException(
                this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
                        new Object[] { allowableSessions }, "Maximum sessions of {0} for this principal exceeded"));
    }

    // 按照最后一次请求的时间进行排序,计算出需要过期的session数量
    sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
    int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
    List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
    for (SessionInformation session : sessionsToBeExpired) {
        session.expireNow();
    }
}
RegisterSessionAuthenticationStrategy

该类的作用主要是向SessionRegistry中记录HttpSession信息:

@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
        HttpServletResponse response) {
    // 调用registerNewSession方法向sessionRegistry中添加一条登录会话信息
    this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
CompositeSessionAuthenticationStrategy

相当于一个代理类,默认使用的其实就是该类的实例:

@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
        HttpServletResponse response) throws SessionAuthenticationException {
    // 遍历维护的SessionAuthenticationStrategy集合,然后分别调用其onAuthentication方法
    for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
        delegate.onAuthentication(authentication, request, response);
    }
}
SessionManagementFilter

和会话并发管理相关的过滤器主要有两个,先来看第一个SessionManagementFilter
其主要用来处理remember-me登录时的会话管理:即如果用户使用了remember-me的方式进行认证,则认证成功后需要进行会话管理,相关的管理操作通过SessionManagementFilter过滤器触发:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    if (request.getAttribute(FILTER_APPLIED) != null) {
        chain.doFilter(request, response);
        return;
    }
    request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
    // 判断当前会话中是否存在SPRING_SECURITY_CONTEXT_KEY变量。如果是正常的认证流程,则SPRING_SECURITY_CONTEXT_KEY变量
    //是存在于当前会话中的。只有当用户使用了remember-me或者匿名访问某一个接口时,该变量才会不存在
    if (!this.securityContextRepository.containsContext(request)) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 如果是通过remember-me的方式进行登录,则SecurityContextHolder中获取到的当前用户实例是RememberMeAuthenticationToken,
        // 因此调用SessionAuthenticationStrategy中的onAuthentication方法进行会话管理
        if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
            try {
                this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
            }
            catch (SessionAuthenticationException ex) {
                SecurityContextHolder.clearContext();
                this.failureHandler.onAuthenticationFailure(request, response, ex);
                return;
            }

            this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
        }
        // 如果是匿名登录,则当前用户实例是AnonymousAuthenticationToken,因此进行会话失效处理
        else {
            if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
                if (this.invalidSessionStrategy != null) {
                    this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
                    return;
                }
            }
        }
    }
    chain.doFilter(request, response);
}
ConcurrentSessionFilter

处理会话并发管理的过滤器。

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    HttpSession session = request.getSession(false);
    if (session != null) {
        SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
        if (info != null) {
            if (info.isExpired()) {
                doLogout(request, response);
                // 调用会话过期的回调
                this.sessionInformationExpiredStrategy
                        .onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
                return;
            }
            // 未过期 - 更新最后一次请求时间
            this.sessionRegistry.refreshLastRequest(info.getSessionId());
        }
    }
    chain.doFilter(request, response);
}
Session创建时机

在spring security中,HttpSession的创建策略一共分为四种:

  • ALWAYS:如果HttpSession不存在,就创建。
  • NEVER:从不创建,但是如果已经存在了,则会使用它。
  • IF_REQUIRED:当有需要时,会创建,默认即此。
  • STATELESS:从不创建,也不使用。

需要注意的是,这四种策略仅仅是指spring security中的创建策略,而并非整个应用程序的。第四种适合于无状态的认证方式,意味着服务端不会创建HttpSession,客户端的每一个请求都需要携带认证信息,同时,一些和HttpSession相关的过滤器也将失效,例如SessionManagementFilterConcurrentSessionFilter等。
如果需要的话,可以自行配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
SessionManagementConfigurer

SessionManagementConfigurer配置类完成了上面两个过滤器的配置:

@Override
public void init(H http) {
    SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
    boolean stateless = isStateless();
    // 如果没有获取到SecurityContextRepository的实例,则进行创建,分为两种情况
    if (securityContextRepository == null) {
        // 如果创建策略是STATELESS,则使用NullSecurityContextRepository,相当于不保存
        if (stateless) {
            http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());
        }
        // 否则构建HttpSessionSecurityContextRepository的实例,并最终存入HttpSecurity的共享对象中以备使用
        else {
            HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
            httpSecurityRepository.setDisableUrlRewriting(!this.enableSessionUrlRewriting);
            httpSecurityRepository.setAllowSessionCreation(isAllowSessionCreation());
            AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
            if (trustResolver != null) {
                httpSecurityRepository.setTrustResolver(trustResolver);
            }
            http.setSharedObject(SecurityContextRepository.class, httpSecurityRepository);
        }
    }
    RequestCache requestCache = http.getSharedObject(RequestCache.class);
    if (requestCache == null) {
        // 如果创建策略是STATELESS,还需要将保存在HttpSecurity共享对象中的请求缓存对象替换为NullRequestCache的实例
        if (stateless) {
            http.setSharedObject(RequestCache.class, new NullRequestCache());
        }
    }
    // 构建三个SessionAuthenticationStrategy的实例,分别是ConcurrentSessionControlAuthenticationStrategy、
    // ChangeSessionIdAuthenticationStrategy、RegisterSessionAuthenticationStrategy,并将这三个实例
    // 由CompositeSessionAuthenticationStrategy进行代理
    http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy(http));
    // 构建InvalidSessionStrategy实例
    http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy());
}

// 主要是构建了两个过滤器SessionManagementFilter和ConcurrentSessionFilter
@Override
public void configure(H http) {
    SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
    // 通过getSessionAuthenticationStrategy方法获取SessionAuthenticationStrategy实例
    // 并传入SessionManagementFilter实例中
    SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(securityContextRepository,
            getSessionAuthenticationStrategy(http));
    if (this.sessionAuthenticationErrorUrl != null) {
        sessionManagementFilter.setAuthenticationFailureHandler(
                new SimpleUrlAuthenticationFailureHandler(this.sessionAuthenticationErrorUrl));
    }
    InvalidSessionStrategy strategy = getInvalidSessionStrategy();
    if (strategy != null) {
        sessionManagementFilter.setInvalidSessionStrategy(strategy);
    }
    AuthenticationFailureHandler failureHandler = getSessionAuthenticationFailureHandler();
    if (failureHandler != null) {
        sessionManagementFilter.setAuthenticationFailureHandler(failureHandler);
    }
    AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
    if (trustResolver != null) {
        sessionManagementFilter.setTrustResolver(trustResolver);
    }
    sessionManagementFilter = postProcess(sessionManagementFilter);
    http.addFilter(sessionManagementFilter);
    // 如果配置了会话并发控制(只要调用了maximumSessions()方法配置了会话最大并发数,就算开启了会话并发控制),
    // 就再创建一个ConcurrentSessionFilter过滤器并加入HttpSecurity中
    if (isConcurrentSessionControlEnabled()) {
        ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http);

        concurrentSessionFilter = postProcess(concurrentSessionFilter);
        http.addFilter(concurrentSessionFilter);
    }
}
AbstractAuthenticationFilterConfigurer

所以,登录成功后,session并发管理到底是在哪里触发的。虽然经过前面的分析,知道有两个过滤器的存在:SessionManagementFilterConcurrentSessionFilter,但是前者在用户使用rememberMe认证时才会触发session并发管理,后者则根部不会触发session并发管理,这时可以回到AbstractAuthenticationProcessingFilterdoFilter方法中去看一下:

// AbstractAuthenticationProcessingFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    // 省略
    try {
        Authentication authenticationResult = attemptAuthentication(request, response);
        if (authenticationResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            return;
        }
        // 在这个地方触发了session的并发管理,这里的sessionStrategy对象则是在
        // AbstractAuthenticationFilterConfigurer类的configure方法中进行配置的
        this.sessionStrategy.onAuthentication(authenticationResult, request, response);
        // Authentication success
        if (this.continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    // 省略
}

// AbstractAuthenticationFilterConfigurer
@Override
public void configure(B http) throws Exception {
    // 省略
    // 从HttpSecurity的共享对象中获取到SessionAuthenticationStrategy实例,
    // 并设置到authFilter过滤器中
    SessionAuthenticationStrategy sessionAuthenticationStrategy = http
            .getSharedObject(SessionAuthenticationStrategy.class);
    if (sessionAuthenticationStrategy != null) {
        this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
    }
    // 省略
}

大致的流程分析:

  • 用户通过用户名/密码发起一个认证请求,当认证成功后,在AbstractAuthenticationProcessingFilter#doFilter方法中触发了session并发管理。
  • 默认的sessionStrategyCompositeSessionAuthenticationStrategy,它一共代理了三个SessionAuthenticationStrategy,分别是ConcurrentSessionControlAuthenticationStrategyChangeSessionIdAuthenticationStrategy以及RegisterSessionAuthenticationStrategy
  • 当前请求在这三个中分别走一圈,第一个用来判断用户的session数是否已经超出限制,如果超出限制就根据配置好的规则作出处理;第二个用来修改sessionId(以防止会话固定攻击);第三个用来将当前session注册到SessionRegistry中。
  • 使用用户名/密码的方式完成认证,将不会涉及ConcurrentSessionFilterSessionManagementFilter两个过滤器。
  • 如果用户使用了remember-me的方式来进行身份认证,则会通过SessionManagementFilter#doFilter方法触发session并发管理。当用户认证成功后,以后的每一次请求都会经过ConcurrentSessionFilter,在该过滤器中,判断当前会话是否已经过期,如果过期就执行注销登录流程;如果没有过期,则更新最近一次请求时间。

7.3会话固定攻击与防御

7.3.1什么是会话固定攻击

会话固定攻击(session fixation attacks)是一种潜在的风险,恶意攻击者有可能通过访问当前应用程序来创建会话,然后诱导用户以相同的会话id登录(通常是将会话id作为参数放在请求链接中,然后诱导用户去单击),进而获取用户的登录身份。例如:

  1. 攻击者自己可以正常访问javaboy网站,在访问的过程中,网站给攻击者分配了一个sessionid。
  2. 攻击者利用自己拿到的sessionid构造一个javaboy网站的链接,并把该链接发送给受害者。
  3. 受害者使用该链接登录javaboy网站(该链接中含有sessionid),登录成功后,一个合法的会话就成功建立了。
  4. 攻击者利用手里的sessionid冒充受害者。

在这个过程中,如果javaboy网站支持URL重写,那么攻击还会变得更加容易。
用户如果在浏览器中禁用了cookie,那么sessionid自然也用不了,所以有的服务端就支持把sessionId放在请求地址中,例如http://www.javaboy.org;jsessionid=xxxxxx。如果服务端支持这种URL重写,那么对于攻击者来说,按照上面的攻击流程,构造一个这样的地址会很容易。

7.3.2会话固定攻击防御策略

Spring security从三个方面入手防范会话固定攻击:

  1. Spring security中默认自带了HTTP防火墙,如果sessionid放在地址栏中,这个请求就会直接被拦截下来。
  2. 在HTTP相应的Set-Cookie字段中有httpOnly属性,这样避免了通过XSS攻击来获取cookie中的会话信息,进而达成会话固定攻击。
  3. 既然会话固定攻击是由于sessionid不变导致的,那么其中一个解决办法就是在用户登录成功后,改变sessionid,spring security中默认实现了该种方案,实现类是ChangeSessionIdAuthenticationStrategy

前两种都是默认行为,第三种方案,spring security中有几种不同的配置策略:

http.sessionManagement().sessionFixation().changeSessionId();

通过sessionFixation()方法开启会话固定攻击防御的配置,一共有四种不同的策略,不同的策略对应了不同的SessionAuthenticationStrategy

  1. changeSessionId():用户登录成功后,直接修改HttpSessionsessionId即可,默认方案即此,对应的处理类是ChangeSessionIdAuthenticationStrategy
  2. none():用户登录成功后,HttpSession不做任何变化,对应的处理类是NullAuthenticatedSessionStrategy
  3. migrateSession():用户登录成功后,创建一个新的HttpSession对象,并将旧的HttpSession中的数据拷贝到新的中,对应的处理类是SessionFixationProtectionStrategy
  4. newSession():用户登录成功后,创建一个新的HttpSession对象,对应的处理类也是SessionFixationProtectionStrategy,只不过将其里边的migrateSessionAttributes属性设置为false。需要注意的是,该方法并非所有的属性都不拷贝,一些spring security使用的属性,如请求缓存,还是会从旧的HttpSession复制到新的HttpSession

这四种策略,无论使用哪种,都相当于配置了一个SessionAuthenticationStrategy,由于默认使用的是ChangeSessionIdAuthenticationStrategy,如果开发者在这里配置了其他类型的SessionAuthenticationStrategy,就会替代掉默认使用的ChangeSessionIdAuthenticationStrategy

7.4Session共享

7.4.1集群会话方案

前面所讲的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的方案就会失效。需要注意的是,这里讨论的范畴是有状态登录,如果采用无状态的认证方案,那么就不涉及会话,也就不存在接下来要讨论的问题。
如果项目是集群化部署,可以采用nginx做反向代理服务器,所有到达nginx上的请求都被转发到不同的tomcat实例上,每个tomcat各自保存自己的会话信息。Spring security中通过维护一张会话注册表来实现会话的并发管理,现在每个tomcat上都有一张会话注册表,所以如果还按照之前的方式去配置会话并发管理,那必然是不生效的。
为了解决集群环境下的会话问题,有三种方案:

  1. Session复制:多个服务之间互相复制session信息,这样每个服务中都包含所有的session信息了,tomcat通过IP组播对这种方案提供支持。但是这种方案占用带宽、有时延,服务数量越多效率越低,所以这种方案使用较少。
  2. Session粘滞:也叫会话保持,就是在nginx上通过一致性hash,将hash结果相同的请求总是分发到一个服务上去。这种方案可以解决一部分集群会话带来的问题,但是无法解决集群中的会话并发管理问题。
  3. Session共享:session共享就是将不同服务的会话统一放在一个地方,所有的服务共享一个会话。一般使用一些key-value数据库来存储session,例如memcached或者redis等,比较常见的方案是使用redis存储,session共享方案由于其简便性与稳定性,是目前使用较多的方案。
    Session共享目前使用比较多的是spring-session,利用spring-session可以方便地实现session的管理。
7.4.2实战

引入web、redis、spring security以及spring session的相关依赖:

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
	<groupId>org.springframework.sessiongroupId>
	<artifactId>spring-session-data-redisartifactId>
dependency>

接下来在application.properties中配置redis的连接信息(根据自己redis服务器的情况进行配置):

spring.redis.password=123
spring.redis.host=127.0.0.1
spring.redis.port=6379

之后提供一个SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	// 注入FindByIndexNameSessionRepository对象,这是一个会话的存储和加载工具,具体的实现类是RedisIndexedSessionRepository
    @Autowired
    FindByIndexNameSessionRepository sessionRepository;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("javaboy")
                .password("{noop}123")
                .roles("admin");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .sessionManagement()
                // 配置session并发数量为1
                .maximumSessions(1)
                // 配置sessionRegistry
                .sessionRegistry(sessionRegistry());
    }

	// 配置SpringSessionBackedSessionRegistry实例,其继承自SessionRegistry,用来维护会话信息注册表
    @Bean
    SpringSessionBackedSessionRegistry sessionRegistry() {
        return new SpringSessionBackedSessionRegistry(sessionRepository);
    }
}

需要注意的是,引入spring-session之后,不再需要配置HttpSessionEventPublisher实例,因为spring-session中通过SessionRepositoryFilter将请求对象重新封装为SessionRepositoryRequestWrapper,并重写了getSession方法。在重写的getSession方法中,最终返回的是HttpSessionWrapper实例,而在HttpSessionWrapper定义时,就重写了invalidate方法。当调用会话的invalidate方法去销毁会话时,就会调用RedisIndexedSessionRepository中的方法,从redis中移除对应的会话信息,所以不再需要HttpSessionEventPublisher实例。
最后再配置一个测试Controller

@RestController
public class HelloController {
    @GetMapping("/")
    public String hello(HttpSession session) {
    	// 返回HttpSession的类型以验证
        return session.getClass().toString();
    }
}

配置完成后进行打包操作:

7.Spring security中的会话管理_第1张图片

需要注意的是,为了方便测试,最好打包成jar文件,方便启动两个实例:

java -jar part7_4-0.0.1-SNAPSHOT.jar --server.port=8083
java -jar part7_4-0.0.1-SNAPSHOT.jar --server.port=8084

两个实例启动完成后,这两个实例实际上共用了一个会话。

你可能感兴趣的:(#,深入浅出spring,security,spring,java)