使用 Spring Security 无需授权的预认证

Posted

技术标签:

【中文标题】使用 Spring Security 无需授权的预认证【英文标题】:Pre-Authentication without Authorization using Spring Security 【发布时间】:2014-04-02 15:43:42 【问题描述】:

我的要求是:

在我的应用程序中,当用户通过登录屏幕(而不是使用 Spring Security)登录时,首次由定制的第三方 API 执行身份验证。现在,我们在使用其余服务调用方面得到了一些改进。根据要求,在从我们的应用程序进行任何休息调用之前,我们需要针对数据库重新验证用户。由于用户在使用登录屏幕登录时已经过验证,并且这些详细信息在请求中可用,我计划使用 spring security 重新验证用户(预验证场景)。我们没有任何角色为我们应用程序中的用户定义。所以不用担心角色。我已经阅读了参考手册,但我没有得到太多关于如何进一步进行的信息。我理解的一件事是,在用户通过定制的第三方 API 进行身份验证后,我们需要以某种方式告诉 spring 上下文有关用户的信息。即使我在谷歌上搜索了一下,但找不到适合我要求的好例子。 如果有人能指导我如何从一个例子开始,那就太好了。

我只需要告诉 spring 上下文类似“嘿..!这个用户已经过身份验证,因此在自定义第三方验证用户凭据后,他可以被允许调用其余服务API。

我不应该更改现有的初始身份验证过程。我应该只使用经过身份验证的用户信息,并进一步使用 spring security 来重新验证用户。

我的问题与spring参考手册http://docs.spring.io/spring-security/site/docs/3.0.x/reference/preauth.html中提到的问题有些相似

请不要用单行回答(除非它有适当的 外部链接)。如果您能给我一个示例或伪代码,那就太好了。

提前致谢。

【问题讨论】:

您在数据库中存储了哪些您想要对其进行身份验证的信息?您可以创建自己的 UserDetailsS​​ervice (docs.spring.io/spring-security/site/docs/3.0.x/apidocs/org/…) 并使用 bean 从数据库中获取您需要的任何信息。 Chinmay, 1) 用户的凭据已经通过定制的第三方 API 进行了身份验证。这些凭据在请求对象中可用。 2)现在,我想通过不再访问数据库来重新验证用户,因为他已经通过了身份验证。 3)我的计划是将用户信息存储在 spring 上下文中(不知道如何)并将其用于重新身份验证而无需再次访问数据库。可能我可能需要自定义 AuthenticationManger,不确定。我的问题现在清楚了吗? 【参考方案1】:

我正在做一些非常相似的事情。我正在为无状态 REST 后端进行身份验证,因此我希望用户进行一次身份验证,然后对于每个后续请求,身份验证必须是透明的。我为此使用令牌。登录时,用户提供的凭据用于身份验证和生成令牌(尽管最终我们希望使用外部服务来获取令牌)。令牌作为标头返回。然后 angularjs 前端在每个后续的 REST 调用中发送令牌。后端检查令牌的有效性,如果它是好的,则将'authenticated'标记为true。

这是我的 security-context.xml:

<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
  http://www.springframework.org/schema/security
  http://www.springframework.org/schema/security/spring-security-3.2.xsd
  http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

<http use-expressions="true" 
      entry-point-ref="restAuthenticationEntryPoint"
      create-session="stateless">
    <intercept-url pattern="/secured/extreme/**" access="hasRole('ROLE_SUPERVISOR')"/>
    <intercept-url pattern="/secured/**" access="isAuthenticated()" />
    <intercept-url pattern="/j_spring_security_check" requires-channel="https" access="permitAll"/>
    <intercept-url pattern="/logon.jsp" requires-channel="https" access="permitAll"/>
    <sec:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
</http>

<beans:bean id="restAuthenticationEntryPoint" class="com.company.project.authentication.security.RestAuthenticationEntryPoint" />

<beans:bean id="authenticationTokenProcessingFilter" class="com.company.project.authentication.security.AuthenticationTokenProcessingFilter" >
    <beans:property name="authenticationManager" ref="authenticationManager" />
    <beans:property name="userDetailsServices"> 
                <beans:list>
                    <beans:ref bean="inMemoryUserDetailsService" />
                    <beans:ref bean="tmpUserDetailsService" />
                </beans:list>
    </beans:property>

</beans:bean>

<beans:bean id="tmpUserDetailsService" class="com.company.project.authentication.security.TokenUserDetailsServiceImpl" />

<user-service id="inMemoryUserDetailsService">
            <user name="temporary" password="temporary" authorities="ROLE_SUPERVISOR" />
            <user name="user" password="userPass" authorities="ROLE_USER" />
</user-service>

<authentication-manager alias="authenticationManager">
    <!-- Use some hard-coded values for development -->
    <authentication-provider user-service-ref="inMemoryUserDetailsService" />
    <authentication-provider ref='companyLdapProvider' />
</authentication-manager>

