如何在 Spring Security/SpringMVC 中手动设置一个经过身份验证的用户

新用户提交“新帐户”表单后,我想手动登录该用户,这样他们就不必登录后续页面。

通过 Spring 安全拦截器的普通表单登录页面工作得很好。

在 new-account-form 控制器中,我创建了一个 UsernamePasswordAuthenticationToken,并在 SecurityContext 中手动设置它:

SecurityContextHolder.getContext().setAuthentication(authentication);

在同一个页面上,我稍后检查用户是否用以下方式登录:

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

这将返回我在前面的身份验证中设置的权限。

但是,当在我加载的下一个页面上调用相同的代码时,身份验证令牌只是 UserAnonymous。

我不清楚为什么它没有保留我在前一个请求上设置的身份验证。有什么想法吗?

  • Could it have to do with session ID's not being set up correctly?
  • 是不是有什么东西可能改写了我的身份验证?
  • Perhaps I just need another step to save the authentication?
  • 或者我需要做些什么来在整个会话中声明身份验证,而不是以某种方式声明单个请求?

我只是在找一些想法也许能帮我弄清楚这里发生了什么。

209121 次浏览

打开调试日志记录,以更好地了解正在发生的事情。

You can tell if the session cookies are being set by using a browser-side debugger to look at the headers returned in HTTP responses. (There are other ways too.)

一种可能性是 SpringSecurity 正在设置安全会话 cookie,并且您的下一个请求页面具有“ http”URL 而不是“ https”URL。(浏览器不会为“ http”URL 发送安全 cookie。)

我以前也遇到过和你一样的问题。我不记得细节了,但是下面的代码让我可以工作。此代码在 SpringWebflow 流中使用,因此使用 RequestContext 和 ExternalContext 类。但是与您最相关的部分是 doAutoLogin 方法。

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
RequestContext requestContext,
ExternalContext externalContext) {


try {
Locale userLocale = requestContext.getExternalContext().getLocale();
this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
return "success";


} catch (EmailAddressNotUniqueException e) {
MessageResolver messageResolvable
= new MessageBuilder().error()
.source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
.code("userRegistration.emailAddress.not.unique")
.build();
requestContext.getMessageContext().addMessage(messageResolvable);
return "error";
}


}




private void doAutoLogin(String username, String password, HttpServletRequest request) {


try {
// Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
token.setDetails(new WebAuthenticationDetails(request));
Authentication authentication = this.authenticationProvider.authenticate(token);
logger.debug("Logging in with [{}]", authentication.getPrincipal());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(null);
logger.error("Failure in autoLogin", e);
}


}

最终找到了问题的根源。

当我手动创建安全上下文时,不会创建会话对象。只有当请求完成处理时,Spring Security 机制才会意识到会话对象为 null (当它试图在请求被处理后将安全上下文存储到会话时)。

在请求的末尾,SpringSecurity 创建一个新的会话对象和会话 ID。但是这个新的会话 ID 永远不会出现在浏览器中,因为它发生在请求结束时,即对浏览器做出响应之后。当下一个请求包含前一个会话 ID 时,这将导致新的会话 ID (因此包含手动登录用户的 Security 上下文)丢失。

Servlet 2.4中的新过滤特性基本上减轻了过滤器只能在应用服务器实际处理请求之前和之后的请求流中操作的限制。相反,Servlet 2.4过滤器现在可以在每个调度点与请求调度程序交互。这意味着当一个 Web 资源将一个请求转发到另一个资源(例如,一个 servlet 将请求转发到同一个应用程序中的一个 JSP 页面)时,过滤器可以在请求被目标资源处理之前进行操作。这还意味着,如果 Web 资源包含来自其他 Web 资源的输出或函数(例如,包含来自多个其他 JSP 页面的输出的 JSP 页面) ,则 Servlet 2.4过滤器可以在每个包含的资源之前和之后工作。.

要打开这个特性,你需要:

Xml

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/<strike>*</strike></url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>

注册管制员

return "forward:/login?j_username=" + registrationModel.getUserEmail()
+ "&j_password=" + registrationModel.getPassword();

我找不到任何其他完整的解决方案,所以我想我会张贴我的。这可能有点像黑客,但它解决了上述问题:

public void login(HttpServletRequest request, String userName, String password)
{


UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);


// Authenticate the user
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);


// Create a new session and add the security context.
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}

我试图测试一个 extjs 应用程序,在成功设置 testingAuthenticationToken 之后,它突然无缘无故地停止工作。

我无法得到以上的答案,所以我的解决方案是跳过这一点春天在测试环境中。我在弹簧周围放了一条接缝,像这样:

public class SpringUserAccessor implements UserAccessor
{
@Override
public User getUser()
{
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
return (User) authentication.getPrincipal();
}
}

User 是这里的自定义类型。

然后我将它包装在一个类中,该类只有一个选项可以让测试代码切换弹簧出。

public class CurrentUserAccessor
{
private static UserAccessor _accessor;


public CurrentUserAccessor()
{
_accessor = new SpringUserAccessor();
}


public User getUser()
{
return _accessor.getUser();
}


public static void UseTestingAccessor(User user)
{
_accessor = new TestUserAccessor(user);
}
}

测试版本看起来就像这样:

public class TestUserAccessor implements UserAccessor
{
private static User _user;


public TestUserAccessor(User user)
{
_user = user;
}


@Override
public User getUser()
{
return _user;
}
}

在调用代码中,我仍然使用从数据库加载的适当用户:

    User user = (User) _userService.loadUserByUsername(username);
CurrentUserAccessor.UseTestingAccessor(user);

显然,如果您实际上需要使用安全性,那么这样做是不合适的,但是我正在使用一个用于测试部署的无安全性设置运行。我以为其他人也会遇到类似的情况。这是我以前用来模拟静态依赖关系的模式。另一个选择是您可以维护包装器类的静态性,但是我更喜欢这个选择,因为代码的依赖关系更加明确,因为您必须将 CurrentUserAccessor 传递到需要它的类中。