如何使用Spring Boot和Spring Security保护REST API?

How to secure REST API with Spring Boot and Spring Security?

提问人:jnemecz 提问时间:9/13/2015 最后编辑:jatin_ghataliyajnemecz 更新时间:7/17/2022 访问量:119573

问:

我知道保护 REST API 是被广泛评论的话题,但我无法创建一个符合我的标准的小型原型(我需要确认这些标准是现实的)。有很多选项可以保护资源以及如何使用 Spring 安全性,我需要澄清我的需求是否切合实际。

我的要求

  • 基于令牌的身份验证器 - 用户将提供其凭据并获得唯一且有时间限制的访问令牌。我想在我自己的实现中管理令牌创建、检查有效性和过期时间。
  • 一些 REST 资源将是公开的 - 根本不需要身份验证,
  • 某些资源只能由具有管理员权限的用户访问,
  • 所有用户在授权后都可以访问其他资源。
  • 我不想使用基本身份验证
  • Java 代码配置(非 XML)

现状

我的 REST API 运行良好,但现在我需要保护它。当我在寻找解决方案时,我创建了一个过滤器:javax.servlet.Filter

  @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;

        String accessToken = request.getHeader(AUTHORIZATION_TOKEN);
        Account account = accountDao.find(accessToken);

        if (account == null) {    
            throw new UnauthorizedException();    
        }

        chain.doFilter(req, res);

    }

但是这个解决方案不能满足我的需要,因为通过 Spring 处理异常存在问题。javax.servlet.filters@ControllerAdviceservlet dispatcher

我需要什么

我想知道这些标准是否现实并获得任何帮助,如何开始使用 Spring Security 保护 REST API。我阅读了许多教程(例如Spring Data REST + Spring Security),但所有教程都在非常基本的配置中工作 - 用户及其凭据存储在配置的内存中,我需要使用DBMS并创建自己的身份验证器。

请给我一些想法如何开始。

java rest spring-security

评论


答:

73赞 Oleksandr Loushkin 9/13/2015 #1

基于令牌的身份验证 - 用户将提供其凭据并获取 唯一且有时间限制的访问令牌。我想管理令牌 在我自己的实现中创建、检查有效性、过期。

实际上,使用过滤器进行令牌身份验证 - 在这种情况下是最好的方法

最终,您可以通过 Spring Data 创建 CRUD 来管理 Token 的属性,例如过期等。

这是我的令牌过滤器:http://pastebin.com/13WWpLq2

和令牌服务实现

http://pastebin.com/dUYM555E

一些 REST 资源将是公开的 - 根本不需要身份验证

这不是问题,你可以像这样通过 Spring 安全配置管理你的资源:.antMatchers("/rest/blabla/**").permitAll()

某些资源只能由具有管理员权限的用户访问,

看看类的注释。例:@Secured

@Controller
@RequestMapping(value = "/adminservice")
@Secured("ROLE_ADMIN")
public class AdminServiceController {

所有用户在授权后都可以访问其他资源。

返回 Spring Security 配置,您可以像这样配置您的 URL:

    http
            .authorizeRequests()
            .antMatchers("/openforall/**").permitAll()
            .antMatchers("/alsoopen/**").permitAll()
            .anyRequest().authenticated()

我不想使用基本身份验证

是的,通过令牌过滤器,您的用户将通过身份验证。

Java 代码配置(非 XML)

回到上面的话,看. 你的班级将是:@EnableWebSecurity

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {}

您必须覆盖 configure 方法。下面的代码,仅举例说明如何配置匹配器。它来自另一个项目。

    @Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/assets/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
                .usernameParameter("j_username")
                .passwordParameter("j_password")
                .loginPage("/login")
                .defaultSuccessUrl("/", true)
                .successHandler(customAuthenticationSuccessHandler)
                .permitAll()
            .and()
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID")
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .and()
                .csrf();
}

评论