对于身份验证过滤器,我将 UsernamePasswordAuthenticationFilter 子类化。当它是一个登录请求时,会通过身份验证提供者进行身份验证,然后生成一个令牌。如果从标头中读取令牌,则检查令牌以进行身份​​验证。这是我的身份验证过滤器(它还没有准备好生产,但它可以让您了解您可以做什么):

public class AuthenticationTokenProcessingFilter extends UsernamePasswordAuthenticationFilter 
//~ Static fields/initializers =====================================================================================

private static final String HEADER_AUTH_TOKEN = "X-Auth-Token";    
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationTokenProcessingFilter.class);

private List<UserDetailsService> userDetailsServices = new ArrayList<UserDetailsService>();
//~ Constructors ===================================================================================================

public AuthenticationTokenProcessingFilter() 
    super();


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

        String authToken = this.extractAuthTokenFromRequest(request);
        if (authToken == null) 
            super.doFilter(request,  res,  chain);
            return;
        
        String userName = TokenUtils.getUserNameFromToken(authToken);

        if (userName != null) 

                UserDetails userDetails = loadUserByUsername(userName);

                if (TokenUtils.validateToken(authToken, userDetails)) 

                        UsernamePasswordAuthenticationToken authentication =
                                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                
        

        chain.doFilter(request, response);


@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 
    if (!request.getMethod().equals("POST")) 
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    

    UsernamePasswordAuthenticationToken authRequest = authenticateWithForm(request, response);
    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

    Authentication authentication = this.getAuthenticationManager().authenticate(authRequest);

    if (authentication.isAuthenticated()) 
        try 
            String authToken = TokenUtils.createToken(obtainUsername(request), obtainPassword(request));
            LOGGER.info("Setting HTTP header  = ", HEADER_AUTH_TOKEN, authToken);
            response.addHeader(HEADER_AUTH_TOKEN, authToken);
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();                
            LOGGER.info("authorities = ", authorities);
            // Now we should make an in-memory table of the token and userdetails for later use 
         catch(Exception e) 
            LOGGER.warn("Error creating token for authentication. Authorization token head cannot be created.", e);
        

    

    return authentication;


protected UsernamePasswordAuthenticationToken authenticateWithForm(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 
    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) 
        username = "";
    

    if (password == null) 
        password = "";
    

    username = username.trim();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

    return authRequest;


private String extractAuthTokenFromRequest(HttpServletRequest httpRequest) 
        /* Get token from header */
        String authToken = httpRequest.getHeader(HEADER_AUTH_TOKEN);

        /* If token not found get it from request parameter */
        if (authToken == null) 
                authToken = httpRequest.getParameter("token");
        

        return authToken;


public List<UserDetailsService> getUserDetailsServices() 
    return userDetailsServices;


public void setUserDetailsService(UserDetailsService userDetailsService) 
    this.userDetailsServices.add(userDetailsService);


public void setUserDetailsServices(List<UserDetailsService> users) 
    if (users != null) 
        this.userDetailsServices.clear();
        this.userDetailsServices.addAll(users);
    

private UserDetails loadUserByUsername(String username) 
    UserDetails user = null;
    List<Exception> exceptions = new ArrayList<Exception>();
    for (UserDetailsService service: userDetailsServices) 
        try 
            user = service.loadUserByUsername(username);
            break;
         catch (Exception e) 
            LOGGER.warn("Could not load user by username  with service ", username, service.getClass().getName());
            LOGGER.info("Exception is: ",e);
            exceptions.add(e);
        
    
    if (user == null && !exceptions.isEmpty()) 
        throw new AuthenticationException(exceptions.get(0));
    
    return user;


不过,我仍在努力完善 UserDetailsS​​ervice。通常,您可以使用身份验证提供程序来获取 UserDetails,但由于我有一个无状态应用程序,所以当我想对令牌进行身份验证时,我必须确定要使用哪个 UserDetailsS​​ervice。我目前正在使用自定义代码执行此操作。

【讨论】:

【参考方案2】:

对于首次身份验证,我希望您可以使用一些输入值(例如用户名/密码)调用第 3 方 API,它会返回 true/false。如果是这样,您可以编写自己的 AuthenticationProvider ,如下所示。并调用您的第 3 方身份验证,如下所示。 Spring 安全框架会自动设置 SecurityContextHolder.getContext().setAuthenticated(true or false);因此。您不必设置它。

        public class MyAuthenticationProvider implements AuthenticationProvider 

            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException 

                String user = (String) authentication.getPrincipal();
                String password = (String) authentication.getCredentials();

                List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                authorities.add(new SimpleGrantedAuthority("ROLE_ONE"));
                authorities.add(new SimpleGrantedAuthority("ROLE_TWO"));

                UsernamePasswordAuthenticationToken authenticationToken = null;



                if (<your 3rd party authentication result == true>)) 
                    authenticationToken = new UsernamePasswordAuthenticationToken(user, password, authorities);
                 else 
                    throw new BadCredentialsException("Invalid credentials supplied. Please try again.");
                
                return authenticationToken;
            


            @Override
            public boolean supports(Class<?> authentication) 
                boolean supports = authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
                return supports;
            

        

