65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Spring Security 实现 RESTFul Spring Boot Web 应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2018年11月20日

MIT

15分钟阅读

viewsIcon

28650

downloadIcon

178

设置使用 Spring Boot 的 RESTFul 服务,并使用 Spring Security 来保护 RESTFul API。

引言

最近,我向CodeProject提交了两篇关于Spring Boot的教程。一篇是关于如何使用Spring Boot和Spring MVC设置一个简单的MVC应用程序,不带安全。 另一篇更进一步,集成了Spring Security来锁定MVC应用程序。从这些教程中,您可以看到Spring Boot能够很好地处理MVC Web应用程序开发。它还支持RESTFul Web服务的开发,这正是我将在本教程中要讨论的。我将首先解释如何使用Spring Boot和Spring Web创建RESTFul服务。在本教程的后半部分,我将展示一种简单的方法来保护RESTFul服务,即为RESTFul服务提供简单的基于令牌的安全机制。

采用MVC架构设计的应用程序,每次页面交互都会经历从浏览器到后端服务器,再从后端服务器返回到浏览器的往返过程。RESTFul服务与典型的MVC应用程序不同。与RESTFul服务的交互通常是浏览器与服务之间的数据交换。RESTFul服务不负责构建UI显示。本教程将展示如何使用Spring Boot设计此类Web服务。

项目文件结构

对于这个项目,我设置了以下项目结构

<Base dir>/pom.xml
<Base dir>/src/main/java/org/hanbo/boot/rest/App.java
<Base dir>/src/main/java/org/hanbo/boot/rest/config/RestAppSecurityConfig.java
<Base dir>/src/main/java/org/hanbo/boot/rest/config/security/AuthTokenSecurityProvider.java
<Base dir>/src/main/java/org/hanbo/boot/rest/config/security/RestAuthSecurityFilter.java
<Base dir>/src/main/java/org/hanbo/boot/rest/controllers/SampleController.java
<Base dir>/src/main/java/org/hanbo/boot/rest/controllers/SampleSecureController.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/CarModel.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/CarRequestModel.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/PurchaseTaxModel.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/UserCredential.java
<Base dir>/src/main/resources/application.properties

这些是基本组件,可以帮助创建最基本的RESTFul服务

  • App.java:Java应用程序的主入口。Spring框架使用它来引导和启动应用程序。
  • 控制器类,其中包含操作方法。这些将处理传入的请求。
  • models目录下的类,它们是请求和响应。

此外,我还为这个应用程序添加了一些配置。这些文件可以在"configuration"文件夹及其子文件夹"security"下找到。

POM 文件

与前两个教程一样,我将从Maven POM文件开始。这是它

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>hanbo-boot-rest</artifactId>
    <version>1.0.1</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
        </dependency>
        <dependency>
           <groupId>commons-codec</groupId>
           <artifactId>commons-codec</artifactId>
           <version>1.11</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

这个Maven POM文件没有什么特别之处。它包含了Spring Boot Starter Web和Spring Boot Starter Security。前者用于实现RESTFul Web服务,后者用于为应用程序提供安全性。Spring Security taglib用于提供@PreAuthroize注解,以锁定RESTFul控制器操作方法。我还添加了Apache Commons Codec jar来编码和解码Base64字符串值,这用于HTTP安全头和解码它的授权过滤器。

与前两个教程将项目打包为war文件不同,在本教程中,项目被打包为常规的jar文件。

主入口

如果您还没有阅读我之前的两篇教程,您可能不知道用Spring Boot编写的应用程序不必部署到应用程序服务器即可执行。它可以是一个独立的应用程序。这就是为什么会有主入口。代码如下所示

package org.hanbo.boot.rest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App
{   
   public static void main(String[] args)
   {
      SpringApplication.run(App.class, args);
   }
}

这是一个非常简单的程序。它所做的就是获取当前类的定义,然后进行扫描,找出可注入的类以及如何设置依赖项树,以便应用程序能够运行。我再次强调,没有基于XML的配置。一切都通过约定或类注解完成。