0赞 Oliver 5/1/2019
@Oleksandr:这是一个很长的镜头,但你能告诉我为什么你在 RESTAuthenticationTokenProcessingFilter 类的 updateLastLogin(...) 方法中启动了一个线程吗?
1赞 Oleksandr Loushkin 5/2/2019
@z3d4s,实际上这是一个旧示例(4 年),现在我建议使用 OffsetDateTime、另一种方法等:)我建议使用新线程来减少用户请求的处理时间,因为在保存到数据库的过程中可能需要额外的时间。
0赞 serkan ersoy 5/24/2016 #2

我也找了这么久。我正在做一个类似的项目。我发现 Spring 有一个模块可以通过 redis 实现会话。它看起来简单而有用。 我也会添加到我的项目中。可能会有所帮助:

http://docs.spring.io/spring-session/docs/1.2.1.BUILD-SNAPSHOT/reference/html5/guides/rest.html

4赞 Nalla Srinivas 12/21/2016 #3

Spring 安全性对于向 REST URL 提供身份验证和授权也非常有用。我们不需要指定任何自定义实现。

首先,您需要在安全配置中将 entry-point-ref 指定为 restAuthenticationEntryPoint,如下所示。

 <security:http pattern="/api/**" entry-point-ref="restAuthenticationEntryPoint" use-expressions="true" auto-config="true" create-session="stateless" >

    <security:intercept-url pattern="/api/userList" access="hasRole('ROLE_USER')"/>
    <security:intercept-url pattern="/api/managerList" access="hasRole('ROLE_ADMIN')"/>
    <security:custom-filter ref="preAuthFilter" position="PRE_AUTH_FILTER"/>
</security:http>

restAuthenticationEntryPoint 的实现可能如下所示。

 @Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException {
      response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" );
   }
}

在此之后,您需要指定 RequestHeaderAuthenticationFilter。它包含 RequestHeader 键。这基本上用于识别用户的身份验证。通常,RequestHeader 在进行 REST 调用时会携带此信息。 例如,请考虑以下代码

   <bean id="preAuthFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
    <property name="principalRequestHeader" value="Authorization"/>
    <property name="authenticationManager" ref="authenticationManager" />
  </bean>

这里

<property name="principalRequestHeader" value="Authorization"/>

“授权”是传入请求中提供的密钥。它保存所需用户的身份验证信息。 此外,还需要配置 PreAuthenticatedAuthenticationProvider 以满足我们的要求。

   <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
  <bean id="userDetailsServiceWrapper"
      class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <property name="userDetailsService" ref="authenticationService"/>
  </bean>
</property>
</bean>

此代码将用于通过身份验证和授权来保护 REST URL,而无需任何自定义实现。

有关完整代码,请找到以下链接:

https://github.com/srinivas1918/spring-rest-security

-3赞 Jeet Singh Parmar 4/17/2017 #4

要验证 REST API,有 2 种方法

1 - 使用在 application.properties 文件中设置的默认用户名和密码进行基本身份验证

基本身份验证

2 - 使用数据库 (userDetailsService) 使用实际用户名和密码进行身份验证

高级身份验证

评论

0赞 Mr.DevEng 4/2/2018
视频是有用的。如何对 ReST API 进行相同的高级身份验证。这里只描述一下 Web。是否有任何关于REST API中高级身份验证的视频教程。
0赞 Jeet Singh Parmar 5/2/2018
如果您看到了第二个视频(高级身份验证),那么我也在使用 REST 客户端(用于 REST API)进行相同的身份验证。
0赞 Enfield Li 7/16/2022 #5

与自定义过滤器一起使用的另一种方式http.addFilterBefore()

此解决方案更像是一个框架,可帮助您设置基础知识。 我创建了一个并添加了一些必要的注释来帮助理解该过程。它带有一些简单的身份验证/授权,您可以轻松拿起和使用这些设置。working demorole-basedpermission-basedpublically accessable endpoint

因此,最好查看完整的代码,并在运行应用程序: github 存储库

用户类设置:

public class User implements UserDetails {

  private final String username;
  private final String password;
  private final List<? extends GrantedAuthority> grantedAuthorities;

