在 RESTful API 中使用 Spring Security

3,061 阅读6分钟

介绍

在篇文章中,我们将学习如何使用 Spring 和 Spring Security 5 提供更安全的 RESTful API。
我们将使用 Java 配置来设置安全性,并将使用登录和 Cookie 方法进行身份验证。

启用Spring Security

Spring Security 的体系结构完全基于 Servlet 过滤器。
注册 Spring Security 过滤器的最简单选择是添加 @EnableWebSecurity 注解到我们的 config 类:

@Config
@EnableWebSecurity
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {
 
    // ...
}

对于非 Spring Boot 应用,我们可以扩展 AbstractSecurityWebApplicationInitializer 并在其构造函数中传递 config 类:

public class SecurityWebApplicationInitializer 
  extends AbstractSecurityWebApplicationInitializer {
 
    public SecurityWebApplicationInitializer() {
        super(SecurityJavaConfig.class);
    }
}

或者我们可以在应用的 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>/*</url-pattern>
</filter-mapping>

我们应该将过滤器命名为 “springSecurityFilterChain”,以匹配容器中 Spring Security 创建的默认 bean。
注意,定义的过滤器不是实现安全逻辑的实际类。相反,它是一个委托 filterproxy,将筛选器的方法委托给内部 bean。这样做是为了让目标 bean 仍然能够从 Spring 上下文的生命周期和灵活性中受益。

Spring Security Java配置

我们可以完全在 Java 类中进行安全配置,方法是创建一个扩展了 WebSecurityConfigurerAdapter 的配置类,并用 @EnableWebSecurity 对其进行注释:

@Configuration
@EnableWebSecurity
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {
 
    // ...
}

现在,让我们在 SecurityJavaConfig 中创建不同角色的用户,我们将使用这些用户来验证我们的 API 端点:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("admin").password(encoder().encode("adminPass")).roles("ADMIN")
        .and()
        .withUser("user").password(encoder().encode("userPass")).roles("USER");
}
 
@Bean
public PasswordEncoder  encoder() {
    return new BCryptPasswordEncoder();
}

接下来,让我们为我们的 API 端点配置安全性:

@Override
protected void configure(HttpSecurity http) throws Exception { 
    http
    .csrf().disable()
    .exceptionHandling()
    .authenticationEntryPoint(restAuthenticationEntryPoint)
    .and()
    .authorizeRequests()
    .antMatchers("/api/foos").authenticated()
    .antMatchers("/api/admin/**").hasRole("ADMIN")
    .and()
    .formLogin()
    .successHandler(mySuccessHandler)
    .failureHandler(myFailureHandler)
    .and()
    .logout();
}

Http

在我们的代码实现中,我们使用 antMatchers 创建安全的映射 /api/foos 和 /api/admin/**。
任何经过身份验证的用户都可以访问 /api/foos。另一方面,/api/admin/** 只能被 admin 角色用户访问。
在标准的 web 应用程序中,当未经身份验证的客户机试图访问受保护的资源时,身份验证过程可能会自动触发。此过程通常重定向到登录页面,以便用户可以输入凭据。
然而,对于 REST Web 服务,这种行为没有多大意义。我们应该能够仅通过对正确URI的请求进行身份验证,如果用户没有经过身份验证,则所有请求都应该失败,并带有401未授权状态码。
Spring Security 使用 Entry Point 的概念处理身份验证过程的自动触发 ——这是配置中必需的一部分,可以通过 authenticationEntryPoint 方法注入。
在触发时简单地返回 401:

@Component
public final class RestAuthenticationEntryPoint 
  implements AuthenticationEntryPoint {
 
    @Override
    public void commence(
        HttpServletRequest request, 
        HttpServletResponse response, 
        AuthenticationException authException) throws IOException {
         
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 
          "Unauthorized");
    }
}

用于 REST 的登录表单

有多种方法可以对 REST API 进行身份验证。 Spring Security 提供的默认值之一是表单登录,它使用身份验证处理过滤器 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter。
formLogin 元素将创建这个过滤器,并提供额外的方法 successHandler 和 failureHandler 来分别设置我们的自定义身份验证成功和失败处理程序。

身份验证应该返回 200 而不是 301

默认情况下,表单登录将使用 301 状态码来回答成功的身份验证请求,这在登录后需要重定向的实际登录表单的上下文中是有意义的。
然而,对于 RESTful web 服务,成功验证所需的响应应该是 200 OK。
为此,我们在表单登录筛选器中注入一个自定义身份验证成功处理程序,以替换默认的处理程序。新的处理程序实现与默认的 org.springframe .security.web.authentication 完全相同的登录。SavedRequestAwareAuthenticationSuccessHandler 有一个明显的区别-它删除了重定向逻辑:

public class MySavedRequestAwareAuthenticationSuccessHandler 
  extends SimpleUrlAuthenticationSuccessHandler {
 
    private RequestCache requestCache = new HttpSessionRequestCache();
 
    @Override
    public void onAuthenticationSuccess(
      HttpServletRequest request,
      HttpServletResponse response, 
      Authentication authentication) 
      throws ServletException, IOException {
  
        SavedRequest savedRequest
          = requestCache.getRequest(request, response);
 
        if (savedRequest == null) {
            clearAuthenticationAttributes(request);
            return;
        }
        String targetUrlParam = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl()
          || (targetUrlParam != null
          && StringUtils.hasText(request.getParameter(targetUrlParam)))) {
            requestCache.removeRequest(request, response);
            clearAuthenticationAttributes(request);
            return;
        }
 
        clearAuthenticationAttributes(request);
    }
 
    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}

身份验证管理器和提供程序

身份验证过程使用内存中的提供者来执行身份验证。
我们创建了两个用户,即具有 USER 角色的 user 和具有 ADMIN 角色的 admin。

最后—针对正在运行的 REST 服务进行身份验证

现在,让我们看看如何针对不同的用户针对REST API进行身份验证。
登录的 URL 为 /login,一个简单的 curl 命令,用于对名为 user 和密码 userPass 的用户执行登录。

curl -i -X POST -d username=user -d password=userPass
http://localhost:8080/spring-security-rest/login

此请求将返回 Cookie,我们可以将其用于针对 REST 服务的任何后续请求。
我们可以使用 curl 进行认证,并将它接收到的 cookie 保存在一个文件中

curl -i -X POST -d username=user -d password=userPass -c /opt/cookies.txt 
http://localhost:8080/spring-security-rest/login

然后我们可以使用**文件中的 cookie **来做进一步的认证请求:

curl -i --header "Accept:application/json" -X GET -b /opt/cookies.txt 
http://localhost:8080/spring-security-rest/api/foos

由于用户可以访问 /api/foos/* 端点,这个经过身份验证的请求将正确地得到一个 * 200 OK *:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 24 Jul 2013 20:31:13 GMT
 
[{"id":0,"name":"qulingfeng"}]

类似地,对于 admin 用户,我们可以使用 curl 进行认证:

curl -i -X POST -d username=admin -d password=adminPass -c /opt/cookies.txt 
http://localhost:8080/spring-security-rest/login

然后更新 cookies 访问管理端点 /api/admin/* :

curl -i --header "Accept:application/json" -X GET -b /opt/cookies.txt 
http://localhost:8080/spring-security-rest/api/admin/x

由于管理用户可以访问端点 /api/admin/*,所有响应成功:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=ISO-8859-1
Content-Length: 5
Date: Mon, 15 Oct 2018 17:16:39 GMT
 
Hello

XML 配置

我们也可以用 XML 代替 Java 配置来完成以上所有的安全配置:

<http entry-point-ref="restAuthenticationEntryPoint">
    <intercept-url pattern="/api/admin/**" access="ROLE_ADMIN"/>
 
    <form-login
      authentication-success-handler-ref="mySuccessHandler"
      authentication-failure-handler-ref="myFailureHandler" />
 
    <logout />
</http>
 
<beans:bean id="mySuccessHandler"
  class="org.rest.security.MySavedRequestAwareAuthenticationSuccessHandler"/>
<beans:bean id="myFailureHandler" class=
  "org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"/>
 
<authentication-manager alias="authenticationManager">
    <authentication-provider>
        <user-service>
            <user name="admin" password="adminPass" authorities="ROLE_ADMIN"/>
            <user name="user" password="userPass" authorities="ROLE_USER"/>
        </user-service>
    </authentication-provider>
</authentication-manager>

结束语

在本教程中,我们介绍了使用 Spring security 5 实现 RESTful 服务的基本安全配置和实现。
我们学习了如何完全通过Java配置为 REST API 进行安全配置,并研究了其 web.xml 配置替代方案。
接下来,我们讨论了如何为受保护的应用程序创建用户和角色,以及如何将这些用户映射到应用程序的特定端点。
最后,我们还研究了如何创建自定义身份验证入口点和自定义成功处理程序,以便在控制安全性方面为应用程序提供更好的灵活性。

欢迎关注我的公众号:曲翎风,获得独家整理的学习资源和日常干货推送。
如果您对我的专题内容感兴趣,也可以关注我的博客:sagowiec.com