控制器类

对于每个Spring Boot应用程序,主入口几乎相同。有时可能会有点不同。一旦有了它,您就可以添加控制器和一些操作方法。然后就可以使用Web应用程序了。

这与基于MVC的应用程序不同,后者要求控制器中的操作方法将视图返回给用户的浏览器。对于RESTFul API,操作方法返回基于Spring的HTTP响应,这要简单得多。在我进一步深入之前,让我向您展示这个基于REST的控制器的代码

package org.hanbo.boot.rest.controllers;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.hanbo.boot.rest.models.CarModel;
import org.hanbo.boot.rest.models.CarRequestModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController
{
   private List<CarModel> allCars;
   
   public SampleController()
   {
      allCars = createCarModelCollection();
   }
   
   @ResponseBody
   @RequestMapping(value="/public/allCars/{yearOfManufacture}", method = RequestMethod.GET)
   public ResponseEntity<List<CarModel>> allCars(
      @PathVariable("yearOfManufacture")  
      int year)
   {
      ResponseEntity<List<CarModel>> retVal = null;
      if (allCars == null || year < 1980)
      {
         List<CarModel> listOfCars = new ArrayList<CarModel>();
         retVal = ResponseEntity.ok(listOfCars);
         return retVal;
      }
      
      List<CarModel> foundCars = allCars
         .stream()
         .filter(x -> x.getYearOfManufacturing() == year)
         .collect(Collectors.toList());
      retVal = ResponseEntity.ok(foundCars);
      return retVal;
   }
   
   @RequestMapping(value="/public/findCar", method = RequestMethod.POST)
   public ResponseEntity<CarModel> findCar(
      @RequestBody
      CarRequestModel req)
   {
      ResponseEntity<CarModel> retVal = null;
      if (allCars == null || req == null)
      {
         retVal = ResponseEntity.ok((CarModel)null);
         return retVal;
      }
      
      Optional<CarModel> foundCar =  allCars
         .stream()
         .filter(x -> x.getYearOfManufacturing() == req.getYear()
            && x.getMaker().equalsIgnoreCase(req.getManufacturer())
            && x.getModel().equalsIgnoreCase(req.getModel()))
         .findFirst();
      if (foundCar.isPresent())
      {
         retVal = ResponseEntity.ok(foundCar.get());
      }
      else
      {
         retVal = ResponseEntity.ok((CarModel)null);         
      }
      
      return retVal;
   }
   
   private List<CarModel> createCarModelCollection()
   {
      List<CarModel> retVal = new ArrayList<CarModel>();
      
      CarModel car = new CarModel();
      car.setFullPrice(20000);
      car.setMaker("Nessan");
      car.setModel("Altima");
      car.setRebateAmount(600);
      car.setSuggestedRetailPrice(19250);
      car.setYearOfManufacturing(2005);
      
      retVal.add(car);

      car = new CarModel();
      car.setFullPrice(20096);
      car.setMaker("Subaru");
      car.setModel("Legacy");
      car.setRebateAmount(487);
      car.setSuggestedRetailPrice(20001);
      car.setYearOfManufacturing(2006);

      retVal.add(car);
      
      car = new CarModel();
      car.setFullPrice(20890);
      car.setMaker("Subaru");
      car.setModel("Outback");
      car.setRebateAmount(695);
      car.setSuggestedRetailPrice(19980);
      car.setYearOfManufacturing(2007);

      retVal.add(car);
      
      car = new CarModel();
      car.setFullPrice(21500);
      car.setMaker("Honda");
      car.setModel("Civic");
      car.setRebateAmount(750);
      car.setSuggestedRetailPrice(20100);
      car.setYearOfManufacturing(2008);

      retVal.add(car);
      
      car = new CarModel();
      car.setFullPrice(22600);
      car.setMaker("Toyota");
      car.setModel("Camery");
      car.setRebateAmount(708);
      car.setSuggestedRetailPrice(21100);
      car.setYearOfManufacturing(2008);

      retVal.add(car);
      
      return retVal;
   }
}

