簡體   English   中英

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

[英]How to secure REST API with Spring Boot and Spring Security?

我知道保護 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);

    }

但是這個帶有javax.servlet.filters的解決方案不能按我的需要工作,因為通過@ControllerAdvice和 Spring servlet dispatcher處理異常存在問題。

我需要的

我想知道這些標准是否現實並獲得任何幫助,如何開始使用 Spring Security 保護 REST API。 我閱讀了許多教程(例如Spring Data REST + Spring Security ),但都在非常基本的配置中工作 - 用戶及其憑據存儲在配置中的內存中,我需要使用 DBMS 並創建自己的身份驗證器。

請給我一些想法如何開始。

基於令牌的身份驗證 - 用戶將提供其憑據並獲得唯一且有時間限制的訪問令牌。 我想在我自己的實現中管理令牌創建、檢查有效性、到期。

實際上,使用過濾器進行令牌身份驗證 - 在這種情況下是最好的方法

最終,您可以通過 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 {}

您必須覆蓋配置方法。 下面的代碼,僅舉例說明如何配置匹配器。 它來自另一個項目。

    @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();
}

Spring security 對於向 REST URL 提供身份驗證和授權也非常有用。 我們不需要指定任何自定義實現。

首先,您需要在安全配置中指定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

我也搜索了很長時間。我正在做一個類似的項目。我發現Spring有一個模塊可以通過redis實現會話。 它看起來簡單而有用。 我也會添加到我的項目中。 可能會有所幫助:

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

http.addFilterBefore()與自定義過濾器一起使用的另一種方法

此解決方案更像是幫助您設置基礎的骨架。 我創建了一個working demo並添加了一些必要的評論來幫助理解這個過程。 它帶有一些簡單role-based permission-based身份驗證/授權,一個可publically accessable endpoint設置,您可以輕松獲取和使用。

所以最好查看完整的代碼,並運行應用程序: github repo

用戶類設置:

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
    );

自定義LoginFilter擴展AbstractAuthenticationProcessingFilter並覆蓋三個方法來處理身份驗證:

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());
  }
}

自定義AuthenticationFilter檢查存儲在會話中的auth info並傳遞給SecurityContext

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 password ,並根據數據庫(在現實世界中)檢查它以進行驗證,如果它是有效用戶,則將其放入會話並將其傳遞給SecurityContext

在注銷過濾器中,我們只是invalidate the session並返回一個字符串。

雖然自定義AuthenticationFilter將對每個傳入請求進行身份驗證,以嘗試從會話中獲取用戶信息,然后將其傳遞給SecurityContext

驗證 REST API 有兩種方法

1 - 使用 application.properties 文件中設置的默認用戶名和密碼進行基本身份驗證

基本認證

2 - 使用帶有實際用戶名和密碼的數據庫 (userDetailsS​​ervice) 進行身份驗證

高級認證

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM