我正在寻找一种在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
应该是SpringSecurity过滤器链中的最后一个过滤器。
现在,每个请求都会被503响应取消。尽管无法访问登录页面。
如果我向Filter添加重定向,这将导致无限循环,因为对登录页面的访问也被拒绝。
此外,我想不出一个好的方法来获得当前用户的角色。还是我应该选择SecurityContextHolder
?
我正在寻找一种方法,将每个用户重定向到登录页面(可能使用查询参数?maintenance=true
),并且每个具有role = MAINTENANCE
的用户都可以使用该应用程序。
因此,过滤器/拦截器的行为应该像:
if(maintenance.isEnabled()) {
if(currentUser.hasRole(MAINTENANCE)) {
// this filter does nothing
} else {
redirectTo(loginPage?maintenance=true);
}
}
我现在找到了两个类似的解决方案,它们都能工作,但我注入代码的地方看起来不太好。
对于两者,我都添加了一个自定义的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()
// ...
}
如果启用了维护模式并且当前用户没有MY_ROLE
,这将执行denyAll()
。
唯一的缺点是,我无法设置自定义响应。我想退回一个503 Service Unavailable
。也许有人能想出怎么做。
这有点像鸡还是蛋的困境,你想向未经授权的用户显示一条"我们处于维护模式…"的消息,同时允许授权用户登录,但在他们登录之前你不知道他们是否被授权。理想情况下,在某种过滤器中设置这一点会很好,但我发现,在实践中,通过在登录后放置逻辑,我更容易解决类似的问题,比如在UserDetailsService
中。
以下是我在一个项目中解决问题的方法。当我处于维护模式时,我会为视图设置一个标志,以在全局标头或登录页面上显示"we are 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
这并不漂亮,但它很容易理解(我认为这对安全配置很有好处),而且它很有效。
更新:
使用您的解决方案,我必须销毁每个人的会话,否则用户在启用维护模式之前登录的,仍然能够使用系统
在我们的项目中,我们不允许任何用户在维护模式下登录。管理员启动一个启用"维护…"消息的任务,并进行倒计时,然后在最后,我们使用SessionRegistry
终止每个人的会话。
我也遇到过类似的情况,发现这个答案很有帮助。我采用了第二种方法,还成功地返回了自定义响应。
以下是我为返回自定义响应所做的操作。
1-定义一个返回所需自定义响应的控制器方法。
@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);