将维护模式添加到 Spring(安全)应用程序

Add Maintenance Mode to Spring (Security) app

我正在寻找一种在我的 Spring 应用程序中实施维护模式的方法。

当应用程序处于维护模式时,仅应允许用户 role = MAINTENANCE 登录。其他所有人都会被重定向到登录页面。

现在我刚刚构建了一个过滤器:

@Component
public class MaintenanceFilter extends GenericFilterBean {
    @Autowired SettingStore settings;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
            HttpServletResponse res = (HttpServletResponse) response; 
            res.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
        } else {
            chain.doFilter(request, response); 
        }
    }
}

并使用以下方式添加:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // omitted other stuff
        .addFilterAfter(maintenanceFilter, SwitchUserFilter.class);
}

因为据我所知 SwitchUserFilter 应该是 Spring 安全过滤器链中的最后一个过滤器。

现在每个请求都会被取消并返回 503 响应。虽然无法访问登录页面。

如果我向过滤器添加重定向,这将导致无限循环,因为登录页面的访问也被拒绝。

此外,我想不出获取当前用户角色的好方法。或者我应该选择 SecurityContextHolder ?


我正在寻找一种将每个用户重定向到登录页面的方法(可能使用查询参数 ?maintenance=true)并且每个 role = MAINTENANCE 的用户都可以使用该应用程序。

所以过滤器/拦截器应该表现得像:

if(maintenance.isEnabled()) {
    if(currentUser.hasRole(MAINTENANCE)) {
        // this filter does nothing
    } else {
        redirectTo(loginPage?maintenance=true);
    }
}

这是先有鸡还是先有蛋的两难选择,你想向未授权用户显示 "we're in maintenance mode ..." 消息,同时允许授权用户登录,但在他们登录之前你不知道他们是否已授权。理想情况下,将它放在某种过滤器中会很好,但我发现在实践中,通过在登录后放置逻辑对我来说更容易解决类似的问题,例如 UserDetailsService.

下面是我在项目中的解决方法。当我处于维护模式时,我为视图设置了一个标志以在全局 header 或登录页面上显示 "we're in maintenance mode .." 消息。所以用户,无论他们是谁,都知道这是维护模式。登录应该可以正常工作。

用户通过身份验证后,在我的自定义 UserDetailsService 中,他们的用户详细信息与他们的角色一起加载,我执行以下操作:

// if we're in maintenance mode and does not have correct role
if(maintenance.isEnabled() && !loadedUser.hasRole(MAINTENANCE)) {
  throw new UnauthorizedException(..)
}
// else continue as normal

它不是很漂亮,但它很容易理解(我认为这对安全配置有好处)并且有效。

更新:

With you solution I'd have to destroy everyone's session, else a user which was logged in before maintenance mode was enabled, is still able to work with the system

在我们的项目中,我们不允许任何用户在维护模式下登录。管理员启动了一项启用 "maintenance..." msg 的任务,倒计时,然后在最后,我们使用 SessionRegistry.

使每个人的 session 过期

我现在找到了两个相似的解决方案,但我注入代码的地方看起来不太好。

我为两者添加了一个自定义 RequestMatcher,它获取 @Autowired 并检查是否启用了维护模式。

解决方案 1:

@Component
public class MaintenanceRequestMatcher implements RequestMatcher {
    @Autowired SettingStore settingStore;

    @Override
    public boolean matches(HttpServletRequest request) {
        return settingStore.get(MaintenanceMode.KEY).isEnabled()
    }
}

在我的安全配置中:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .requestMatchers(maintenanceRequestMatcher).hasAuthority("MY_ROLE")
            .anyRequest().authenticated()
    // ...
}

解决方案 2:

非常相似,但使用 HttpServletRequest.isUserInRole(...):

@Component
public class MaintenanceRequestMatcher implements RequestMatcher {
    @Autowired SettingStore settingStore;

    @Override
    public boolean matches(HttpServletRequest request) {
        return settingStore.get(MaintenanceMode.KEY).isEnabled() && !request.isUserInRole("MY_ROLE");
    }
}

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .requestMatchers(maintenanceRequestMatcher).denyAll()
            .anyRequest().authenticated()
    // ...
}

如果启用了维护模式,这将执行 denyAll() 并且 当前用户没有 MY_ROLE


唯一的缺点是,我无法设置自定义响应。我更愿意 return 一个 503 Service Unavailable。也许有人可以弄清楚如何做到这一点。

我遇到了类似的情况,发现这个答案很有帮助。我采用了第二种方法,并且还设法 return 自定义响应。

这是我对 return 自定义响应所做的。

1- 定义 return 所需的自定义响应的控制器方法。

@RestController
public class CustomAccessDeniedController {
    
    @GetMapping("/access-denied")
    public String getAccessDeniedResponse() {
        return "my-access-denied-page";
    }
}

2- 在您的安全上下文中,您应该允许此 URL 可访问。

http.authorizeRequests().antMatchers("/access-denied").permitAll()

3- 创建自定义访问被拒绝异常处理程序

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    private SettingStore settingStore;
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        
        if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
            response.sendRedirect(request.getContextPath() + "/access-denied");
        }
    }
}

4- 在安全配置中注册自定义访问拒绝异常处理程序

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;


    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);