在之前学习RestController的时候,其实已经将原来的项目的查询功能对外提供了API。这次先来看看API的设计思路,然后继续升级其他部分的API。
REST API 设计
REST API设计是一个很广泛的话题,与业务关联很大,一般有如下步骤:
- 了解API的相关需求
以我们的增删改查项目来说,REST API要实现的功能是完整的增删改查功能。而像天气预报网站,可能只有查的功能。
- 了解要提供的资源,这个阶段主要关注名词,即网站服务要对外提供什么信息,然后将这个名词转换成程序的数据对象。以我们的增删改查项目为例,要提供的是Customer这个对象及其集合的信息。
- 设计HTTP请求类型对应的动作,之前有一张表,这里不再赘述,看一下设计好的API与对应动作的关系:
HTTP请求 |
路径 |
操作 |
POST |
/api/customers |
创建新用户 |
GET |
/api/customers |
获取所有用户列表 |
GET |
/api/customers/{customerId} |
获取单个用户 |
PUT |
/api/customers |
更新已经存在的用户 |
DELETE |
/api/customers/{customerId} |
删除指定的用户 |
对于POST和PUT请求,需要在请求体内包含JSON字符串。
这样设计完之后,很清晰,注意不要在路径中包含动作,以下是一些设计不好的API:
- /api/customersList
- /api/deleteCustomer
- /api/addCustomer
- /api/updateCustomer
要用HTTP的请求方法来作为动作要素,API内一般只包括名词。
现实中也有很多例子可供参考,这里有一些知名网站的API设计可供参考:
- Paypal的API指南
- Github
- Salesforce
升级增删改查项目
之前设计好了API,可以发现,在刚才已经完成了对应API的查询方式。而且我们也定义了自己的异常转JSON类,自定义异常类和普通的异常处理方法。
所以现在可以专注于API的编写,其实异常还可以在增删改查的具体过程中再细化,不过这个都是之后加强的内容了。先来编写添加用户的功能。
添加用户
在添加用户的时候,我们需要发送POST请求附带一个JSON字符串到/api/customers
,由于是一个新用户,所以无需附带id。
一般在添加成功之后,需要返回这个新添加的对象,但是包含id,这样客户端就可以知道添加成功了。
在之前使用的GET方法,是返回一个对象即可。Jackson可以自动转换从请求体中获取数据,使用@RequestBody
注解来绑定一个POJO。由于我们的Service层和DAO层都配好了,因此实际只需编写一个控制器:
@PostMapping("/customers")
public Customer addCustomer(@RequestBody Customer customer) {
customer.setId(0);
customerService.saveCustomer(customer);
return customer;
}
这个控制器注解为只接受POST方法,然后由于传入的数据无需带有id,就将id设置为了0,这是因为在DAO层使用了Hibernate的.saveOrUpdate(...)
方法,所以给个没有的id就行了。在保存了以后,这个对象就和session有了关联,于是就有了id。
之后我们采用Postman给这个地址发送POST请求。在发送的时候,选择左侧的POST请求,贴上地址,然后在body里输入一串JSON:
{"firstName":"Saner","lastName":"Penguine","email":"saner@gmail.com"}
之后要记得点选上边raw,然后选择右侧的JSON格式,这样会在头部信息里附加Content-type: application/json
,这个很重要,否则发过去的就是纯文本,没有用处。
发送之后,可以看到返回的JSON字符串中带有了id信息,再访问获取该用户或者列表的页面,就可以看到该用户已经被添加到数据库。
修改用户
修改用户需要使用PUT请求,此外数据中必须有id,无需将其设置为0,这样Hibernate就可以自动更新了。
控制器方法也很简单:
@PutMapping("/customers")
public Customer updateCustomer(@RequestBody Customer customer) {
Customer customer1 = customerService.getCustomer(customer.getId());
if (customer1 == null) {
throw new CustomerNotfoundError("Customer with id " + customer.getId() + " NOT FOUND!");
}
customerService.saveCustomer(customer);
System.out.println(customer);
System.out.println(customer1);
return customer;
}
这里先通过ID检索对象,如果找不到,就说明没有该对象,抛错误。如果有,就更新。这里先去检索就可以获得更新前和更新后的对象。
删除用户
DELETE请求发送用户即可。其实逻辑和PUT一样,也是先判断该id存在与否,再进行删除。
@DeleteMapping("/customers/{customerId}")
public Customer deleteCustomer(@RequestBody Customer customer) {
Customer oldCustomer = customerService.getCustomer(customer.getId());
if (oldCustomer == null) {
throw new CustomerNotfoundError("Customer with id " + customer.getId() + " NOT FOUND!");
}
customerService.deleteCustomer(customer.getId());
return oldCustomer;
}
可见Spring容器解耦方面的威力,在不修改任何其他业务逻辑的情况下,就完成了添加增删改查REST API。
REST的安全问题
这里还涉及到安全问题,很显然,目前我们的程序只要知道了我们API的规则,就可以进行操作。在实际生活中是不可能允许如此操作的,尤其是删除操作。
一般有两种做法,一是网站会需要你注册然后获取一个KEY,这个KEY必须包含在每次的请求中,用于验证身份。二是网站基于角色的权限管理,必须有对应权限才能操作。
我们这里采用使用Spring Security基于角色的管理来解决安全问题。开发方法也很熟悉了:
- Maven导入Spring Security
- 配置Security从数据库中读取用户和角色
- 做好登录界面即可
实际上就是把我们之前编写的登录和新创建用户结合起来即可。
添加相关依赖
添加依赖唯一要注意的是Spring 各个组件之间的兼容情况。因为Spring Security基于Filter技术,还需要配置Servlet相关的内容。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
</dependency>
因为Spring Framework的版本是5.1.5版,这里就使用了5.1.4版本的Spring Security。
然后来配置Spring Security。首先是创建Spring的初始化器,老样子:
package cc.conyli.config;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebAppInitializer extends AbstractSecurityWebApplicationInitializer {
}
这样就启动了Spring Security,然后需要进行配置。
配置Spring Security
这里为了简单,我们就直接用内存中存储数据的方式,其他配置也尽量简化:
package cc.conyli.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//该方法在5.0版本中还可以用,在5.1.5版本中提示已过期,最好仅用于简单应用。
User.UserBuilder users = User.withDefaultPasswordEncoder();
//直接在内存中保存验证数据
auth.inMemoryAuthentication()
.withUser(users.username("john").password("test123").roles("EMPLOYEE"))
.withUser(users.username("mary").password("test123").roles("EMPLOYEE", "MANAGER"))
.withUser(users.username("susan").password("test123").roles("EMPLOYEE", "ADMIN"));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 把所有REST API路径都设置成需要认证
http.authorizeRequests()
.antMatchers("/api/customers/**").authenticated()
.and()
.httpBasic()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Spring 5默认开启CSRF验证。这个里边关闭了CSRF验证和session验证,因为一般提供给程序用的不需要CSRF验证。而session中不保存数据,就要求每次API请求都要验证身份,这也是类似于使用API KEY的方法。
测试项目
使用Postman访问,需要在Authorization中选上Basic Auth,然后在右侧输入用户名和密码,就可以正常访问,如果不勾选Auth或者用户名密码填写错误,就会得到401错误响应。
如果使用现代浏览器访问的话,在访问成功之后无需反复输入密码,这不是因为我们代码错误,而是现代浏览器具有保存认证凭据的功能。但Postman每次就是发无状态的请求,所以每次都必须带上密码。
在完成了最基础的认证之后,如果想要进一步按照角色来配置可操作的API,只要按照如下方式配置即可:
.antMatchers(HttpMethod.GET, "/api/customers").hasRole("EMPLOYEE")
.antMatchers(HttpMethod.GET, "/api/customers/**").hasRole("EMPLOYEE")
.antMatchers(HttpMethod.POST, "/api/customers").hasAnyRole("MANAGER", "ADMIN")
.antMatchers(HttpMethod.POST, "/api/customers/**").hasAnyRole("MANAGER", "ADMIN")
.antMatchers(HttpMethod.PUT, "/api/customers").hasAnyRole("MANAGER", "ADMIN")
.antMatchers(HttpMethod.PUT, "/api/customers/**").hasAnyRole("MANAGER", "ADMIN")
.antMatchers(HttpMethod.DELETE, "/api/customers/**").hasRole("ADMIN")
如果愿意的话,还可以继续把用户注册的内容等移植过来。