单点登录在 Spring Web 应用程序中不起作用

Posted

技术标签:

【中文标题】单点登录在 Spring Web 应用程序中不起作用【英文标题】:Single sign-on not working in Spring web app 【发布时间】:2015-02-15 21:41:37 【问题描述】:

我将我的 Web 应用程序配置为通过 Active Directory 存储库进行身份验证,它工作正常,但我总是需要在登录表单中插入凭据。

应用程序客户端将是连接到公司网络的 Windows 机器,都在同一个域中。

我需要将我的 Web 应用程序配置为自动对已在 Windows 中进行身份验证的用户进行身份验证。

我正在将此配置用于 Spring Security:

<security:authentication-manager alias="authenticationManager">
    <security:authentication-provider
        user-service-ref="userDetailsService">
        <security:password-encoder hash="plaintext" />
    </security:authentication-provider> <!-- for DB authentication -->
    <security:authentication-provider
        ref="adAuthenticationProvider" />
</security:authentication-manager>

<bean id="adAuthenticationProvider"
    class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <constructor-arg name="domain" value="mydomain.it" />
    <constructor-arg name="url" value="ldap://domaincontroller.mydomain.it/" />
</bean>

注意我还需要辅助身份验证提供程序来提供数据库身份验证。

我还在 IE (v 9) 中设置了以下应该启用自动登录的选项:

但它不起作用......那么我的配置有什么问题?

注意 #2 我使用的是 Spring v 3.2.9 和 Spring Security v 3.2.3

【问题讨论】:

【参考方案1】:

“正确”的方法是使用 Kerberos/SPNEGO。但是,服务器必须是您的 Windows 域上的受信任节点。如果您的服务器是 Windows 机器,那么这应该很容易。但是,如果它是一个 'NIX/Linux 机器,那么它可以是一个真正的 PITA。

这涉及到一些事情,例如在 Active Directory 中使用 SPN(服务主体名称)设置它,以及在您的服务器上安装一大堆东西以与 A/D 集成并对用户进行身份验证。

如果您(或您友好的 Windows 基础架构团队成员)对做这一切感到满意,那就去做吧!但是,如果您不熟悉它,我应该警告您,当它不起作用时,诊断问题是地狱。

但是有一个快速而肮脏的选项,它不涉及对网络上您的服务器的任何信任。事实上,整个事情都可以包含在您的 Spring 应用程序中。那是不太安全但可以工作的 NTLM。设置起来非常简单。

首先,如果没有会话,您需要一个 Servlet 过滤器来拦截请求并执行握手:

import java.io.IOException;

import org.apache.commons.codec.binary.Base64;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

/**
 * Simple authentication filter designed to get hold of the username via NTLM SSO. 
 * See Spring documentation on pre-authentication filters to see how this can be used.
 * </p>
 * <p>
 * <a href="http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#preauth">http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#preauth</a>
 * </p>
 */