这个类有几个值得注意的地方。首先是类上使用的注解

@RestController
public class SampleController
{
...
}

如果您阅读了我之前的两篇教程中的一篇,您会知道,对于MVC,类上使用的注解应该是@Controller。定义RESTFul Web服务使用不同的注解,即@RestController。这是定义RESTFul API控制器和MVC控制器之间的显著区别之一。

现在,看看其中的一个操作方法

@RequestMapping(value="/public/allCars/{yearOfManufacture}", method = RequestMethod.GET)
public ResponseEntity<List<CarModel>> allCars(
   @PathVariable("yearOfManufacture")  
   int year)
{
   ResponseEntity<List<CarModel>> retVal = null;
   if (allCars == null || year < 1980)
   {
      List<CarModel> listOfCars = new ArrayList<CarModel>();
      retVal = ResponseEntity.ok(listOfCars);
      return retVal;
   }
   
   List<CarModel> foundCars = allCars
      .stream()
      .filter(x -> x.getYearOfManufacturing() == year)
      .collect(Collectors.toList());
   retVal = ResponseEntity.ok(foundCars);
   return retVal;
}

另一个值得注意的地方是这个方法是@RequestMapping注解。这指定了操作方法可以处理的URL路径。它还指定了此操作方法可以处理的HTTP方法。在这种情况下,它只处理HTTP GET请求。

该方法返回一个ResponseEntity<List<CarModel>>类型的对象。ResponseEntity封装了实际返回给HTTP响应的对象。它还可以将Java对象转换为JSON,并准备好要返回的正确HTTP响应。我喜欢使用这种对象类型,因为它允许我设置状态码和返回对象。使用方法如下

ResponseEntity<List<CarModel>> retVal = null;
...

retVal = ResponseEntity.ok(foundCars);
return retVal;

接下来,方法的参数用@PathVariable注解。这告诉Spring Web,URL路径中的一部分可以用作值。@RequestMapping注解指定了URL路径模式:"/public/allCars/{yearOfManufacture}". "{yearOfManufacture}"将被用作方法中的参数值。在这种情况下,由"{yearOfManufacture}"表示的值被传递到名为year的参数中。

最后,方法体查找列表中特定年份的制造年份的所有汽车。找到的列表将作为JSON对象列表返回。如果没有找到汽车,将返回一个空列表。

此时,创建RESTFul服务所需的一切都已具备。一个主入口,一个控制器,以及一些DTO数据模型。我将不展示DTO数据模型的代码。它们是为了教程目的而设计的模拟对象类型,包含模拟数据。为了让本教程更有趣,我添加了一个安全过滤器,我们将在下一节中讨论。

安全过滤器

在RESTFul服务中处理安全与在MVC Web应用程序中处理安全略有不同。当一个利用RESTFul服务的Web应用程序运行时,服务本身不会保留会话,而是每次接收到请求时,服务都必须检查HTTP头中的身份验证数据。一旦找到,它就可以将身份验证数据转换为适当的授权。也就是说,它将创建一个包含用户名、凭据和用户角色的授权令牌。然后将请求传递给操作方法。

受保护的操作方法将带有@PreAuthorize(...)等注解。除非请求具有适当角色的安全令牌,否则该注解会在操作方法获取请求之前强制返回403。如果您足够努力,可以强制执行基于会话的安全机制(通过授权cookie)。然而,RESTFul服务应该是无状态的,使用会话来强制执行安全机制会限制服务的可伸缩性,从而违背了RESTFul Web服务的初衷。

在本教程中,我假设来自客户端(通常来自浏览器)的请求已经过身份验证。也就是说,某个其他服务已经为请求提供了安全令牌。RESTFul服务一旦拦截到请求,过滤器将使用请求中的令牌来确定请求在传递给实际操作方法之前所需的授权。

Spring Security配置

这是我定义的Spring安全配置类

package org.hanbo.boot.rest.config;

