Spring 33 Spring REST - REST增删改查及安全认证

Spring 33 Spring REST - REST增删改查及安全认证

在之前学习RestController的时候,其实已经将原来的项目的查询功能对外提供了API。这次先来看看API的设计思路,然后继续升级其他部分的API。 REST API 设计 REST API设计是一个很广泛的话题,与业务关联很大,一般有如下步骤: 了解API的相关需求 以我们的增删改查项目来说

在之前学习RestController的时候,其实已经将原来的项目的查询功能对外提供了API。这次先来看看API的设计思路,然后继续升级其他部分的API。

REST API 设计

REST API设计是一个很广泛的话题,与业务关联很大,一般有如下步骤:
  1. 了解API的相关需求 以我们的增删改查项目来说,REST API要实现的功能是完整的增删改查功能。而像天气预报网站,可能只有查的功能。
  2. 了解要提供的资源,这个阶段主要关注名词,即网站服务要对外提供什么信息,然后将这个名词转换成程序的数据对象。以我们的增删改查项目为例,要提供的是Customer这个对象及其集合的信息。
  3. 设计HTTP请求类型对应的动作,之前有一张表,这里不再赘述,看一下设计好的API与对应动作的关系:
    HTTP请求 路径 操作
    POST /api/customers 创建新用户
    GET /api/customers 获取所有用户列表
    GET /api/customers/{customerId} 获取单个用户
    PUT /api/customers 更新已经存在的用户
    DELETE /api/customers/{customerId} 删除指定的用户
    对于POST和PUT请求,需要在请求体内包含JSON字符串。
这样设计完之后,很清晰,注意不要在路径中包含动作,以下是一些设计不好的API:
  1. /api/customersList
  2. /api/deleteCustomer
  3. /api/addCustomer
  4. /api/updateCustomer
要用HTTP的请求方法来作为动作要素,API内一般只包括名词。 现实中也有很多例子可供参考,这里有一些知名网站的API设计可供参考:
  1. Paypal的API指南
  2. Github
  3. 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基于角色的管理来解决安全问题。开发方法也很熟悉了:
  1. Maven导入Spring Security
  2. 配置Security从数据库中读取用户和角色
  3. 做好登录界面即可
实际上就是把我们之前编写的登录和新创建用户结合起来即可。

添加相关依赖

添加依赖唯一要注意的是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")
如果愿意的话,还可以继续把用户注册的内容等移植过来。
LICENSED UNDER CC BY-NC-SA 4.0
Comment