在 RESTful API 中使用 Spring Security

介绍

在篇文章中,我们将学习如何使用 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容