import org.hanbo.boot.rest.config.security.AuthTokenSecurityProvider;
import org.hanbo.boot.rest.config.security.RestAuthSecurityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.
           authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.
           method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.
           web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class RestAppSecurityConfig  extends WebSecurityConfigurerAdapter
{
   @Autowired
   private AuthTokenSecurityProvider authProvider;
   
   @Override
   protected void configure(HttpSecurity http) throws Exception
   {
      System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                        ( Http Security )...");
      http.csrf().disable()
         .authorizeRequests()
         .antMatchers("/public/**").permitAll()
         .anyRequest().authenticated();
      http.addFilterAfter(new RestAuthSecurityFilter
           (super.authenticationManager()), BasicAuthenticationFilter.class);
   }
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                        ( Auth Manager )...");
      authMgrBuilder.authenticationProvider(authProvider);
   }
   
   @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
   }
}

该类名为"RestAppSecurityConfig"。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class RestAppSecurityConfig  extends WebSecurityConfigurerAdapter
{
...
}

它继承自WebSecurityConfigurerAdapter。任何派生自WebSecurityConfigurerAdapter的子类都将为Web应用程序提供安全配置。问题是Spring框架不会查找它,除非您使用@Configuration注解子类。该类还有两个额外的注解。一个是@EnableWebSecurity。此注解将为整个应用程序启用Spring安全。另一个注解EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)为操作方法启用授权注解,如@PreAuthorize(...)@PostAuthorize(...)

名为configure(...)的方法用于定义如何处理请求

@Override
protected void configure(HttpSecurity http) throws Exception
{
   System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                     ( Http Security )...");
   http.csrf().disable()
      .authorizeRequests()
      .antMatchers("/public/**").permitAll()
      .anyRequest().authenticated();
   http.addFilterAfter(new RestAuthSecurityFilter
                      (super.authenticationManager()), BasicAuthenticationFilter.class);
}

