Spring Security应用程序中的su和sudo

很久以前,我从事的项目具有很强大的功能。 有两个角色:用户和主管。 主管可以以任何方式更改系统中的任何文档,而用户则更受工作流约束的限制。 当普通用户对当前正在编辑和存储在HTTP会话中的文档有疑问时,主管可以介入,切换到特殊的主管模式并绕过所有约束。 完全自由。 相同的计算机,相同的键盘,相同的HTTP会话。 通过输入秘密密码,只有管理员可以设置的特殊标志。 主管完成后,他或她可以清除该标志并再次启用常规约束。

此功能运行良好,但实施效果不佳。 每个单个输入字段的可用性取决于该超级用户模式标志。 使用isSupervisorMode()检查在数十个地方污染了业务方法。 请记住,如果管理员只是使用常规凭据登录,则此模式是隐式的,因此安全约束基本上是重复的。

当我们的应用程序可以高度自定义并具有大量安全角色时,就会出现另一个有趣的用例。 迟早您将面临异常(确定, 错误 ),您无法复制具有不同权限的异常。 能够以该特定用户身份登录并环顾四周可能是一个很大的胜利。 当然,您不知道用户的密码( 不是吗? )。 类似UNIX的系统找到了解决此问题的方法: su切换用户 )和sudo命令。 出乎意料的是, Spring Security附带了内置的SwitchUserFilter ,它在原则上模仿Web应用程序中的su 。 试一试吧!

您需要声明自定义过滤器:

<bean id="switchUserProcessingFilter"
       class="org.springframework.security.web.authentication.switchuser.SwitchUserFilter">
    <property name="userDetailsService" ref="userDetailsService"/>
    <property name="targetUrl" value="/"/>
</b:bean>

并在<http>配置中指向它:

<http auto-config="true" use-expressions="true">
    <custom-filter position="SWITCH_USER_FILTER" ref="switchUserProcessingFilter" />
    <intercept-url pattern="/j_spring_security_switch_user" access="hasRole('ROLE_SUPERVISOR')"/>
    ...

而已! 请注意,我保护了/j_spring_security_switch_user URL模式。 您猜对了,这就是您以其他用户身份登录的方式,因此我们希望此资源得到良好的保护。 默认情况下,使用j_username参数名称。 将上述更改应用于您的Web应用程序并以具有ROLE_SUPERVISOR的用户ROLE_SUPERVISOR登录后,只需浏览以下内容即可:

/j_spring_security_switch_user?j_username=bob

假设存在这样的用户,您将自动以bob身份登录。 此处不需要密码。 模拟完他后,浏览至/j_spring_security_exit_user将还原您以前的凭据。 当然,所有这些URL是可配置的。 参考文档中未记录SwitchUserFilter ,但谨慎使用时它是非常有用的工具。

确实具有强大的力量…… 。 像任何其他任意用户一样,甚至让最受信任的用户登录也具有很大的风险。 想象一下在Facebook上的这种功能,这是不可能的! ( 很好…… )因此,跟踪和审核成为一项主要要求。

我通常首先要做的是在Spring Security过滤器之后添加一个小的servlet过滤器,该过滤器将用户名添加到MDC

import org.slf4j.MDC;
 
public class UserNameFilter implements Filter {
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        final String userName = authentication.getName();
        final String fullName = userName + (realName != null ? " (" + realName + ")" : "");
 
        MDC.put("user", fullName);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("user");
        }
    }
 
    private String findSwitchedUser(Authentication authentication) {
        for (GrantedAuthority auth : authentication.getAuthorities()) {
            if (auth instanceof SwitchUserGrantedAuthority) {
                return ((SwitchUserGrantedAuthority)auth).getSource().getName();
            }
        }
        return null;
    }
 
    //...
}

只要记住 Spring Security 之后将其添加到web.xml 。 此时,您可以在logback.xml引用"user"键:

<pattern>%d{HH:mm:ss.SSS} | %-5level | %X{user} | %thread | %logger{1} | %m%n%rEx</pattern>

看到%X{user}代码段? 每次登录的用户在系统中执行某些触发日志语句的操作时,都会看到该用户的名称:

21:56:55.074 | DEBUG | alice | http-bio-8080-exec-9 | ...
//...
21:56:57.314 | DEBUG | bob (alice) | http-bio-8080-exec-3 | ...

