使用自定义令牌来保护REST API(无状态,无UI,无cookies,无基础认证,无OAuth,无登录页面)

41
有许多指南和示例代码展示如何使用Spring Security保护REST API,但它们大多假设有一个Web客户端,并谈到登录页面、重定向、使用cookie等。也许只需简单的筛选器检查HTTP标头中的自定义令牌即可满足要求。我应该如何实现以下系统安全要求?是否有任何与此相同的gist/github项目?我的 Spring Security 知识有限,如果有更简单的方式来使用 Spring Security 实现它,请告诉我。
  • 使用HTTPS提供服务的无状态后端的 REST API
  • 客户端可以是Web应用程序、移动应用程序、任何SPA风格的应用程序、第三方API
  • 没有基本身份验证、没有cookie、没有UI(没有JSP/HTML/静态资源)、没有重定向,没有OAuth提供程序。
  • 在HTTPS标头上设置自定义令牌
  • 对外部存储(如MemCached / Redis/甚至任何RDBMS)进行令牌验证
  • 除了选定路径(如/login,/signup,/public等)之外,所有API都需要经过身份验证

我使用Spring Boot、Spring Security等.. 希望您提供使用Java配置(没有XML)的解决方案。


看起来还不错。你只需要为Spring Security创建一个自定义过滤器,一个处理令牌的自定义提供程序,一个支持令牌的UserDetailService和一个令牌管理器。按照目前的写法,你的问题太广泛了,但是在我看来,你可以安全地继续进行这个项目,并在你遇到困难时回到这里寻求帮助。 - Serge Ballesta
我认为不需要一个UserDetailService。 - Chris DaMour
5个回答

34
我的示例应用程序正是如此-在无状态情况下使用Spring Security保护REST端点。 通过HTTP标头对单个REST调用进行身份验证。 身份验证信息存储在服务器端的内存缓存中,并提供与典型Web应用程序中HTTP会话提供的语义相同。 应用程序使用完整的Spring Security基础架构,几乎没有自定义代码。 没有裸过滤器,没有超出Spring Security基础架构范围的代码。
基本思路是实现以下四个Spring Security组件:
  1. org.springframework.security.web.AuthenticationEntryPoint,用于捕获需要身份验证但缺少所需身份验证令牌的REST调用,从而拒绝请求。
  2. org.springframework.security.core.Authentication,用于保存REST API所需的身份验证信息。
  3. org.springframework.security.authentication.AuthenticationProvider,用于执行实际的身份验证(针对数据库、LDAP服务器、Web服务等)。
  4. org.springframework.security.web.context.SecurityContextRepository,用于在HTTP请求之间保存身份验证令牌。 在示例中,实现将令牌保存在EHCACHE实例中。
示例使用XML配置,但您可以轻松使用等效的Java配置。

1
非常干净的解决方案,指引了我正确的方向!如果可以的话,我会给你点赞不止一次 :) - Thomas Eizinger
非常棒的答案...我想我会为了自己的用途而审查你的实现... ;) - Edward J Beckett
非常好。对我所需的内容提供了很多启示。谢谢! - Lorin S.
6
这个问题似乎是关于Java的,样例应用程序位于一个名为manish-in-java的区域。但是下载的项目包含2个Java文件和23个Scala文件。是否有Java版本? - Chris
没有纯Java解决方案,因为我发布的示例来自我的实际生产应用程序,该应用程序大部分使用Scala。就个人而言,即使在刚开始学习该语言时,我也没有遇到阅读Scala代码的任何问题,但我同意一般情况下并非所有Java程序员都能做到这一点。我建议您尝试一下;毕竟只有四个感兴趣的类。 - manish
显示剩余4条评论

9

你说得对,这并不容易,而且很难找到好的例子。我看到的例子使你不能同时使用其他spring security内容。最近我也做了类似的事情,以下是我的做法。

你需要一个自定义令牌来保存你的头部信息。

public class CustomToken extends AbstractAuthenticationToken {
  private final String value;

  //Getters and Constructor.  Make sure getAutheticated returns false at first.
  //I made mine "immutable" via:

      @Override
public void setAuthenticated(boolean isAuthenticated) {
    //It doesn't make sense to let just anyone set this token to authenticated, so we block it
    //Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
    if (isAuthenticated) {

        throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
    }

    super.setAuthenticated(false);
}
}

你需要一个Spring Security过滤器来提取标头并要求经理进行身份验证,类似于这样强调文字
public class CustomFilter extends AbstractAuthenticationProcessingFilter {


    public CustomFilter(RequestMatcher requestMatcher) {
        super(requestMatcher);

        this.setAuthenticationSuccessHandler((request, response, authentication) -> {
        /*
         * On success the desired action is to chain through the remaining filters.
         * Chaining is not possible through the success handlers, because the chain is not accessible in this method.
         * As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
         * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
         * "Subclasses can override this method to continue the FilterChain after successful authentication."
         */
        });

    }



    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {


        String tokenValue = request.getHeader("SOMEHEADER");

        if(StringUtils.isEmpty(tokenValue)) {
            //Doing this check is kinda dumb because we check for it up above in doFilter
            //..but this is a public method and we can't do much if we don't have the header
            //also we can't do the check only here because we don't have the chain available
           return null;
        }


        CustomToken token = new CustomToken(tokenValue);
        token.setDetails(authenticationDetailsSource.buildDetails(request));

        return this.getAuthenticationManager().authenticate(token);
    }



