vlambda博客
学习文章列表

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Chapter 13. Ticket Management – Advanced CRUD

我们的应用程序必须满足实时业务案例,例如 Ticket 管理。本章将回顾本书前几章中涵盖的大部分主题。

在本章中,我们将创建一个实时场景并实现我们场景的业务需求——由用户管理工单,客户服务代表CSR)和管理员。

我们的最后一章包括以下主题:

  • Creating a ticket by customer
  • Updating the ticket by customer, CSR, and admin
  • Deleting the ticket by customer
  • CSR/admin deletes multiple tickets

Ticket management using CRUD operations


在进入 Ticket 管理系统之前,我们将涵盖 业务需求。

假设我们有一个可供我们的客户 Peter 和 Kevin 使用的银行 Web 应用程序,并且我们的管理员 Sammy 和 CSR Chloe 可以在出现任何应用程序问题时提供帮助。

彼得和凯文在付款过程中面临一些问题。当他们尝试点击支付交易提交按钮时,它不起作用。此外,交易视图位于网页中。所以我们的用户(Peter 和 Kevin)将创建一张票来分享他们的问题。

创建工单后,客户/CSR/管理员可以对其进行更新。此外,客户可以删除自己的票。更新时,任何人都可以更改严重性;但是,只有 CSR 和 admin 可以更改状态,因为工单的状态与官方活动有关。

客户可以查看全部或单张票,但一次只能删除一张票。多重删除选项适用于 CSR 和管理员。但是,CSR 一次只能删除三张工单。管理员将完全控制工单管理应用程序,并且可以随时删除任意数量的工单。

Registration


让我们开始编写代码来满足上述要求。首先,我们需要从客户、CSR 和管理员注册开始。由于这些用户具有不同的角色,我们将为每个用户赋予 不同的用户类型。

User types

为了区分用户,我们提出了三种不同的用户类型,因此当他们访问我们的 REST API 时,他们的授权会有所不同。以下是三种不同的 user 类型:

名称

用户类型

一般用户/客户

1

企业社会责任

2

行政

3

User POJO

在我们之前的 User 类中,我们只有 useridusername。我们可能还需要两个变量来满足我们之前提到的业务需求。我们将 passwordusertype 添加到我们现有的 User 类中:

private String password;  
  /*
   * usertype:
   * 1 - general user
   * 2 - CSR (Customer Service Representative)
   * 3 - admin 
   */
private Integer usertype;
public String getPassword() {
    return password;
}
public void setPassword(String password) {
   this.password = password;
}
public void setUsertype(Integer usertype){
    this.usertype = usertype;
}  
public Integer getUsertype(){
    return this.usertype;
}

在前面的代码中,我们刚刚添加了 passwordusertype。此外,我们还为变量添加了 getter 和 setter 方法。

Note

