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


107

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

通过spring安全拦截器的普通表单登录页面工作正常。

在新帐户形式的控制器中,我正在创建一个UsernamePasswordAuthenticationToken并在SecurityContext中手动进行设置:

SecurityContextHolder.getContext().setAuthentication(authentication);

稍后,我在同一页面上检查用户的登录身份:

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

这将返回我之前在身份验证中设置的权限。一切都很好。

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

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

  • 可能与会话ID设置不正确有关吗?
  • 是否有某种方式可能会覆盖我的身份验证?
  • 也许我还需要保存身份验证的另一步骤?
  • 还是我需要做一些事情来在整个会话中声明身份验证,而不是某种程度上的单个请求?

只是寻找一些想法可以帮助我了解这里发生的事情。



2
如果读者告诉您这样做,请当心该问题的答案:SecurityContextHolder.getContext().setAuthentication(authentication)。它可以正常工作,并且很常见,但是如果您这样做,则会遇到严重的功能缺陷。有关更多信息,请参阅我的问题和答案:stackoverflow.com/questions/47233187/…–
山羊

Answers:


62

不久前,我和您有同样的问题。我不记得详细信息,但是以下代码对我有用。此代码在Spring Webflow流中使用,因此在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);
    }

}

2
谢谢,代码对帮助我了解我在正确的地方进行故障排除非常有帮助。看起来我像是抽烟的烟斗,它是在手动身份验证后创建一个新的会话ID,但仍从cookie中识别出旧的会话ID。一定要弄清楚为什么现在,但是至少我显然是正确的。谢谢!
David Parks

4
任何人以下这个指导意见也应该看到这个相关的问题:stackoverflow.com/questions/4824395/...
大卫·公园

14
您能否解释一下如何获取身份验证提供者
Vivex

1
@ s1moner3d,您应该可以通过IoC注入它-> \ @Autowired
Hartmut

1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi

66

我找不到其他完整的解决方案,所以我想我会发布我的。这可能有点hack,但是它解决了上述问题:

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);
}

7
+1-这对我有帮助!我错过了SPRING_SECURITY_CONTEXT更新。...但是这有多“肮脏”?
l3dx 2012年

12
authenticationManager从哪里得到的?
艾萨克(Isaac)2012年

2
像@Autowired AuthenticationServiceImpl authenticationManager一样,该类中的authenticationManager是自动连线的。而且在您的xml配置中还必须进行bean注入,因此Spring知道要注入什么。

1
AuthenticationServiceImpl的实现在哪里?这堂课有什么用?
Pra_A 2015年

3
为什么必须创建一个新会话?SecurityContext不处理吗?
Vlad Manuel Mureșan

17

最终找到了问题的根源。

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

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


4
老实说,这比弹簧安全性更像是设计缺陷。有许多用其他语言编写的框架对此没有任何问题,但是Spring Security只是中断了。
chubbsondubs 2012年

3
解决方法是?
s1moner3d

2
解决方案是什么?
Thiago

6

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

您可以通过使用浏览器端调试器查看HTTP响应中返回的标头来判断是否设置了会话Cookie。(还有其他方法。)

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


谢谢,这些都是非常有用和相关的建议!
David Parks

5

Servlet 2.4中的新过滤功能从根本上减轻了过滤器只能在应用程序服务器实际请求处理之前和之后的请求流中运行的限制。取而代之的是,Servlet 2.4过滤器现在可以在每个分配点与请求分配器进行交互。这意味着当Web资源将请求转发到另一个资源(例如,将请求转发到同一应用程序中的JSP页面的servlet)时,可以在目标资源处理请求之前运行过滤器。这也意味着,如果Web资源包括其他Web资源的输出或功能(例如,一个JSP页面包括多个其他JSP页面的输出),则Servlet 2.4过滤器可以在每个包含的资源之前和之后工作。。

要启用该功能,您需要:

web.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>

RegistrationController

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

好信息,但是将用户名和密码放入url是不好的。1)没有进行转义操作,因此具有特殊字符的用户名或密码很可能会被破坏,甚至更糟,被用作安全利用媒介。2)网址中的密码是错误的,因为网址经常会记录到磁盘上,这对安全性来说是非常糟糕的-您所有的纯文本密码都只是放在那儿。
山羊

1

我试图测试extjs应用程序,并成功设置了testingAuthenticationToken后,这突然停止工作,没有明显原因。

我无法获得上述答案,因此我的解决方案是在测试环境中略过这一点。我在春天周围引入了一个接缝,如下所示:

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

用户是此处的自定义类型。

然后,我将其包装在一个类中,该类仅具有测试代码切换选项。

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传递到需要它的类中。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.