    /*
     * Overriding this method to maintain the chaining on authentication success.
     * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
     * "Subclasses can override this method to continue the FilterChain after successful authentication."
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {


        //if this isn't called, then no auth is set in the security context holder
        //and subsequent security filters can still execute.  
        //so in SOME cases you might want to conditionally call this
        super.successfulAuthentication(request, response, chain, authResult);

        //Continue the chain
        chain.doFilter(request, response);

    }


}

在Spring Security链中注册您的自定义过滤器

 @Configuration
 public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {        

      //Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
      protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
        CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
        filter.setAuthenticationManager(this.authenticationManagerBean());
        return filter;
      }

       @Override
       protected void configure(HttpSecurity http) throws Exception {                  

            http
            //fyi: This adds it to the spring security proxy filter chain
            .addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
       }
}

一个自定义的认证提供者,用于验证使用过滤器提取的令牌。
public class CustomAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {

        CustomToken token = (CustomToken)auth;

        try{
           //Authenticate token against redis or whatever you want

            //This i found weird, you need a Principal in your Token...I use User
            //I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
            org.springframework.security.core.userdetails.User principal = new User(...); 

            //Our token resolved to a username so i went with this token...you could make your CustomToken take the principal.  getCredentials returns "NO_PASSWORD"..it gets cleared out anyways.  also the getAuthenticated for the thing you return should return true now
            return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
        } catch(Expection e){
            //TODO throw appropriate AuthenticationException types
            throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
        }


    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomToken.class.isAssignableFrom(authentication);
    }


}

最后,将您的提供者注册为bean,以便身份验证管理器在某个@Configuration类中找到它。您可能只需要将其@ Component化,但我更喜欢这种方法。
@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies)  {
    return new CustomAuthenticationProvider(injectedDependencies);
}

1
正如 Manish 在另一个答案中展示的那样,如果你使用 SecurityContextRepository 接口,就不需要自定义过滤器,这将使代码更加简洁,并且很可能是你应该使用框架的方式。 - Thomas Eizinger
这不是更适用于当您可以将用户/密码转换为令牌时吗? - Chris DaMour
嗨,使用您的代码Filter-> onAuthenticationSuccess -> chain.doFilter()调用有时会返回NullPointerExceptions。堆栈跟踪涉及ApplicationFilterChain类。有什么想法吗? :) 谢谢 - Timson
你知道我们遇到了那个问题...让我更新一下我们的修复方案。 - Chris DaMour
更新了,问题在于以前的setAuthenticationSuccessHandler闭包在每次调用时都设置了一个类成员变量...所以你可能会继续别人的链条...这是从未发生过的。现在不可能再发生了。 - Chris DaMour
谢谢!问题已解决。 - Timson

4

这段代码保护了所有的端点 - 但我相信你可以对其进行修改 :). Token使用Spring Boot Starter Security存储在Redis中,您需要定义自己的UserDetailsService并将其传递给AuthenticationManagerBuilder

简而言之 - 复制粘贴EmbeddedRedisConfigurationSecurityConfig,并将AuthenticationManagerBuilder替换为您自己的逻辑。

HTTP:

请求token - 在请求头中发送基本的HTTP身份验证内容。 token将在响应头中返回。

http --print=hH -a user:password localhost:8080/v1/users

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af

使用令牌的相同请求:

http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token:  cacf4a97-75fe-464d-b499-fcfacb31c8af

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

如果您输入错误的用户名/密码或令牌,则会收到401错误。
JAVA
我将这些依赖项添加到build.gradle中。
compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")

然后是 Redis 配置。
@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {

    private static RedisServer redisServer;

    @Bean
    public JedisConnectionFactory connectionFactory() throws IOException {
        redisServer = new RedisServer(Protocol.DEFAULT_PORT);
        redisServer.start();
        return new JedisConnectionFactory();
    }

    @PreDestroy
    public void destroy() {
        redisServer.stop();
    }

}

安全配置:

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .requestCache()
                .requestCache(new NullRequestCache())
                .and()
                .httpBasic();
    }

    @Bean
    public HttpSessionStrategy httpSessionStrategy() {
        return new HeaderHttpSessionStrategy();
    }
}

通常在教程中,您会发现AuthenticationManagerBuilder使用inMemoryAuthentication,但还有很多其他选择(LDAP等)。只需查看类定义即可。我正在使用需要UserDetailsService对象的userDetailsService
最后,我的用户服务使用CrudRepository
@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userRepository.findByEmail(username);
        if (userAccount == null) {
            return null;
        }
        return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

0

另一个使用JWT的示例项目 - Jhipster

尝试使用JHipster生成微服务应用程序。它会生成一个模板,其中包含Spring Security和JWT的开箱即用集成。

https://jhipster.github.io/security/


-1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接