在进一步调用 REST API 之前,您可以在其 Spring 控制器中检查用户是否有效使用代码 SecurityContextHolder.getContext().getAuthentication().isAuthenticated();

您还可以使用如下代码获取许多其他用户信息。

                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                System.out.println("getAuthorities : " + authentication.getAuthorities());
                System.out.println("getName : " + authentication.getName());
                System.out.println("getCredentials : " + authentication.getCredentials());
                System.out.println("getDetails : " + authentication.getDetails());
                System.out.println("getPrincipal : " + authentication.getPrincipal());

                if (authentication.getPrincipal() instanceof User) 
                    User user = (User) authentication.getPrincipal();
                    System.out.println(user.getUsername());
                    System.out.println(user.getPassword());
                    System.out.println(user.getAuthorities());
                

【讨论】:

【参考方案3】:

我建议建立一个从 Spring Security 到 3rd 方登录页面的“桥梁”。如果您使用 Spring,我认为这是最好的工作方式。

意思是,您有一个登录处理程序,可以将用户重定向到第 3 方登录页面。登录后,用户将被重定向回网络应用程序。

这是你的意思吗?听起来不错?有意义吗?

如果是这样,您可以使用my article 获得帮助:

<security:http entry-point-ref="legacyEntryPoint">

通常,这意味着每当相关的 http 调用尝试访问您的应用时,这就是处理请求的入口点。在您的情况下, legacyEntryPoint 是您将实现的一个类,它将检查用户是否经过身份验证;如果不是,它将用户重定向到第 3 方登录系统,否则它使用已知的“令牌”来使用您的应用程序。

希望有帮助!

【讨论】:

条件是我不应该触及用户通过第三方认证的初始认证。您可以假设我们不知道第三方如何通过登录屏幕首次对用户进行身份验证,但我们将在请求中拥有经过验证的用户凭据。因此,由于我们不了解第三方 API,因此我不知道将 spring 安全性和第三方 API 链接起来。但是,应该有某种方法可以在他通过 3rd 身份验证后使用用户信息用于通过 spring 重新验证用户的派对 API 我认为我的回答指导了这个案例。您的应用程序(您可以控制您的应用程序,对吗?)可以使用 Spring Security 编写。在配置中,您会将身份验证重定向到第 3 方,并获取响应。 不久前我写了一篇文章,解释了如何做类似的事情:codeproject.com/Articles/598581/… 我不应该更改现有的初始身份验证过程。我应该只使用经过身份验证的用户信息,并进一步使用 spring security 来重新验证用户。我快速浏览了您发布的链接。在您的示例中,总身份验证由 spring security 执行。在我的情况下,它不是。这是第一次由第 3 方完成,从那里,spring security 应该注意使用第 3 方提供的用户信息。 我的问题与spring参考手册docs.spring.io/spring-security/site/docs/3.0.x/reference/…中提到的问题有些相似 我明白,这正是我所说的。您的应用程序使用弹簧。您将用户重定向到第 3 方(非 spring),然后返回身份验证,spring 将获取令牌。我已经更新了我的答案。看看吧。【参考方案4】:

你试过了吗?

SecurityContextHolder.getContext().setAuthenticated(true);.

http://docs.spring.io/autorepo/docs/spring-security/3.0.x/apidocs/org/springframework/security/core/Authentication.html

【讨论】:

Chinmay,你能举个例子吗?我已经看到“SecurityContextHolder.getContext().setAuthenticated(true); 在哪里使用此代码?”。我的问题是我很困惑如何开始使用它。你能用伪代码从头开始给我看一个例子吗?像web.xml中需要的配置,*security.xml,自定义任何spring框架工作类等等。 我假设您已经设置了 Spring Security 配置。你可以在任何你喜欢的地方使用它。一个好的地方是让您的用户通过第三方服务进行身份验证?只需复制粘贴代码行。我仍然不确定您在寻找什么?如果您不需要 Spring Security,为什么还要配置它? Chinmay, 1) 用户已经通过第三方 API 认证。我不应该改变它,因为它是一个遗留应用程序。2)为了在进行休息调用之前重新进行身份验证(这是一个新要求),我计划使用 spring security。我需要一个适合我要求的例子。您可以查看docs.spring.io/spring-security/site/docs/3.0.x/reference/… 以了解类似情况。唯一的问题是没有适用于此类场景的示例。

以上是关于使用 Spring Security 无需授权的预认证的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security OAuth2

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

spring-security-oauth2.0 SSO大体流程图

更改 Spring Security SAML2 登录的 URL

仅使用 Spring-Security(或)Spring 进行授权

Spring Security +Oauth2 +Spring boot 动态定义权限