@Component("ntlmFilter")
public class NtlmFilter implements Filter 

    private static Logger log = LoggerFactory.getLogger(NtlmFilter.class);

    public static final String USERNAME_KEY = "SM_USER";

    public NtlmFilter() 
        log.info("Initialising the NTLM filter.");
    

    @Override
    public void init(FilterConfig filterConfig) throws ServletException 
        // No initialisation tasks.
    

    @Override
    public void destroy() 
        // No destruction tasks.
    

    /**
     * 
     */
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException 
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (isAuthenticated(request)) 
            log.debug("Session already authenticated. Proceeding down filter chain.");
            setRequestHeaders(request);
            proceed(req, res, chain);
         else 
            log.debug("Session not yet authenticated. Attempting to login...");
            login(request, response, chain);
        
    

    private void proceed(ServletRequest req, ServletResponse res, FilterChain chain) 
            throws IOException, ServletException 
        try 
            chain.doFilter(req, res);
         catch (IOException e) 
            log.error("IOException processing NtlmAuthFilter Servlet filter.", e);
            throw e;
         catch (ServletException e) 
            log.error("ServletException processing NtlmAuthFilter Servlet filter.", e);
            throw e;
        
    

    /**
     * If the user name has been stored in the session, then the user has been
     * authenticated by the application.
     */
    private boolean isAuthenticated(HttpServletRequest req) 
        if (req.getSession().getAttribute(USERNAME_KEY) != null) 
            return true;
         else 
            return false;
        
    

    public void login(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException 

        String username = null;

        String auth = req.getHeader("Authorization");
        if (auth == null) 
            // First phase. Return NTLM challenge headers.
            res.setHeader("WWW-Authenticate", "NTLM");
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.setContentLength(0);
            res.flushBuffer();
            return;
         else if (auth.startsWith("NTLM ")) 
            byte[] msg = Base64.decodeBase64(auth.substring(5));
            int off = 0, length, offset;
            if (msg[8] == 1) 
                // Login details are not valid. Reject.
                byte z = 0;
                byte[] msg1 =  (byte) 'N', (byte) 'T', (byte) 'L',
                        (byte) 'M', (byte) 'S', (byte) 'S', (byte) 'P',
                        z, (byte) 2, z, z, z, z, z, z, z, (byte) 40, z,
                        z, z, (byte) 1, (byte) 130, z, z, z, (byte) 2,
                        (byte) 2, (byte) 2, z, z, z, z, z, z, z, z, z,
                        z, z, z ;
                res.setHeader(
                        "WWW-Authenticate", 
                        "NTLM " + Base64.encodeBase64String(msg1));
                res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                res.setContentLength(0);
                res.flushBuffer();
                return;
             else if (msg[8] == 3) 
                // Login details seem valid. Grab the username.
                off = 30;

                length = msg[off + 9] * 256 + msg[off + 8];
                offset = msg[off + 11] * 256 + msg[off + 10];
                username = new String(msg, offset, length);
                username = canonicalUsername(username);
            
        

        req.getSession().setAttribute(USERNAME_KEY, username);

        setRequestHeaders(req);

        log.info("User details now stored in session: " + username);

        proceed(req, res, chain);
    

    private void setRequestHeaders(HttpServletRequest req) 
        req.setAttribute(USERNAME_KEY, req.getSession().getAttribute(USERNAME_KEY));
    

    /**
     * To avoid issues with comparing user names with differing case and spaces, 
     * this method strips out extraneous spaces and lower-cases it.
     */
    private String canonicalUsername(String username) 
        return username.replaceAll("[^a-zA-Z0-9#]", "").toLowerCase().trim();
    


您可能会注意到这会在请求中创建一个SM_USER 标头。如果您确保此过滤器在RequestHeaderAuthenticationFilter 之前运行,那么您有一个很好的设置,其中标头由 SSO 过滤器定义,然后所有内容都传递给标准 Spring 身份验证处理。这可以像这样完成......

@Configuration
@EnableWebSecurity
@EnableWebMvcSecurity
@Profile("secure")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired(required = true)
    @Qualifier("ntlmFilter")
    private Filter ntlmFilter;

    @Autowired(required = true)
    @Qualifier("headerAuthFilter")
    private Filter headerAuthFilter;

    // ...

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.addFilterBefore(ntlmFilter, RequestHeaderAuthenticationFilter.class)
            .anonymous().disable()
            .csrf().disable()
            .exceptionHandling().authenticationEntryPoint(http403ForbiddenEntryPoint());
    

    @Bean(name = "headerAuthFilter")
    public Filter headerAuthFilter(AuthenticationManager authenticationManager) 
        RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
        filter.setPrincipalRequestHeader("SM_USER");
        filter.setAuthenticationManager(authenticationManager);
        filter.setExceptionIfHeaderMissing(false);
        return filter;
    

    // ...


【讨论】:

任何使用上述代码的人都可能会穿着裤子绕脚踝四处走动。我也不建议使用 LDAP 进行身份验证,因为它需要明文密码。 Kerberos 很困难,只能在有限的场景中工作。在 Java 应用程序中拥有强大 SSO 的一种安全且简单的方法是 Jespa。【参考方案2】:

您配置的是 LDAP 身份验证,如果要进行自动身份验证,则必须使用 Kerberos/SPNEGO。

Spring 有一个用于 Kerberos/SPNEGO 身份验证的模块,请查看 blog,它解释了 Kerberos/SPNEGO 的工作原理以及如何配置 spring 安全性。

您还必须在 IE 中启用“Windows 集成身份验证”,如下所示。

【讨论】:

谢谢。您能提供一个完整的工作示例吗?我正在按照您建议的教程进行操作,但是我在 jar 版本和配置方面遇到了一些问题... @davioooh 请检查github.com/spring-projects/spring-security-kerberos/tree/master/… 例如。【参考方案3】:

尝试使用这个答案:

    https://***.com/a/26581526/3587592 https://***.com/a/26350288/3587592 https://***.com/a/26300751/3587592(更详细) https://***.com/a/26350395/3587592

希望对你有帮助

【讨论】:

以上是关于单点登录在 Spring Web 应用程序中不起作用的主要内容,如果未能解决你的问题,请参考以下文章

SPNEGO Kerberos 单点登录在 tomcat 服务器的 AD 域中不起作用

使用dwr后,javaweb设置的session超时失效,web.xml和tomcat设置都不起作

Spring-security-cas 插件单点注销不起作用

自定义登录在 Spring 中不起作用

使用 Spring Security 的预认证/单点登录

Windows 身份验证在 Flex 中不起作用