单点登录在 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设置都不起作