  public User(
    String username,
    String password,
    List<? extends GrantedAuthority> grantedAuthorities
  ) {
    this.username = username;
    this.password = password;
    this.grantedAuthorities = grantedAuthorities;
  }

  // And other default method overrides
}

通过方法添加自定义过滤器:addFilterBefore()

http
    .authorizeRequests()
    .antMatchers("/") 
    .permitAll()
    .addFilterBefore( // Filter login request only
        new LoginFilter("login", authenticationManager()),
        UsernamePasswordAuthenticationFilter.class
    )
    .addFilterBefore( // Filter logout request only
        new LogoutFilter("logout"),
        UsernamePasswordAuthenticationFilter.class
    )
    .addFilterBefore( // Verify user on every request
        new AuthenticationFilter(),
        UsernamePasswordAuthenticationFilter.class
    );

自定义扩展并覆盖三种方法来处理自动:LoginFilterAbstractAuthenticationProcessingFilter

public class LoginFilter extends AbstractAuthenticationProcessingFilter {

  public LoginFilter(String url, AuthenticationManager authManager) {
    super(url, authManager);
  }

  @Override
  public Authentication attemptAuthentication(
    HttpServletRequest req,
    HttpServletResponse res
  )
    throws AuthenticationException, IOException {
    LoginUserDto loginUserDto = new ObjectMapper() // this dto is a simple {username, password} object
    .readValue(req.getInputStream(), LoginUserDto.class);

    return getAuthenticationManager()
      .authenticate(
        new UsernamePasswordAuthenticationToken(
          loginUserDto.getUsername(),
          loginUserDto.getPassword()
        )
      );
  }

  @Override
  protected void successfulAuthentication(
    HttpServletRequest req,
    HttpServletResponse res,
    FilterChain chain,
    Authentication auth
  )
    throws IOException, ServletException {
    User user = (User) auth.getPrincipal();

    req.getSession().setAttribute(UserSessionKey, user); // Simply put it in session

    res.getOutputStream().print("You are logged in as " + user.getUsername());
  }

  @Override
  protected void unsuccessfulAuthentication(
    HttpServletRequest request,
    HttpServletResponse response,
    AuthenticationException failed
  )
    throws IOException, ServletException {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    response.setContentType("text/plain");
    response.getOutputStream().print(failed.getMessage());
  }
}

自定义检查存储在会话中并传递给:AuthenticationFilterauth infoSecurityContext

public class AuthenticationFilter extends GenericFilterBean {

  @Override
  public void doFilter(
    ServletRequest request,
    ServletResponse response,
    FilterChain filterChain
  )
    throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) request;
    HttpSession session = req.getSession();

    User user = (User) session.getAttribute(UserSessionKey);

    if (user != null) {
      UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
        user,
        user.getPassword(),
        user.getAuthorities()
      );

      SecurityContextHolder.getContext().setAuthentication(authToken);
    }

    // Either securityContext has authToken or not, we continue the filter chain
    filterChain.doFilter(request, response);
  }
}

自定义相当简单明了,使会话失效并终止身份验证过程:LogoutFilter

public class LogoutFilter extends AbstractAuthenticationProcessingFilter {

  public LogoutFilter(String url) {
    super(url);
  }

  @Override
  public Authentication attemptAuthentication(
    HttpServletRequest req,
    HttpServletResponse res
  )
    throws AuthenticationException, IOException {
    req.getSession().invalidate();
    res.getWriter().println("You logged out!");

    return null;
  }
}

一点解释:

这三个自定义过滤器的作用就是这样,过滤器只侦听它们的替代终结点。loginlogout

在登录过滤器中,我们从客户端发送并根据数据库(在现实世界中)检查它以进行验证,如果它是有效用户,则将其放入会话中并将其传递给 。username and passwordSecurityContext

在注销过滤器中,我们只需返回一个字符串。invalidate the session

虽然自定义将对每个传入请求进行身份验证,以尝试从会话中获取用户信息,然后将其传递给 .AuthenticationFilterSecurityContext