第二个日志语句很有趣。 如果您查看上面的findSwitchedUser()调用,很明显,作为管理员的alice切换到用户bob ,现在代表他浏览。

有时您需要更强大的审核系统。 幸运的是,Spring框架具有内置的事件基础结构,我们可以利用当有人切换用户并退出此模式时发送的AuthenticationSwitchUserEvent

@Service
public class SwitchUserListener
       implements ApplicationListener<AuthenticationSwitchUserEvent> {
 
    private static final Logger log = LoggerFactory.getLogger(SwitchUserListener.class);
 
    @Override
    public void onApplicationEvent(AuthenticationSwitchUserEvent event) {
        log.info("User switch from {} to {}",
                event.getAuthentication().getName(),
                event.getTargetUser().getUsername());
    }
}

当然,您可以使用所需的任何业务逻辑来替换简单的日志记录,例如,将此类事件存储在数据库中或向安全员发送电子邮件。

因此,我们知道如何以其他用户身份登录一段时间,然后退出这种模式。 但是,如果我们需要“ sudo ”,即代表其他用户仅发出一个HTTP请求,该怎么办? 当然,我们可以切换到该用户,运行该请求,然后退出。 但这似乎太繁重且麻烦。 当客户端程序访问我们的API并希望以其他用户身份查看数据时(考虑测试复杂的ACL),可能会弹出这样的要求。

添加自定义HTTP标头以表示这样的特殊模拟请求听起来很合理。 假设客户端已经在进行身份验证(例如使用JSESSIONID cookie),则该功能仅在一个请求期间有效。 不幸的是,Spring Security不支持此功能,但是很容易在SwitchUserFilter之上SwitchUserFilter

public class SwitchUserOnceFilter extends SwitchUserFilter {
 
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
 
        final String switchUserHeader = request.getHeader("X-Switch-User-Once");
        if (switchUserHeader != null) {
            trySwitchingUserForThisRequest(chain, request, res, switchUserHeader);
        } else {
            super.doFilter(req, res, chain);
        }
    }
 
    private void trySwitchingUserForThisRequest(FilterChain chain, HttpServletRequest request, ServletResponse response, String switchUserHeader) throws IOException, ServletException {
        try {
            proceedWithSwitchedUser(chain, request, response, switchUserHeader);
        } catch (AuthenticationException e) {
            throw Throwables.propagate(e);
        }
    }
 
    private void proceedWithSwitchedUser(FilterChain chain, HttpServletRequest request, ServletResponse response, String switchUserHeader) throws IOException, ServletException {
        final Authentication targetUser = attemptSwitchUser(new SwitchUserRequest(request, switchUserHeader));
        SecurityContextHolder.getContext().setAuthentication(targetUser);
 
        try {
            chain.doFilter(request, response);
        } finally {
            final Authentication originalUser = attemptExitUser(request);
            SecurityContextHolder.getContext().setAuthentication(originalUser);
        }
 
    }
 
}

与原始SwitchUserFilter的唯一区别是,如果存在"X-Switch-User-Once" ,则将凭据切换到由此标头的值表示的用户-但是仅在一个HTTP请求期间。 SwitchUserFilter假定要切换到的用户名在j_username参数下,因此我不得不使用SwitchUserRequest包装器作弊:

private class SwitchUserRequest extends HttpServletRequestWrapper {
 
    private final String switchUserHeader;
 
    public SwitchUserRequest(HttpServletRequest request, String switchUserHeader) {
        super(request);
        this.switchUserHeader = switchUserHeader;
    }
 
    @Override
    public String getParameter(String name) {
        switch (name) {
            case SPRING_SECURITY_SWITCH_USERNAME_KEY:
                return switchUserHeader;
            default:
                return super.getParameter(name);
        }
    }
}

我们的自定义“ sudo ”就位了! 您可以使用curl进行测试:

$ curl localhost:8080/books/rest/book \
    -H "X-Switch-User-Once: bob" \
    -b "JSESSIONID=..."

当然,如果没有JSESSIONID cookie,系统将不允许我们进入。我们必须首先登录,并具有访问sudo功能的特殊特权。 切换用户是一个方便且功能强大的工具。 如果您想在实践中尝试,请查看GitHub上的工作示例

参考: Java和社区博客上我们JCG合作伙伴 Tomasz Nurkiewicz提供的Spring Security应用程序中的su和sudo

翻译自: https://www.javacodegeeks.com/2013/07/su-and-sudo-in-spring-security-applications.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值