在此方法中,我定义了如下内容

  • 任何路由到<appContext>/public/**的请求都将被处理而无需任何身份验证。尽管这没有作用,因为我们添加了一个自定义过滤器。
  • 任何路由到"/public/"以外的子路径的请求都必须显式进行身份验证和授权。
  • 添加一个自定义过滤器,该过滤器将处理请求中的安全令牌。自定义过滤器名为RestAuthSecurityFilter,它接受一个AuthenticationManager对象,以便authenticationManager对象可以在将请求发送到操作方法之前对其进行适当授权。

在使用AuthenticationManager对象之前,必须对其进行正确初始化。这正是下一个方法的作用

@Override
protected void configure(AuthenticationManagerBuilder authMgrBuilder)
   throws Exception
{
   System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                     ( Auth Manager )...");
   authMgrBuilder.authenticationProvider(authProvider);
}

AuthenticationManagerBuilder是一个可以创建AuthenticationManager对象的构建器对象。它需要一个认证提供程序(AuthenticationProvider)。这就是我为此创建了自己的认证提供程序的原因。我们将在下一节中介绍它。此类中还有最后一个方法,它是一个返回AuthenticationManager对象的getter。这是必需的,因为我的过滤器被定义为Spring服务,并且需要AuthenticationManager对象的依赖注入。因此,为了使启动正常工作,我需要这个返回AuthenticationManager bean的getter。

认证提供程序

自定义认证提供程序也是必需的。最简单的方法是

  • 认证管理器构建器将创建一个认证管理器。
  • 认证管理器使用认证提供程序来验证用户凭据。

创建一个自定义认证提供程序,允许程序员定义如何验证用户凭据以及如何设置用户授权。我们都喜欢做的一件常见事情是

  • 获取用户名和密码,然后对密码进行哈希处理。
  • 从数据库中获取用户凭据。
  • 将用户名和密码哈希与数据库中的用户凭据进行匹配,如果它们相等且用户处于活动状态。然后,将从数据库中获取的角色添加到安全令牌中。
  • 如果用户凭据不匹配,则不会创建安全令牌。当请求向下传递到操作方法时,安全注解将导致请求失败,状态码为403。

这是Web应用程序身份验证和授权的典型场景。对于RESTFul服务,用户登录的处理方式不同,大多数情况下,请求中包含一个安全令牌。在此示例中,我将假设HTTP请求有一个可以转换为用户名和密码对的头条目。我的认证提供程序将能够检查用户凭据,就像我描述的场景一样。我的自定义认证提供程序类的外观如下

package org.hanbo.boot.rest.config.security;

import java.util.ArrayList;
import java.util.List;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

@Service
public class AuthTokenSecurityProvider implements AuthenticationProvider
{
   @Override
   public Authentication authenticate(Authentication auth) throws AuthenticationException
   {
      if (auth == null)
      {
         return null;
      }
      
      String name = auth.getName();
      String password = "";
      if (auth.getCredentials() != null) 
      {
         password = auth.getCredentials().toString();
      }
      
      if (name == null || name.length() == 0)
      {
         return null;
      }

      if (password == null || password.length() == 0)
      {
         return null;
      }
      
      Authentication retVal = null;
      List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
      
      if (name.equalsIgnoreCase("anonymous-user") && 
          password.equalsIgnoreCase("anonymouspass"))
      {
         grantedAuths.clear();
         retVal = new UsernamePasswordAuthenticationToken(
            "anonymous", "not-authenticated", grantedAuths
         );
      }
      else if (name.equalsIgnoreCase("Elrick") && 
               password.equalsIgnoreCase("123tset321"))
      {
         grantedAuths.clear();
         grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
         retVal = new UsernamePasswordAuthenticationToken(
            name, "UserAuthenticated", grantedAuths
         );
      }

      System.out.println("Add Auth - User Name: " + retVal.getName());
      System.out.println("Add Auth - Roles Count: " + 
             (retVal.getAuthorities() != null? retVal.getAuthorities().size() : 0));

      return retVal;
   }

   @Override
   public boolean supports(Class<?> tokenClass)
   {
      return tokenClass.equals(UsernamePasswordAuthenticationToken.class);
   }
}

这个类有一个名为supports(...)的方法。此方法基本上检查认证令牌是否是此提供程序可以处理的正确类型。如果是,它将使用其另一个方法authenticate(...)来尝试验证和授权用户。

authenticate(...)方法将检查用户名和密码,如果用户名是"Elrick"且密码是"123tset321",则会为用户设置身份验证令牌,其角色将是"ROLE_USER"。如果用户名是"anonymous-user"且密码是"anonymouspass",则会创建一个匿名用户身份验证令牌,该令牌没有用户角色。对于所有其他情况,将返回一个null身份验证令牌。在这种情况下,身份验证过程失败。这将在我的安全过滤器中处理。

安全过滤器

最后,我为本教程创建的安全过滤器的外观如下

package org.hanbo.boot.rest.config.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.tomcat.util.codec.binary.Base64;
import org.hanbo.boot.rest.models.UserCredential;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class RestAuthSecurityFilter extends GenericFilterBean
{
   private AuthenticationManager authManager;
   
   public RestAuthSecurityFilter(AuthenticationManager authManager)
   {
      this.authManager = authManager;
   }
   
   @Override
   public void doFilter(
      ServletRequest request, 
      ServletResponse response,
      FilterChain chain) throws IOException, ServletException
   {
      try
      {
         if (request != null)
         {
            String auth = ((HttpServletRequest)request).getHeader("testAuth");
            System.out.println("Test Auth: " + auth);
            
            SecurityContext sc = SecurityContextHolder.getContext();
            UsernamePasswordAuthenticationToken authReq = null;
            
            authReq = getAuthCredentialFrom(auth);

            Authentication authCool = authManager.authenticate(authReq);
            sc.setAuthentication(authCool);
            chain.doFilter(request, response);
         }
         else
         {
            ((HttpServletResponse)response).sendError
            (HttpServletResponse.SC_BAD_REQUEST, "Invalid Request.");
         }
      }
      catch (Exception ex)
      {
         throw new ServletException("Unknown exception in RestAuthSecurityFilter", ex);
      }
   }
   
   private UsernamePasswordAuthenticationToken getAuthCredentialFrom(String authHdrValue) 
           throws JsonParseException, JsonMappingException, IOException
   {
      UsernamePasswordAuthenticationToken defUserToken =
         new UsernamePasswordAuthenticationToken("anonymous-user", "anonymouspass");
      if (authHdrValue == null || authHdrValue.length() == 0) 
      {
         return defUserToken;
      }
      else
      {
         byte[] decodedBytes = Base64.decodeBase64(authHdrValue);
         String authCred = new String(decodedBytes);
         System.out.println(authCred);
         if (authCred != null && authCred.length() > 0)
         {
            ObjectMapper objMapper = new ObjectMapper();
            UserCredential userCrede = objMapper.readValue(authCred, UserCredential.class);
            if (userCrede != null)
            {
               return new UsernamePasswordAuthenticationToken
                      (userCrede.getUserName(), userCrede.getUserPassword());
            }
            else
            {
               return defUserToken;
            }
         }
         else
         {
            return defUserToken;
         }
      }
   }
}

我定义的类名为RestAuthSecurityFilter。它扩展自GenericFilterBean,因此我可以覆盖名为doFilter的过滤器方法。该类被注解为@Component,因此如果需要,它可以被注入。这就是为什么我需要在RestAppSecurityConfig类中有一个bean getter。好吧,这只是部分原因。另一个原因是,对于这个类,我添加了一个接受AuthenticationManager对象作为参数的构造函数。我真的很需要AuthenticationManager对象。您将在下一节中看到原因。

这是doFilter()的代码

   @Override
   public void doFilter(
      ServletRequest request, 
      ServletResponse response,
      FilterChain chain) throws IOException, ServletException
   {
      try
      {
         if (request != null)
         {
            String auth = ((HttpServletRequest)request).getHeader("testAuth");
            System.out.println("Test Auth: " + auth);
            
            SecurityContext sc = SecurityContextHolder.getContext();
            UsernamePasswordAuthenticationToken authReq = null;
            
            authReq = getAuthCredentialFrom(auth);

            Authentication authCool = authManager.authenticate(authReq);
            sc.setAuthentication(authCool);
            chain.doFilter(request, response);
         }
         else
         {
            ((HttpServletResponse)response).sendError
            (HttpServletResponse.SC_BAD_REQUEST, "Invalid Request.");
         }
      }
      catch (Exception ex)
      {
         throw new ServletException("Unknown exception in RestAuthSecurityFilter", ex);
      }
   }

此方法首先从请求的头中获取未转换的安全令牌。为了进行健全性检查,它被输出到命令行控制台。这是通过以下代码完成的

String auth = ((HttpServletRequest)request).getHeader("testAuth");
System.out.println("Test Auth: " + auth);

然后,我使用一个辅助方法来验证安全令牌,代码如下

authReq = getAuthCredentialFrom(auth);

此辅助方法的代码如下

private UsernamePasswordAuthenticationToken getAuthCredentialFrom(String authHdrValue) 
        throws JsonParseException, JsonMappingException, IOException
{
   UsernamePasswordAuthenticationToken defUserToken =
      new UsernamePasswordAuthenticationToken("anonymous-user", "anonymouspass");
   if (authHdrValue == null || authHdrValue.length() == 0) 
   {
      return defUserToken;
   }
   else
   {
      byte[] decodedBytes = Base64.decodeBase64(authHdrValue);
      String authCred = new String(decodedBytes);
      System.out.println(authCred);
      if (authCred != null && authCred.length() > 0)
      {
         ObjectMapper objMapper = new ObjectMapper();
         UserCredential userCrede = objMapper.readValue(authCred, UserCredential.class);
         if (userCrede != null)
         {
            return new UsernamePasswordAuthenticationToken
                   (userCrede.getUserName(), userCrede.getUserPassword());
         }
         else
         {
            return defUserToken;
         }
      }
      else
      {
         return defUserToken;
      }
   }
}

转换前的安全令牌是JSON格式的字符串,编码为Base64字节数组。它必须从Base64编码的字节数组转换回字符串。以下是实现方法

byte[] decodedBytes = Base64.decodeBase64(authHdrValue);
String authCred = new String(decodedBytes);
System.out.println(authCred);

一旦我们有了可读的安全令牌字符串,它将被转换为一个实际的Java对象。由于Spring Starter Web库支持RESTFul Web服务,它必须包含某种JSON序列化器/反序列化器。该库是Jackson。这意味着我们不必添加另一个JSON序列化器/反序列化器库。它已经可用了。我反序列化字符串的方式如下

ObjectMapper objMapper = new ObjectMapper();
UserCredential userCrede = objMapper.readValue(authCred, UserCredential.class);

UserCredential类的定义如下

package org.hanbo.boot.rest.models;

public class UserCredential
{
   private String userName;
   
   private String userPassword;

   public String getUserName()
   {
      return userName;
   }

   public void setUserName(String userName)
   {
      this.userName = userName;
   }

   public String getUserPassword()
   {
      return userPassword;
   }

   public void setUserPassword(String userPassword)
   {
      this.userPassword = userPassword;
   }
}

辅助方法会将UserCredential对象转换为UsernamePasswordAuthenticationToken对象,并将其返回给调用方doFilter()方法。回到doFilter()方法中,一旦有了有效的UsernamePasswordAuthenticationToken对象,就会将其传递给AuthenticationManager对象进行身份验证和授权尝试。哪个对象实际执行此身份验证和授权?正确的答案是我的认证提供程序。辅助方法将尝试返回默认的匿名用户,没有关联的角色。在过滤器中,如果请求有问题,响应将是状态码400。如果有任何异常,响应将是500内部服务器错误。

受保护的RESTFul API控制器

现在安全过滤器和认证提供程序已就位,是时候保护API控制器了。我有两个控制器类,一个是公共可访问的。也就是说,此类中的操作方法没有用@PreAuthorize注解,它们可以被未经验证的用户访问。此控制器的源代码已在前面的章节中列出。如果您需要回顾,请向上滚动查看。类名为SampleController

要测试此控制器和操作方法,您可以使用以下两个URL

  • https://:8080/public/allCars/{制造年份}:这可以使用HTTP GET方法完成。
  • https://:8080/public/findCar:这可以使用HTTP POST方法完成。请求体是一个JSON对象。响应将是一个表示找到的car对象的JSON字符串。

受保护的API控制器使用@PreAuthorize注解来过滤掉任何没有正确授权令牌的请求。此控制器的源代码如下

package org.hanbo.boot.rest.controllers;

import org.hanbo.boot.rest.models.CarRequestModel;
import org.hanbo.boot.rest.models.PurchaseTaxModel;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleSecureController
{
   @PreAuthorize("hasRole('ROLE_USER')")
   @ResponseBody
   @RequestMapping(value="/secure/calculateTax.", method = RequestMethod.POST)
   public ResponseEntity<PurchaseTaxModel> calculateTax(
      @RequestBody
      CarRequestModel req)
   {
      PurchaseTaxModel retVal = new PurchaseTaxModel();
      retVal.setYear(req.getYear());
      retVal.setModel(req.getModel());
      retVal.setManufacturer(req.getManufacturer());
      retVal.setTaxAmount(4000);
      retVal.setTaxRate(0.03f);
      
      ResponseEntity<PurchaseTaxModel> resp = ResponseEntity.ok(retVal);
      return resp;
   }
}

正如您在类中看到的,操作方法有一个@PreAuthorize注解。此注解确保只有具有"ROLE_USER"角色的已验证用户才能访问此操作方法。

要测试此控制器和操作方法,您可以使用以下URL:https://:8080/secure/calculateTax

此方法仅接受HTTP POST请求。下一节将介绍更多有关测试的信息。

测试RESTFul Web服务

测试此Web服务非常简单。这一切都与HTTP请求和响应有关,无需进行任何页面导航。我使用了一个名为"Postman"的工具。您可能听说过它。总之,在我们测试之前,我们必须构建整个项目。

要构建项目,请转到项目根目录并运行以下命令

mvn clean install

要运行项目根目录中的应用程序,请运行以下命令

java -jar target\hanbo-boot-rest-1.0.1.jar

构建和执行将成功。让我们开始测试。启动"Postman"应用程序。启动时,屏幕截图如下所示

从第一个测试场景开始,运行请求以获取特定制造年份的所有汽车。支持的年份是:2005、2006、2007和2008。以下是请求的设置

  • 将HTTP方法设置为"GET"。
  • URL设置为:https://:8080/public/allCars/2008
  • 单击"发送"按钮

这是请求的屏幕截图

随着Web服务的运行,它将响应以下内容

[
    {
        "yearOfManufacturing": 2008,
        "model": "Civic",
        "maker": "Honda",
        "suggestedRetailPrice": 20100,
        "fullPrice": 21500,
        "rebateAmount": 750
    },
    {
        "yearOfManufacturing": 2008,
        "model": "Camery",
        "maker": "Toyota",
        "suggestedRetailPrice": 21100,
        "fullPrice": 22600,
        "rebateAmount": 708
    }
]

响应的屏幕截图如下所示

下一个测试是查找汽车的HTTP POST请求。请求URL是:https://:8080/public/findCar

Postman设置的HTTP POST请求

  • HTTP方法应为"POST"。
  • URL是:https://:8080/public/findCar
  • 选择"Body"选项卡,然后选中"Raw"单选按钮。
  • 在下拉菜单中,选择"Application/JSON"。

请求需要一个JSON对象,它看起来像这样

{
   "year": 2007,
   "manufacturer": "Subaru",
   "model": "Outback"
}

这是请求的屏幕截图

单击"发送"按钮。返回的响应将是

{
    "yearOfManufacturing": 2007,
    "model": "Outback",
    "maker": "Subaru",
    "suggestedRetailPrice": 19980,
    "fullPrice": 20890,
    "rebateAmount": 695
}

最后,受保护的RESTFul控制器的测试场景。对于这个场景,我们需要请求中的安全头。请求的完整字符串如下

{ "userName": "Elrick", "userPassword": "123tset321" }

为了增加一点趣味,我将其编码为Base64字符串,如下所示

eyAidXNlck5hbWUiOiAiRWxyaWNrIiwgInVzZXJQYXNzd29yZCI6ICIxMjN0c2V0MzIxIiB9

要为这个场景设置Postman应用程序,请执行以下步骤

  • HTTP方法应为"POST"。
  • URL是:https://:8080/secure/calculateTax
  • 首先选择"Header"选项卡
  • 在第一个可用的header条目中,将键设置为"testAuth",值为: eyAidXNlck5hbWUiOiAiRWxyaWNrIiwgInVzZXJQYXNzd29yZCI6ICIxMjN0c2V0MzIxIiB9
  • 选择"Body"选项卡,设置与上一个请求相同
    • 选中"Raw"单选按钮。
    • 在下拉菜单中,选择"Application/JSON"。
    • 使用与请求体相同的JSON。

单击"发送"按钮,您将收到以下响应

{
    "year": 2007,
    "manufacturer": "Subaru",
    "model": "Outback",
    "taxRate": 0.03,
    "taxAmount": 4000
}

为了让事情更有趣一些,删除名为"testAuth"的header条目。然后再次运行相同的请求,您将看到错误响应而不是预期的响应

{
    "timestamp": "2018-11-19T03:43:12.349+0000",
    "status": 403,
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/secure/calculateTax"
}

这是带有空"testAuth"header条目的请求响应的屏幕截图

关注点

本教程到此结束。本教程涵盖内容的总结

  • 如何使用Spring Boot设置RESTFul服务
  • 如何为Spring Boot应用程序添加安全配置,并配置使用自定义过滤器和认证提供程序

再次,我写这个非常开心。希望您喜欢这篇教程。

历史

  • 2018年11月18日 - 初稿
© . All rights reserved.