【问题标题】:Spring Security authentication via token通过令牌进行 Spring Security 身份验证
【发布时间】:2013-06-12 03:33:53
【问题描述】:

我有一个带有 API 的服务器。服务器受 Spring Security 保护。

我想使用请求参数中的令牌从外部应用程序访问 API

首先,用户会去一个给他一个令牌的服务,然后用这个令牌访问 API。

但我想通过标准 Spring Security 解决方案保留对 API 的先前访问权限。

那么,您能帮帮我吗,我该如何实现呢?

【问题讨论】:

    标签: spring-mvc spring-security


    【解决方案1】:

    你需要像这样实现自定义的AuthenticationFilter

    public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
      private static final String SECURITY_TOKEN_KEY    = "token";
      private static final String SECURITY_TOKEN_HEADER = "X-Token";
      private String token = null;
    
      protected CustomAuthenticationFilter() {
        super("/");
      }
    
      @Override
      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
    
        this.token = request.getParameter(SECURITY_TOKEN_KEY);
        // or this.token = request.getHeader(SECURITY_TOKEN_HEADER);
    
        if (request.getAttribute(FILTER_APPLIED) != null) {
          chain.doFilter(request, response);
          return;
        }
    
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
    
        if(request.getParameter(actionParameter) !=null &&
            request.getParameter(actionParameter).equals("logout")) {
          SecurityContextHolder.clearContext();
          return;
        }
    
        if (!requiresAuthentication(request, response)) {
          chain.doFilter(request, response);
          return;
        }
    
        Authentication authResult;
        try {
          authResult = attemptAuthentication(request, response);
          if (authResult == null) {
            return;
          }
        } catch (AuthenticationException failed) {
          unsuccessfulAuthentication(request, response, failed);
          return;
        }
    
        try {
          successfulAuthentication(request, response, chain, authResult);
        } catch (NestedServletException e) {
          if(e.getCause() instanceof AccessDeniedException) {
            unsuccessfulAuthentication(request, response, new LockedException("Forbidden"));
          }
        }
      }
    
      @Override
      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
    
        AbstractAuthenticationToken userAuthenticationToken = authUserByToken(this.token);
        if(userAuthenticationToken == null)
          throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
    
        return userAuthenticationToken;
      }
    
      private AbstractAuthenticationToken authUserByToken(String tokenRaw) {
        AbstractAuthenticationToken authToken = null;
        try {
          // check your input token, identify the user
          // if success create AbstractAuthenticationToken for user to return
          // eg:
          authToken = new UsernamePasswordAuthenticationToken(username, userHash, userAuthorities);
    
        } catch (Exception e) {
          logger.error("Error during authUserByToken", e);
        }
        return authToken;
      }
    
      @Override
      protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              Authentication authResult) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);
    
        getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
      }
    
    }
    

    和这样的自定义 SuccessHandler

    public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
      @Override
      protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
        return request.getServletPath();
      }
    
      @Override
      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        request.getRequestDispatcher(request.getServletPath()).forward(request, response);
      }
    }
    

    并在 spring 配置中连接它

    <?xml version="1.0" encoding="UTF-8"?>
    <b:beans
        xmlns="http://www.springframework.org/schema/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:b="http://www.springframework.org/schema/beans"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:sec="http://www.springframework.org/schema/security"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
    
      <context:annotation-config/>
      <context:component-scan base-package="com.your.path" />
    
      <aop:aspectj-autoproxy/>
    
      <global-method-security pre-post-annotations="enabled" secured-annotations="enabled" proxy-target-class="true"
                              access-decision-manager-ref="accessDecisionManager"/>
    
      <http entry-point-ref="restAuthenticationEntryPoint" use-expressions="true"
            auto-config="true" access-decision-manager-ref="accessDecisionManager">
        <custom-filter ref="restFilter" position="PRE_AUTH_FILTER"/>
        <logout/>
      </http>
    
      <b:bean id="restAuthenticationEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"/>
    
      <b:bean id="restFilter" class="com.your.path.CustomAuthenticationFilter">
        <b:property name="authenticationSuccessHandler" ref="mySuccessHandler"/>
      </b:bean>
    
      <b:bean id="mySuccessHandler" class="com.your.path.CustomAuthenticationSuccessHandler"/>
    
      <b:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
        <b:property name="allowIfAllAbstainDecisions" value="true"/>
        <b:property name="decisionVoters">
          <b:list>
            <b:bean class="org.springframework.security.access.vote.RoleVoter">
              <b:property name="rolePrefix" value=""/>
            </b:bean>
            <b:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
          </b:list>
        </b:property>
      </b:bean>
    
    </b:beans>
    

    这应该会有所帮助。

    【讨论】:

    • Spring Security 上没有基于令牌的身份验证过滤器吗??
    • 我试过你的代码,但我有一个问题。当发生错误时,doFilter 会被无限调用,并显示 StackOverflowError。仅当请求中有错误时才会发生这种情况。
    【解决方案2】:

    您可以在 spring security 中使用以下 bean TimedKeyBasedPersistenceTokenService

    <bean name="tokenService" class="com.digipos.security.core.token.TimedKeyBasedPersistenceTokenService">
        <property name="tokenLifeInMinutes" value="15000"/>
        <property name="serverSecret" value="1234567"/>
        <property name="serverInteger" value="15062013"/>
        <property name="pseudoRandomNumberBits" value="7"/>
        <property name="secureRandom" ref="secureRandom"/>
    </bean>
    
    
    <bean name="secureRandom" class="java.security.SecureRandom">
        <property name="seed" value="122"/>
    </bean>
    

    除此之外你还需要

    使用PreAuthenticatedAuthenticationProvider

    &lt;http&gt;entry-point-ref 属性到Http403ForbiddenEntryPoint bean

    【讨论】:

    • 我对这种基于令牌的身份验证非常感兴趣,但不了解与 PreAuthenticatedAuthenticationProvider 的关系……是否需要验证令牌才能进行身份验证? ...你能详细说明一下吗?
    【解决方案3】:

    我找到了一个更简单的方法:

    我的解决方案适用于令牌身份验证和表单身份验证,但您可以根据需要轻松禁用其中之一。

    我的过滤器类似于 Roman 的过滤器,但我不需要检查用户是否有权访问特定资源,也没有处理注销 -> 传递给 springSecurity。

    身份验证过滤器:

    public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
    private static final String SECURITY_TOKEN_KEY = "token";
    private static final String SECURITY_TOKEN_HEADER = "X-Token";
    
    public TokenAuthenticationFilter() {
    
        super( "/" );
    }
    
    @Override
    public void doFilter( ServletRequest req, ServletResponse res, FilterChain chain ) throws IOException, ServletException {
    
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
    
        String token = request.getParameter( SECURITY_TOKEN_KEY );
        // or this.token = request.getHeader(SECURITY_TOKEN_HEADER);
    
        if ( token != null ) {
    
            Authentication authResult;
            try {
                authResult = attemptAuthentication( request, response, token );
                if ( authResult == null ) {
                    notAuthenticated( request, response, new LockedException( "User Not found" ) );
                    return;
                }
            } catch ( AuthenticationException failed ) {
                notAuthenticated( request, response, failed );
                return;
            }
    
            try {
                successfulAuthentication( request, response, chain, authResult );
                return;
            } catch ( NestedServletException e ) {
                logger.error( e.getMessage( ), e );
                if ( e.getCause( ) instanceof AccessDeniedException ) {
                    notAuthenticated( request, response, new LockedException( "Forbidden" ) );
                    return;
                }
            }
        }
        chain.doFilter( request, response );// return to others spring security filters
    }
    
    public void notAuthenticated( HttpServletRequest request, HttpServletResponse response, AuthenticationException failed ) throws IOException {
    
        response.sendRedirect( "http://www.google.ro" );
        // unsuccessfulAuthentication( request, response, failed );
    }
    
    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response, String token ) throws AuthenticationException, IOException, ServletException {
    
        AbstractAuthenticationToken userAuthenticationToken = authUserByToken( token );
        if ( userAuthenticationToken == null )
            throw new AuthenticationServiceException( MessageFormat.format( "Error | {0}", "Bad Token" ) );
    
        return userAuthenticationToken;
    }
    
    private AbstractAuthenticationToken authUserByToken( String tokenRaw ) {
    
        AbstractAuthenticationToken authToken = null;
        try {
            // check your input token, identify the user
            // if success create AbstractAuthenticationToken for user to return
            // eg:
            // authToken = new UsernamePasswordAuthenticationToken( username, userHash, userAuthorities );
            // authToken = new UsernamePasswordAuthenticationToken( tokenRaw, authToken, )
            logger.info( "token received = " + tokenRaw );
            // obtain user by your methods
            // if ( user != null ) {
            // SecurityUser securityUser = new SecurityUser( user );
            // return new PreAuthenticatedAuthenticationToken( securityUser, securityUser.getPassword( ), securityUser.getAuthorities( ) );
            // }
        } catch ( Exception e ) {
            logger.error( "Error during authUserByToken", e );
        }
        return authToken;
    }
    
    @Override
    protected void successfulAuthentication( HttpServletRequest request, HttpServletResponse response, Authentication authResult ) throws IOException, ServletException {
    
        SecurityContextHolder.getContext( ).setAuthentication( authResult );
    
        new CustomAuthenticationSuccessHandler( ).onAuthenticationSuccess( request, response, authResult );
    }
    
    @Override
    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException, IOException, ServletException {
    
        logger.error( "No TOKEN PROVIDED" );
        return null;
        }
    
    }
    

    那么映射这个过滤器所要做的就是在 springSecurity(addFilterBefore) 中配置它,这不必在 servlet 配置中映射。

            http.authorizeRequests( ).antMatchers( "/login*" ).permitAll( );
            http.authorizeRequests( ).antMatchers( "/register*" ).permitAll( );
    
            http.authorizeRequests( ).antMatchers( "/admin/**" ).hasAnyAuthority( "ROLE_ADMIN", "ROLE_USER" );//
    
            http.authorizeRequests( ).and( ).formLogin( )//
                    .loginPage( "/login" )//
                    .successHandler( successHandler( ) )//
                    .failureUrl( "/login?error" ).permitAll( )//
                    .and( ).logout( )//
                    .logoutUrl( "/logout" ).logoutSuccessUrl( "/login?logout" ).permitAll( )//
                    .and( ).rememberMe( ).key( applicationName + "_key" ).tokenValiditySeconds( 2419200 ); // remember me for 2 weeks
    
            http.addFilterBefore( new TokenAuthenticationFilter( ), AnonymousAuthenticationFilter.class );
    

    【讨论】:

    • 我对这一切有疑问吗?当在后端生成令牌时,生成令牌的内容是什么,以及它是如何存储/持久化在后端的?它存在多长时间?当我们收到一个带有令牌的 AJAX 请求到后端时……我看到我们有一些代码可以从令牌存储中查找该令牌,对吗?当令牌在后端生成并存储时,当我们将令牌发送到前端时,您是否将其存储为cookie?如果 cookie 被关闭了怎么办?谢谢!
    • 这取决于您的需求。例如,JWT 不需要商店。它里面有信息。否则,您将需要一个令牌验证 api(store/check/invalidate)。如果您在谈论 AJAX req .. 再次,您可以将其添加到本地存储,在获取或 cookie 之后,或者只是页面中的一些变量.. 这取决于您的很多目标/需求。一般来说,我使用它来验证 api 消费者,并且我有一个从 db 获取的本地缓存,带有
    猜你喜欢
    • 2012-03-06
    • 2013-02-23
    • 2020-07-17
    • 1970-01-01
    • 2017-07-10
    • 1970-01-01
    • 2019-12-26
    • 2014-10-30
    • 2014-02-01
    相关资源
    最近更新 更多