您可以在我们的 GitHub 存储库 (https://github.com/PacktPublishing/Building-RESTful-Web-Services-with-Spring-5-Second-Edition) .你可能厌倦了添加 getter 和 setter 方法,所以我们将用 Lombok 库替换它们,我们将在本章后面讨论。但是,Lombok 库与 Eclipse 或 STS IDE 存在一些冲突问题,您可能需要注意这些问题。在这些 IDE 的某些版本中,由于 Lombok 库问题,您不会在创建类时获得预期的行为。此外,一些开发人员提到他们在使用 Lombok 时存在部署问题。

为了从我们的 User 类自动生成用户 ID,我们将使用一个单独的计数器。我们将保留一个静态变量来做到这一点;在实际应用中不建议保留静态计数器。为了简化我们的实现逻辑,我们使用了静态计数器。

以下代码将添加到我们的 User 类中:

private static Integer userCounter = 100;

我们从 100 个用户开始。每当添加新用户时,它会自动增加 userid 并将其分配给新用户。

Note

userCounter 起点没有限制。通过将用户序列保持在 2 (2XX) 并将票证保持在序列 3 (3XX),读者更容易区分用户和票。

现在我们将创建一个新的构造函数来将用户添加到我们的应用程序中。此外,我们将增加 usercounter 参数并将其分配为每个新用户的 userid

public User(String username, String password, Integer usertype) {
    userCounter++;
    this.userid = userCounter;
    this.username = username;
    this.password = password;
    this.usertype = usertype;
  }

前面的构造函数将填充所有用户详细信息,包括 userid(来自 usercounter)。

在这里,我们将使用 usernamepasswordusertype 添加一个新用户> UserServiceImpl 类中; usertype 会因每个用户而异(例如,usertype for admin 是 3):

  @Override
  public void createUser(String username, String password, Integer usertype){
    User user = new User(username, password, usertype); 
    this.users.add(user);
  }

在前面的代码中,我们创建了一个新用户并将其添加 到现有用户列表中。

Note

在前面的代码中,我们没有提到 UserService 中的抽象方法。假设每个具体方法在接口中都有一个抽象方法。此后,考虑在适当的接口中添加所有抽象方法。

Customer registration

现在是添加客户的时候了。新客户必须通过添加用户名和密码详细信息来创建帐户。

我们将讨论客户注册 API。此 API 将帮助任何新客户在我们这里注册他们的帐户:

  @ResponseBody
  @RequestMapping(value = "/register/customer", method = RequestMethod.POST)
  public Map<String, Object> registerCustomer( 
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password
    ) {   
    userSevice.createUser(username, password, 1); 
    return Util.getSuccessResult();
  }

在前面的代码中,我们添加了一个 API 来注册客户。调用此 API 的人将被视为客户(而非管理员/CSR)。如您所见,我们提到了 1 作为 usertype,因此将其视为客户。

这是用于客户注册的 SoapUI 的屏幕截图:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

此外,在前面的代码中,我们使用了 Util 类中的 getSuccessResult。我们将看到其他 Util 方法,如下代码所示:

package com.packtpub.util;
import java.util.LinkedHashMap;
import java.util.Map;
public class Util {
  public static <T> T getUserNotAvailableError(){
    Map<String, Object> map = new LinkedHashMap<>();    
    map.put("result_code", 501);
    map.put("result", "User Not Available"); 
    return (T) map;
  }  
  public static <T> T getSuccessResult(){
    Map<String, Object> map = new LinkedHashMap<>();    
    map.put("result_code", 0);
    map.put("result", "success"); 
    return (T) map;
  }  
  public static <T> T getSuccessResult(Object obj){
    Map<String, Object> map = new LinkedHashMap<>();    
    map.put("result_code", 0);
    map.put("result", "success");
    map.put("value", obj);
    return (T) map;
  }
}

在前面的代码中,我们创建了一个 Util 类来保存将在不同控制器中使用的通用方法,例如 Ticket用户。这些 Util 方法用于避免我们的应用程序中的代码重复。

Note

为了简化流程,我们在这段代码中没有使用任何异常处理机制。您可能需要使用适当的异常处理技术来实现这些方法。

Admin registration

每个应用程序有一个管理员来控制所有操作,例如删除客户和更改状态。在这里,我们将讨论管理员注册 API。

管理员注册 API 也将使用 createUser 方法来创建管理员。这是管理员注册的代码:

  @ResponseBody
  @RequestMapping(value = "/register/admin", method = RequestMethod.POST)
  public Map<String, Object> registerAdmin( 
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password
    ) {
    Map<String, Object> map = new LinkedHashMap<>();
    userSevice.createUser(username, password, 3); // 3 - admin (usertype)
    map.put("result", "added");
    return map;
  }

在前面的代码中,我们添加了管理员注册的代码,同时在 createUser 构造函数调用中提到了 3(admin 的用户类型) .另外,您可以看到我们使用 POST 方法进行注册。

以下是 http://localhost:8080/user/register/admin admin 注册 SoapUI API 调用的截图:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Note

在我们的工单管理中,我们对复制用户没有任何限制,这意味着我们可以拥有多个同名用户。我们建议您避免复制它们,因为这会破坏流程。为了尽可能简化我们的实现,我们忽略了这些限制。但是,您可以实施限制以改进应用程序。

CSR registration

在本节中,我们讨论 CSR 注册。

客户注册只有一个区别——usertype。除了 usertype 和 API 路径之外,与其他注册调用没有什么不同:

  @ResponseBody
  @RequestMapping(value = "/register/csr", method = RequestMethod.POST)
  public Map<String, Object> registerCSR( 
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password
    ) {     
    userSevice.createUser(username, password, 2);
    return Util.getSuccessResult();
  }

正如我们对其他 API 所做的那样,我们使用 2(CSR 的用户类型)来注册 CSR。我们看一下SoapUI中的API调用,如下:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Login and token management


在上一节中,我们介绍了用户注册主题,例如客户、管理员和 CSR。用户成功注册后,他们必须登录才能执行操作。因此,让我们创建与登录和会话相关的 API 和业务实现。

在转到登录和会话之前,我们讨论一下JSON Web Token,它将被使用 用于会话身份验证。由于我们的 securityService 类中已经有了 createToken 方法,我们将只讨论 令牌生成中使用的主题

Generating a token

我们可能需要将 JSON Web Token 用于会话目的。我们使用我们现有的令牌生成方法来保存我们的用户详细信息:

    String subject = user.getUserid()+"="+user.getUsertype();
    String token = securityService.createToken(subject, (15 * 1000 * 60)); // 15 mins expiry time

我们使用 user.getUserid()+"="+user.getUsertype() 作为主题。此外,我们提到了 15 分钟作为到期时间,因此令牌将仅在 15 分钟内有效。

Customer login

让我们为客户创建一个登录 API。 customer 必须提供用户名和密码 详细信息作为参数。在实际应用程序中,这些详细信息可能来自 HTML 表单,如下所示:

  @ResponseBody
  @RequestMapping(value = "/login/customer", method = RequestMethod.POST)
  public Map<String, Object> loginCustomer( 
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password
    ) {
    User user = userSevice.getUser(username, password, 1);    
    if(user == null){
      return Util.getUserNotAvailableError();
    }    
    String subject = user.getUserid()+"="+user.getUsertype();
    String token = securityService.createToken(subject, (15 * 1000 * 60)); // 15 minutes expiry time    
    return Util.getSuccessResult(token);
  }

在前面的代码中,我们通过传递所有必要的参数从 userService 调用了 getUser 方法。由于用户类型是 1,我们在方法中传递了 1。一旦我们得到用户,我们就检查它是否为空。如果为 null,我们将简单地抛出错误。如果用户不为空,我们创建一个令牌主题(user.getUserid()+"="+user.getUsertype())并使用 15 分钟到期时间。

如果一切顺利,我们将创建一个结果映射并将该映射作为 API 响应返回。当我们调用此 API 时,此地图将在我们的结果中显示为 JSON 响应。

另外,在前面的代码中,我们使用了 getUserNotAvailableError 来返回错误详情。由于我们将在所有与会话相关的 API 中使用此错误,因此我们创建了一个单独的方法来避免代码重复。

在这里,我们可以看到客户登录SoapUI的截图:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Note

如果用户登录成功,我们将在响应 JSON 中获得一个令牌。我们必须将令牌用于与会话相关的 API,例如添加票证。此处提供了一个示例令牌:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMDM9MSIsImV4cCI6MTUxNTg5MDMzN30.v9wtiG-fNWlpjgJmou7w2oxA9XjXywsH32cDZ-P4zM4 在某些方法中,我们可能会看到 <T> T 返回类型,它是 Java 泛型的一部分。通过保留这样的泛型,我们可以通过正确地强制转换来返回任何对象。这是一个示例: return (T) map; 返回类型

Admin login

正如我们所看到的 customer 登录部分,我们还将拥有 用于管理员的登录 API。

在这里,我们将创建一个用于管理员登录的 API,并在成功验证后生成一个令牌:

  @ResponseBody
  @RequestMapping(value = "/login/admin", method = RequestMethod.POST)
  public Map<String, Object> loginAdmin( 
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password
    ) {
    Map<String, Object> map = new LinkedHashMap<>();   
    User user = userSevice.getUser(username, password, 3);    
    if(user == null){ 
      return Util.getUserNotAvailableError();
    }    
    String subject = user.getUserid()+"="+user.getUsertype();
    String token = securityService.createToken(subject, (15 * 1000 * 60)); // 15 mins expiry time    
    map.put("result_code", 0);
    map.put("result", "success");
    map.put("token", token);    
    return map;
  }

前面的登录 API 将仅用于管理目的。我们使用 usertype 作为 3 来创建管理员用户。此外,我们使用了 Util 方法 getUserNotAvailableError

这是管理员登录的 SoapUI 屏幕截图:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

CSR login

在本节中,我们将讨论 CSR 登录和令牌 generation TicketController 中的 CSR:

  @ResponseBody
  @RequestMapping(value = "/login/csr", method = RequestMethod.POST)
  public Map<String, Object> loginCSR( 
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password
    ) {    
    User user = userSevice.getUser(username, password, 2);    
    if(user == null){
      return Util.getUserNotAvailableError();
    }    
    String subject = user.getUserid()+"="+user.getUsertype();
    String token = securityService.createToken(subject, (15 * 1000 * 60)); // 15 mins expiry time

    return Util.getSuccessResult(token);
  }

像往常一样,我们将从列表中获取用户并检查是否为空。如果用户不可用,我们将抛出错误,否则代码将失败。正如我们对其他用户类型所做的那样,我们将为 CSR 创建一个单独的 API,并将 usertype 作为 1 来创建一个 CSR。

您可以在以下屏幕截图中看到 CSR 登录 API:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Ticket management


为了创建票证,我们需要创建一个 Ticket 类并将票证存储在列表中。我们会多讲Ticket类,ticket list,以及其他ticket相关的工作,比如user Ticket管理、admin工单管理、CSR工单管理。

Ticket POJO

我们将创建一个 Ticket 类,其中包含一些 basic 变量来存储所有details 与票证相关。下面的代码将帮助我们理解 Ticket 类:

public class Ticket {
  private Integer ticketid;  
  private Integer creatorid;  
  private Date createdat;  
  private String content;  
  private Integer severity;  
  private Integer status;
  // getter and setter methods
  @Override
  public String toString() {
    return "Ticket [ticketid=" + ticketid + ", creatorid=" + creatorid
        + ", createdat=" + createdat + ", content=" + content
        + ", severity=" + severity + ", status=" + status + "]";
  }   
  private static Integer ticketCounter = 300;  
  public Ticket(Integer creatorid, Date createdat, String content, Integer severity, Integer status){
    ticketCounter++;
    this.ticketid = ticketCounter;
    this.creatorid = creatorid;
    this.createdat = createdat;
    this.content = content;
    this.severity = severity;
    this.status = status;
  }
}

上述代码将存储工单详细信息,例如 ticketidcreatoridcreatedatcontent严重性status。此外,我们使用了一个名为 ticketCounter 的静态计数器来在创建票证时增加 ticketid。默认情况下,它将以 300 开头。

此外,我们使用了构造函数和 toString 方法,因为我们将在实现中使用它们。

我们必须为所有与票证相关的业务逻辑实现创建 TicketService 接口(用于抽象方法)和 TicketServiceImpl 具体类.

以下代码将显示如何添加票证:

  @Override
  public void addTicket(Integer creatorid, String content, Integer severity, Integer status) {
    Ticket ticket = new Ticket(creatorid, new Date(), content, severity, status);    
    tickets.add(ticket);
  }

在前面的代码片段中,我们只是使用构造函数创建了一张票并将票添加到我们的列表中。我们可以清楚地看到,我们没有使用 Ticket 类中的增量器创建的 ticketid。创建工单后,我们将其添加到工单列表中,该工单列表将用于其他操作。

Getting a user by token

对于所有与工单相关的操作,我们需要 用户会话。在 login 方法中,我们在登录成功后得到了 token。我们可以使用 token 来获取用户的详细信息。如果令牌不可用、不匹配或过期,我们将无法获取用户详细信息。

在这里,我们将实现从令牌中获取用户详细信息的方法:

  @Override
  public User getUserByToken(String token){
    Claims claims = Jwts.parser()              .setSigningKey(DatatypeConverter.parseBase64Binary(SecurityServiceImpl.secretKey))
             .parseClaimsJws(token).getBody();    
    if(claims == null || claims.getSubject() == null){
      return null;
    }    
    String subject = claims.getSubject();   
    if(subject.split("=").length != 2){
      return null;
    }    
    String[] subjectParts = subject.split("=");    
    Integer usertype = new Integer(subjectParts[1]);
    Integer userid = new Integer(subjectParts[0]);   
    return new User(userid, usertype);
  }

在前面的代码中,我们使用了令牌来获取用户详细信息。我们使用 JWT 解析器首先获取声明,然后我们将获取主题。如果您还记得,当我们为所有用户登录选项创建令牌时,我们使用了 user.getUserid()+"="+user.getUsertype() 作为主题。因此主题将采用相同的格式,例如,101 (user ID)=1 (user type)客户,因为客户的用户类型是 1

此外,我们使用 subject.split("=").length != 2 检查主题是否有效。如果我们使用不同的令牌,它将简单地返回 null。

一旦我们得到正确的主题,我们将获得 useridusertype,然后我们将通过创建一个 <代码类="literal">用户 对象。

Note

因为 getUserByToken 对所有用户都是通用的,所以它将用于我们所有的用户检索方法。

User Ticket management

首先,为了简化我们的业务需求,我们保留只有客户才能创建工单的规则。管理员和 CSR 都不能创建工单。在实时情况下,您可能有不同的工单管理方法。但是,我们会尽量简化业务要求

Ticket controller

在这里,我们将讨论由客户创建票证:

  /*
   * Rule:
   * Only user can create a ticket
   */
  @SuppressWarnings("unchecked")
  @ResponseBody
  @UserTokenRequired
  @RequestMapping(value = "", method = RequestMethod.POST)
  public <T> T addTicket( 
      @RequestParam(value="content") String content, 
      HttpServletRequest request
      ) {    
    User user = userSevice.getUserByToken(request.getHeader("token")); 
    ticketSevice.addTicket(user.getUserid(), content, 2, 1);     
    return Util.getSuccessResult(); 
  }

当用户提交工单时,他们只会发送有关他们在应用程序中遇到的问题的详细信息。我们为此类详细信息提供了 content 变量。此外,我们从他们在标头中传递的令牌中获取用户详细信息。

我们可以在以下屏幕截图中看到成功响应:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

在之前的 API 中,我们使用了 @UserTokenRequired 注解来验证用户令牌。我们将在此处检查注释和实现的详细信息。

The UserTokenRequired interface

在这里,我们将引入 UserTokenRequired 接口并跟进验证逻辑在下一节中:

package com.packtpub.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserTokenRequired {
}

The UserTokenRequiredAspect class

此类将在解密后检查用户 ID 和用户类型 validation 的用户令牌:

package com.packtpub.aop;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.packtpub.service.SecurityServiceImpl;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
@Aspect
@Component
public class UserTokenRequiredAspect { 
  @Before("@annotation(userTokenRequired)")
  public void tokenRequiredWithAnnotation(UserTokenRequired userTokenRequired) throws Throwable{    
    ServletRequestAttributes reqAttributes = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
    HttpServletRequest request = reqAttributes.getRequest();    
    // checks for token in request header
    String tokenInHeader = request.getHeader("token");    
    if(StringUtils.isEmpty(tokenInHeader)){
      throw new IllegalArgumentException("Empty token");
    }     
    Claims claims = Jwts.parser()              .setSigningKey(DatatypeConverter.parseBase64Binary(SecurityServiceImpl.secretKey))
             .parseClaimsJws(tokenInHeader).getBody();    
    if(claims == null || claims.getSubject() == null){
      throw new IllegalArgumentException("Token Error : Claim is null");
    }   
    String subject = claims.getSubject();

    if(subject.split("=").length != 2){
      throw new IllegalArgumentException("User token is not authorized");
    } 
  }
}

在前面的 UserTokenRequiredAspect 类中,我们刚刚从 header 中获取了 token 并验证了 token 是否有效。如果令牌无效,我们将抛出异常。

如果用户为 null(可能存在错误或空标记),它将在响应中返回 "User Not Available"。一旦提供了必要的令牌,我们将通过调用前面提到的 TicketServiceImpl 中的 addTicket 方法添加票证。

Note

严重程度如下:

  • Minor: Level 1
  • Normal: Level 2
  • Major: Level 3
  • Critical: Level 4

1 级被认为是低级,4 级被认为是高级,如此处所示@SuppressWarnings ("unchecked")。在某些地方,我们可能使用了 @SuppressWarnings 注释,我们需要告诉编译器它不需要担心正确的转换,因为它会被处理.

如果用户在任何与会话相关的 API 中传递了错误的 JWT,我们将得到错误,如下所示:

{
   "timestamp": 1515786810739,
   "status": 500,
   "error": "Internal Server Error",
   "exception": "java.lang.IllegalArgumentException",
   "message": "JWT String argument cannot be null or empty.",
   "path": "/ticket"
}

前面的错误只是提到 JWT 字符串为空或 null。

Getting my tickets – customer

创建票证后,客户可以通过调用/my/查看他们的票证门票 API。以下方法将处理获取票证要求:

  @ResponseBody
  @RequestMapping("/my/tickets")
  public Map<String, Object> getMyTickets(
      HttpServletRequest request
      ) {    
    User user = userSevice.getUserByToken(request.getHeader("token"));    
    if(user == null){
      return Util.getUserNotAvailableError();
    }    
    return Util.getSuccessResult(ticketSevice.getMyTickets(user.getUserid()));
  }

在前面的代码中,我们通过令牌验证了用户会话,并获得了会话中可用用户的票证:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Allowing a user to view their single ticket

与查看所有客户工单一样,客户也可以通过调用/{ticketid} API。让我们看看他的方法是如何工作的:

  @ResponseBody
  @TokenRequired
  @RequestMapping("/{ticketid}")
  public <T> T getTicket(
    @PathVariable("ticketid") final Integer ticketid, 
    HttpServletRequest request
    ) {

    return (T) Util.getSuccessResult(ticketSevice.getTicket(ticketid));
  }

在前面的 API 中,在验证了会话之后,我们使用了 TicketServiceImpl 中的 getTicket 方法来获取用户票的详细信息。

您可以借助此屏幕截图验证结果:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

您可以清楚地看到我们的标头中使用了令牌。如果没有令牌,API 将抛出异常,因为它是与会话相关的事务。

Allowing a customer to update a ticket

假设 customer 出于某种原因想要更新自己的工单,例如添加额外信息。我们将为客户提供更新票证的选项。

Updating a ticket – service (TicketServiceImpl)

对于 updating 选项,我们将 updateTicket 方法添加到我们的 < code class="literal">TicketServiceImpl 类:

  @Override
  public void updateTicket(Integer ticketid, String content, Integer severity, Integer status) {    
    Ticket ticket = getTicket(ticketid);    
    if(ticket == null){
      throw new RuntimeException("Ticket Not Available");
    }    
    ticket.setContent(content);
    ticket.setSeverity(severity);
    ticket.setStatus(status); 
  }

在上述方法中,我们通过 getTicket 方法获取票证,然后更新 content严重性状态

现在我们可以在我们的 API 中使用 updateTicket 方法,这里提到:

  @ResponseBody
  @RequestMapping(value = "/{ticketid}", method = RequestMethod.PUT)
  public <T> T updateTicketByCustomer (
      @PathVariable("ticketid") final Integer ticketid,      
      @RequestParam(value="content") String content,      
      HttpServletRequest request,
      HttpServletResponse response
      ) {   
    User user = userSevice.getUserByToken(request.getHeader("token"));    
    if(user == null){
      return getUserNotAvailableError();
    }    
    ticketSevice.updateTicket(ticketid, content, 2, 1);    
    Map<String, String> result = new LinkedHashMap<>();
    result.put("result", "updated");    
    return (T) result; 
  }

在前面的代码中,在验证了会话之后,我们调用了 updateTicket 并传递了新的内容。此外,在成功完成后,我们向调用者发送了正确的响应。

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Note

对于更新选项,我们使用了 PUT 方法,因为它是用于更新目的的适当 HTTP 方法。但是,我们也可以使用 POST 方法进行此类操作,因为没有限制。

Deleting a ticket

到目前为止,我们已经介绍了工单的创建、读取和更新actions。在本节中,我们将讨论客户的删除选项。

Deleting a service – service (TicketServiceImpl)

我们将在 TicketServiceImpl 类中添加 deleteMyTicket 方法,假设我们已经添加了抽象 method 到我们的界面:

@Override
  public void deleteMyTicket(Integer userid, Integer ticketid) { 
    tickets.removeIf(x -> x.getTicketid().intValue() == ticketid.intValue() && x.getCreatorid().intValue() == userid.intValue());
  }

在前面的代码中,我们使用 removeIf Java Streams 选项从流中查找和删除项目。如果用户 ID 和票证匹配,该项目将自动从流中删除。

Deleting my ticket – API (ticket controller)

我们可以在 API 的前面创建 调用deleteMyTicket 方法:

  @ResponseBody
  @UserTokenRequired
  @RequestMapping(value = "/{ticketid}", method = RequestMethod.DELETE)
  public <T> T deleteTicketByUser (
      @RequestParam("ticketid") final Integer ticketid,      
      HttpServletRequest request 
      ) {   
    User user = userSevice.getUserByToken(request.getHeader("token"));    
    ticketSevice.deleteMyTicket(user.getUserid(), ticketid);    
    return Util.getSuccessResult(); 
  }

像往常一样,我们将检查会话并调用 TicketServiceImpl 类中的 deleteTicketByUser 方法。一旦删除选项完成,我们将简单地返回显示 "success" 作为结果的映射。

这是删除票证后的 SoapUI 响应:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Note

在我们的票据 CRUD 中,我们没有选项在异常为空时抛出异常。如果您删除所有现有票证并调用获取票证,您将收到一条包含空值的成功消息。您可以通过添加空检查和限制来改进应用程序。

Admin Ticket management


在上一节中,我们看到了客户管理的 Ticket。客户可以单独控制他们的票,不能对其他客户的票做任何事情。在管理员模式下,我们可以控制应用程序中可用的任何票证。在本节中,我们将看到由管理员完成的工单管理。

Allowing a admin to view all tickets

由于管理员有 full 控件来查看应用程序中的所有工单,因此我们在 TicketServiceImpl 类没有任何限制。

Getting all tickets – service (TicketServiceImpl)

在这里,我们将讨论 admin 实现部分以获取应用程序中的所有票证:

  @Override
  public List<Ticket> getAllTickets() {
    return tickets;
  }

在前面的代码中,我们没有任何特定的限制,只是从我们的票列表中返回所有票。

Getting all tickets – API (ticket controller)

在工单控制器 API 中,我们将为管理员添加一个获取所有 工单 的方法:

  @ResponseBody
  @AdminTokenRequired
  @RequestMapping("/by/admin")
  public <T> T getAllTickets(
    HttpServletRequest request,
    HttpServletResponse response) {

    return (T) ticketSevice.getAllTickets();
  }

当管理员需要查看所有工单时,会调用上述 API,/by/admin。我们在 TicketServiceImpl 类中调用了 getAllTickets 方法。

我们使用了一个简单的 AOP 来验证名为 @AdminTokenRequired 的管理令牌。让我们看看这个 API 的实现部分。

The AdminTokenRequired interface

AdminTokenRequired 接口将是 我们的 实现的基础,我们将稍后覆盖:

package com.packtpub.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AdminTokenRequired {
}

在前面的代码中,我们介绍了验证管理员令牌的接口。验证方法将在 AdminTokenRequiredAspect 类中跟进。

The AdminTokenRequiredAspect class

在方面类中,我们将对管理员令牌进行 validation

package com.packtpub.aop;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.packtpub.service.SecurityServiceImpl;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
@Aspect
@Component
public class AdminTokenRequiredAspect { 
  @Before("@annotation(adminTokenRequired)")
  public void adminTokenRequiredWithAnnotation(AdminTokenRequired adminTokenRequired) throws Throwable{    
    ServletRequestAttributes reqAttributes = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
    HttpServletRequest request = reqAttributes.getRequest();    
    // checks for token in request header
    String tokenInHeader = request.getHeader("token");  
    if(StringUtils.isEmpty(tokenInHeader)){
      throw new IllegalArgumentException("Empty token");
    }    
    Claims claims = Jwts.parser()              .setSigningKey(DatatypeConverter.parseBase64Binary(SecurityServiceImpl.secretKey))
             .parseClaimsJws(tokenInHeader).getBody();    
    if(claims == null || claims.getSubject() == null){
      throw new IllegalArgumentException("Token Error : Claim is null");
    }    
    String subject = claims.getSubject();   
    if(subject.split("=").length != 2 || new Integer(subject.split("=")[1]) != 3){
      throw new IllegalArgumentException("User is not authorized");
    } 
  }
}

在前面的代码中,我们在 AdminTokenRequiredAspect 类中提供了令牌验证技术。此方面组件将在方法执行之前执行。此外,在此方法中,我们检查了令牌的空和 null 以及令牌的用户类型。

检查管理员对票证视图的 SoapUI 响应:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

如果我们使用了错误的令牌或空的令牌,我们将得到如下响应:

{
   "timestamp": 1515803861286,
   "status": 500,
   "error": "Internal Server Error",
   "exception": "java.lang.RuntimeException",
   "message": "User is not authorized",
   "path": "/ticket/by/admin"
}

通过保留 AOP 注释,我们可以在每个方法上保留几行代码,因为注释将处理业务逻辑。

Admin updates a ticket

创建票证后, 管理员可以查看它。与客户不同,管理员除了其内容外,还可以更好地控制更新工单状态和严重性。

Updating a ticket by admin – service (TicketServiceImpl)

在这里,我们将实现 方法 用于管理员更新票证:

  @ResponseBody
  @RequestMapping(value = "/by/admin", method = RequestMethod.PUT)
  public <T> T updateTicketByAdmin (
      @RequestParam("ticketid") final Integer ticketid,      
      @RequestParam(value="content") String content,
      @RequestParam(value="severity") Integer severity,
      @RequestParam(value="status") Integer status,      
      HttpServletRequest request,
      HttpServletResponse response
      ) {    
    User user = userSevice.getUserByToken(request.getHeader("token"));    
    if(user == null){
      return getUserNotAvailableError();
    }    
    ticketSevice.updateTicket(ticketid, content, severity, status);    
    Map<String, String> result = new LinkedHashMap<>();
    result.put("result", "updated");    
    return (T) result; 
  }

在前面的代码中,我们在 API 中使用了 /by/admin 路径来区分这个 API 和客户的更新方法。此外,我们从请求中获取严重性和状态参数。一旦管理员通过令牌验证,我们将调用 updateTicket 方法。如果你看到这个 updateTicket 方法,我们没有硬编码任何东西。

更新过程完成后,我们返回结果 "success" 作为响应,您可以在屏幕截图中查看:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Note

在实际应用中,管理员可能无法控制客户的内容,例如问题。但是,我们为管理员提供了一个选项来编辑内容以简化我们的业务逻辑。

Allowing admin to view a single ticket

由于管理员对工单具有完全的控制,因此他们还可以查看用户创建的任何单个工单。由于我们已经定义了 getTicket API /{ticketid},我们也可以将相同的 API 用于管理员查看目的。

Allowing admin to delete tickets

由于管理员拥有更多控制权,我们提供了一个无限的多次删除选项,供管理员在应用程序中删除。当管理员需要一次性删除一堆票时,这将非常方便。

Deleting tickets – service (TicketServiceImpl):

在下面的代码中,我们讨论管理员的多票删除选项:

  @Override
  public void deleteTickets(User user, String ticketids) {  
    List<String> ticketObjList = Arrays.asList(ticketids.split(","));    
    List<Integer> intList =
      ticketObjList.stream()
      .map(Integer::valueOf)
      .collect(Collectors.toList());     
    tickets.removeIf(x -> intList.contains(x.getTicketid()));
  }

在前面的代码中,我们赋予管理员删除多张工单的权力。由于管理员拥有完全控制权,因此我们在这里没有应用特定的过滤器。我们使用 Java Streams 获取票证作为列表,然后将它们与票证 ID 匹配以从票证列表中删除。

Deleting tickets by admin – API (ticket controller):

以下方法ticketids转发到对应的TicketServiceImpl 方法:

  @ResponseBody
  @AdminTokenRequired
  @RequestMapping(value = "/by/admin", method = RequestMethod.DELETE)
  public <T> T deleteTicketsByAdmin ( 
      @RequestParam("ticketids") final String ticketids,
      HttpServletRequest request
      )  {

    User user = userSevice.getUserByToken(request.getHeader("token"));

    ticketSevice.deleteTickets(user, ticketids);

    return Util.getSuccessResult(); 
  }

在前面的代码中,我们首先通过 @AdminTokenRequired 检查会话,然后在会话验证后删除票证。

我们可以用这个 SoapUI 截图检查 API 结果:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Note

在 multiple-ticket-delete 选项中,我们使用逗号分隔值来发送多个票证 ID。单个 ticketid 也可用于调用此 API。

CSR Ticket management


最后,我们在本节中讨论CSR Ticket 管理。 CSR 可能没有像管理员这样的控制;但是,在大多数情况下,他们可以选择在票务管理应用程序中匹配管理员。在下一节中,我们将讨论所有 CSR 授权的工单上的 CRUD 操作。

CSR updates a ticket

在本节中,我们讨论通过 CSR 在工单管理中使用新内容、严重性和状态更新工单:

  @ResponseBody
  @CSRTokenRequired
  @RequestMapping(value = "/by/csr", method = RequestMethod.PUT)
  public <T> T updateTicketByCSR (
      @RequestParam("ticketid") final Integer ticketid,     
      @RequestParam(value="content") String content,
      @RequestParam(value="severity") Integer severity,
      @RequestParam(value="status") Integer status,      
      HttpServletRequest request
      ) {    
    ticketSevice.updateTicket(ticketid, content, severity, status);    
    return Util.getSuccessResult(); 
  }

在前面的代码中,我们获取了所有必要的信息,例如内容、严重性和状态,并将这些信息提供给 updateTicket 方法。

我们使用了一个简单的 AOP 来验证名为 @CSRTokenRequired 的管理令牌。我们来看看这个 API 的实现部分。

CSRTokenRequired AOP

AdminTokenRequired 接口将是我们将要实现的 base通过以后:

package com.packtpub.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CSRTokenRequired {
}

在前面的代码中,我们引入了验证管理员令牌的注解。验证方法将在 CSRTokenRequiredAspect 类中跟进。

CSRTokenRequiredAspect

CSRTokenRequiredAspect 类中,我们将进行 admin token 的 validation

package com.packtpub.aop;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.packtpub.service.SecurityServiceImpl;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
@Aspect
@Component
public class CSRTokenRequiredAspect {  
  @Before("@annotation(csrTokenRequired)")
  public void adminTokenRequiredWithAnnotation(CSRTokenRequired csrTokenRequired) throws Throwable{    
    ServletRequestAttributes reqAttributes = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
    HttpServletRequest request = reqAttributes.getRequest();    
    // checks for token in request header
    String tokenInHeader = request.getHeader("token");    
    if(StringUtils.isEmpty(tokenInHeader)){
      throw new IllegalArgumentException("Empty token");
    }     
    Claims claims = Jwts.parser()              .setSigningKey(DatatypeConverter.parseBase64Binary(SecurityServiceImpl.secretKey))
             .parseClaimsJws(tokenInHeader).getBody();   
    if(claims == null || claims.getSubject() == null){
      throw new IllegalArgumentException("Token Error : Claim is null");
    }    
    String subject = claims.getSubject();    
    if(subject.split("=").length != 2 || new Integer(subject.split("=")[1]) != 2){
      throw new IllegalArgumentException("User is not authorized");
    } 
  }
}

在前面的代码中,我们在 CSRTokenRequiredAspect 类中提供了令牌验证技术。此方面组件将在方法执行之前执行。此外,在此方法中,我们检查令牌是否为空和 null 以及令牌的用户类型。

这是我们的 /ticket/{ticketid} 更新 API 的屏幕截图:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

CSR view all tickets

在查看所有工单方面,CSR 拥有 same 权限为 admin,所以我们不需要更改服务实现.但是,我们可能需要验证令牌以确保用户是 CSR。

Viewing all tickets by CSR – API (ticket controller)

当任何 CSR 调用 CSR 时,以下内容将获取 CSR 的所有 tickets

  @ResponseBody
  @CSRTokenRequired
  @RequestMapping("/by/csr")
  public <T> T getAllTicketsByCSR(HttpServletRequest request) {  
    return (T) ticketSevice.getAllTickets();
  }

在前面的 API 中,我们只使用了 @CSRTokenRequired 来验证用户。除了 API 路径和注释之外的所有内容都是相同的,因为管理员查看所有票证。

当我们查看 SoapUI 的截图时,我们可以清楚地看到客户创建的两张票。

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

CSR view single ticket

除了多删除选项之外,CSR 与 admin 具有相同的 rights,我们可以使用相同的 /{ticketid},我们在此处用于 CSR 和管理员查看单票 API。

CSR delete tickets

通过 CSR 删除 tickets 几乎就像在管理员模式下删除门票一样。但是,我们的业务需求表明,CSR 一次删除的工单不能超过三个。我们会将特定的逻辑添加到我们现有的方法中。

Deleting tickets – service (TicketServivceImpl)

下面是 CSR 删除多个 tickets 的服务实现:

  @Override
  public void deleteTickets(User user, String ticketids) {   
    List<String> ticketObjList = Arrays.asList(ticketids.split(","));   
    if(user.getUsertype() == 2 && ticketObjList.size() > 3){
      throw new RuntimeException("CSR can't delete more than 3 tickets");
    }    
    List<Integer> intList =
      ticketObjList.stream()
      .map(Integer::valueOf)
      .collect(Collectors.toList())
        ;     
    tickets.removeIf(x -> intList.contains(x.getTicketid()));
  }

为了删除多票,我们使用了 TicketServiceImpl 类中的现有代码。但是,根据我们的业务需求,我们的 CSR 不能删除超过三个票证,因此我们添加了额外的逻辑来检查票证大小。如果票证列表大小超过三个,我们会抛出异常,否则我们将删除这些票证。

Deleting tickets by CSR – API (ticket controller)

在 API 中,我们简单地调用我们实现的deleteTickets方法早些时候:

  @ResponseBody
  @CSRTokenRequired
  @RequestMapping(value = "/by/csr", method = RequestMethod.DELETE)
  public <T> T deleteTicketsByCSR (
      @RequestParam("ticketids") final String ticketids,     
      HttpServletRequest request,
      HttpServletResponse response
      ) {    
    User user = userSevice.getUserByToken(request.getHeader("token"));    
    ticketSevice.deleteTickets(user.getUserid(), ticketids);    
    Map<String, String> result = new LinkedHashMap<>();
    result.put("result", "deleted");    
    return (T) result; 
  }

除了删除选项的最大票证限制外,CSR 删除票证不需要大的更改。但是,我们在 API 中添加了 @CSRTokenRequired 注释。

这是 SoapUI for CSR 删除多张工单的截图:

读书笔记《building-restful-web-services-with-spring-5-second-edition》票证管理-高级CRUD

Note

Postman 工具的 DELETE 选项可能存在问题,包括参数(从 5.4.0 版本开始),当您在管理员和企业社会责任。对于此类场景,请使用 SoapUI 客户端。

Summary


在最后一章中,我们通过满足本章第一节中提到的所有业务需求实现了一个小型票务管理系统。此实施涵盖客户、CSR 和管理员的工单 CRUD 操作。此外,我们的实现也满足了业务需求,例如为什么 CSR 不能一次删除超过三个工单。