现在我们知道了如何使用 servlet 类创建 Web 应用程序。我们知道如何获取用户输入、如何访问数据库以及如何处理用户登录。但是如果我们想要支持不同类型的程序而不仅仅是一个网络应用程序呢?如果我们要创建桌面应用程序或 Android 应用程序怎么办?我们如何为这些程序提供对我们数据的访问,而无需每次都从头开始编写所有内容?
本教程介绍了创建 REST API 的想法,这是一种组织代码的方式,因此我们可以从多个应用程序访问我们的数据。您的 REST API 是服务器代码,其工作是提供对您的数据的访问并执行诸如谁可以看到什么等规则。然后其他程序使用您的 REST API 与您的数据进行交互。
这个高级图表显示了您可以如何组织代码:您将拥有一个数据库(或多个数据库),而您的 REST API 将位于该数据库之上。它将使用 SQL 和 JDBC 与数据库交互,就像我们已经了解的一样。然后其他应用程序将调用您的 REST API,这使您可以将所有核心逻辑集中在一个地方,而不是每次想要创建新应用程序时都重写它。
换句话说:REST API 只是一个 Web 应用程序,与我们已经构建的所有 Web 应用程序非常相似。GET 唯一的区别是它提供数据而不是显示请求的网站。它不是使用 HTML 表单来创建POST 请求,而是接受POST 来自其他应用程序的请求!(当然,其中一个应用程序可能是另一个使用 HTML 表单获取用户输入的 Web 应用程序!)
REST
REST 代表representational?s?state?transfer,它只是一组规则的花哨名称,您可以遵循这些规则来构建一个 Web 应用程序,该应用程序以一种可被多个应用程序或多个相同用户重用的方式提供对数据的访问应用。REST 实际上并不涉及任何新的技术概念。它更多地是使用我们已经学过的概念的一种方式。
REST 背后有一些基本思想:
- 您通过 URL 访问数据。例如,
/users/Ada 允许您访问有关名为 Ada 的人的数据。 - 您使用 HTTP 方法来访问或更改数据。例如,您可以通过向 发出
GET 请求来查看 Ada 的数据,并通过向 发出请求来修改/people/Ada Ada 的数据。您也可以使用其他 HTTP 方法(如或)与数据进行交互。POST /people/Ada PUT DELETE - 您可以随心所欲地表示数据。例如,该
GET 请求可能会返回代表用户数据的 JSON 字符串。然后该POST 请求可能会采用一个JSON 字符串。或者它可以采用二进制字符串、XML 或属性列表。由你决定。 - 每个请求都应该是独立的。换句话说,您不应该在服务器上存储会话信息!完成请求所需的一切都必须包含在请求本身中!
所有这些“规则”的存在都是有原因的,但重要的是要记住,最终,一切都取决于你。你是程序员。如果你的代码“违反”了这些规则之一,REST 警察不会来敲你的门。您应该将 REST 视为一种工具,而不是您必须不惜一切代价遵守的一套严格的规则。做对你有意义的事情,做适合你的事情。
API 代表应用程序编程器接口,对于程序员用来与语言或库进行交互的任何东西,它都是一个花哨的名称。例如,Processing 的引用是一个 API:它是我们用来编写 Processing 代码的类和函数。同样,Java API是我们用来编写 Java 代码的类和函数的列表。你可以在MDN上查看 JavaScript 的 API?。关键是 API 是我们在编写代码时可以做的事情的集合。所以当我们说我们正在创建一个 REST API 时,我们只是意味着我们正在使用 REST 思想来创建程序员可以用来与我们的数据交互的东西。
简单示例 REST API
让我们使用所有这些想法来创建一个 REST API。首先,假设我们有一个类可以访问我们的数据:
import java.util.HashMap;
import java.util.Map;
/**
* Example DataStore class that provides access to user data.
* Pretend this class accesses a database.
*/
public class DataStore {
//Map of names to Person instances.
private Map<String, Person> personMap = new HashMap<>();
//this class is a singleton and should not be instantiated directly!
private static DataStore instance = new DataStore();
public static DataStore getInstance(){
return instance;
}
//private constructor so people know to use the getInstance() function instead
private DataStore(){
//dummy data
personMap.put("Ada", new Person("Ada", "Ada Lovelace was the first programmer.", 1815));
personMap.put("Kevin", new Person("Kevin", "Kevin is the author of HappyCoding.io.", 1986));
personMap.put("Stanley", new Person("Stanley", "Stanley is Kevin's cat.", 2007));
}
public Person getPerson(String name) {
return personMap.get(name);
}
public void putPerson(Person person) {
personMap.put(person.getName(), person);
}
}
此示例使用 aMap 将数据存储在内存中,但这也可以很容易地使用数据库连接。关键是这个类提供了对我们数据的访问。它使用一个简单的Person 类:
public class Person {
private String name;
private String about;
private int birthYear;
public Person(String name, String about, int birthYear) {
this.name = name;
this.about = about;
this.birthYear = birthYear;
}
public String getName() {
return name;
}
public String getAbout() {
return about;
}
public int getBirthYear() {
return birthYear;
}
}
这只是一个普通的旧 Java 类,其中包含用于访问这些变量的变量和函数。现在我们可以创建我们的 servlet 类:
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
public class PersonServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String requestUrl = request.getRequestURI();
String name = requestUrl.substring("/people/".length());
Person person = DataStore.getInstance().getPerson(name);
if(person != null){
String json = "{\n";
json += "\"name\": " + JSONObject.quote(person.getName()) + ",\n";
json += "\"about\": " + JSONObject.quote(person.getAbout()) + ",\n";
json += "\"birthYear\": " + person.getBirthYear() + "\n";
json += "}";
response.getOutputStream().println(json);
}
else{
//That person wasn't found, so return an empty JSON object. We could also return an error.
response.getOutputStream().println("{}");
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String name = request.getParameter("name");
String about = request.getParameter("about");
int birthYear = Integer.parseInt(request.getParameter("birthYear"));
DataStore.getInstance().putPerson(new Person(name, about, birthYear));
}
}
这个 servlet 类包含一个doGet() 函数,该函数从 URL 中获取一个人的姓名,然后使用DataStore 该类来获取那个人。然后它从该人的数据中创建一个 JSON 字符串,并将该 JSON 作为对请求的响应返回GET 。(如果您不记得 JSON,请查看JSON 教程。)此代码使用json.org?Java 库在将String 值添加到我们的 JSON 之前对其进行转义。这个很重要!请记住,JSON 使用用引号括起来的键和值,如下所示:
{
"name": "Ada",
"about": "Ada Lovelace was the first programmer.",
"birthYear": 1815
}
但是如果name 或about 包含引号怎么办?我们会有这样的事情:
{
"name": "Ada",
"about": "This contains " a quote",
"birthYear": 1815
}
这不是有效的 JSON,当您尝试解析它时会出现错误。我们需要转义引号,使其看起来像这样:
{
"name": "Ada",
"about": "This contains \" a quote",
"birthYear": 1815
}
转义的引号\" 被视为不会破坏 JSON 格式的单个字符。JSONObject.quote() 我们正在使用 JSON 库通过函数为我们处理这种情况。
然后该doPost() 函数从POST 请求中获取三个参数,并使用它们向DataStore 类添加数据。
最后,我们只需要一个web.xml 文件来将我们的 servlet 映射到一个 URL:
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>PersonServlet</servlet-name>
<servlet-class>PersonServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PersonServlet</servlet-name>
<url-pattern>/people/*</url-pattern>
</servlet-mapping>
</web-app>
/people/ 这会将任何以我们的 servlet开头的 URL 映射。尝试在本地服务器上运行它,然后在浏览器中导航到http://localhost:8080/people/Ada 。您应该看到 JSON 表示:
这不是很令人兴奋,但这不是重点。无论如何,您实际上不应该在浏览器中查看 REST API。重点是您可以使用这些数据来构建其他应用程序!
简单的 Java REST 客户端
关于如何构建其中一个应用程序(也称为客户端)的详细信息略超出本教程,本教程的目标是向您展示如何创建 REST API,而不是客户端。但只是为了向您展示所有内容如何组合在一起,这是一个非常基本的命令行程序,它使用我们的 REST API 来获取或设置用户数据:
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Scanner;
import org.json.JSONObject;
public class RestApiClient {
public static void main(String[] args) throws IOException{
Scanner scanner = new Scanner(System.in);
System.out.println("Welcome to the Person Info Command Line Editor.");
System.out.println("(PICLER for short.)");
System.out.println("Do you want to get or set a person's info?");
System.out.println("(Type 'get' or 'set' now.)");
String getOrSet = scanner.nextLine();
if("get".equalsIgnoreCase(getOrSet)){
System.out.println("Whose info do you want to get?");
System.out.println("(Type a person's name now.)");
String name = scanner.nextLine();
String jsonString = getPersonData(name);
JSONObject jsonObject = new JSONObject(jsonString);
int birthYear = jsonObject.getInt("birthYear");
System.out.println(name + " was born in " + birthYear + ".");
String about = jsonObject.getString("about");
System.out.println(about);
}
else if("set".equalsIgnoreCase(getOrSet)){
System.out.println("Whose info do you want to set?");
System.out.println("(Type a person's name now.)");
String name = scanner.nextLine();
System.out.println("When was " + name + " born?");
System.out.println("(Type a year now.)");
String birthYear = scanner.nextLine();
System.out.println("Can you tell me about " + name + "?");
System.out.println("(Type a sentence now.)");
String about = scanner.nextLine();
setPersonData(name, birthYear, about, password);
}
scanner.close();
System.out.println("Thanks for using PICLER.");
}
public static String getPersonData(String name) throws IOException{
HttpURLConnection connection = (HttpURLConnection) new URL("http://localhost:8080/people/" + name).openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if(responseCode == 200){
String response = "";
Scanner scanner = new Scanner(connection.getInputStream());
while(scanner.hasNextLine()){
response += scanner.nextLine();
response += "\n";
}
scanner.close();
return response;
}
// an error happened
return null;
}
public static void setPersonData(String name, String birthYear, String about) throws IOException{
HttpURLConnection connection = (HttpURLConnection) new URL("http://localhost:8080/people/" + name).openConnection();
connection.setRequestMethod("POST");
String postData = "name=" + URLEncoder.encode(name);
postData += "&about=" + URLEncoder.encode(about);
postData += "&birthYear=" + birthYear;
connection.setDoOutput(true);
OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream());
wr.write(postData);
wr.flush();
int responseCode = connection.getResponseCode();
if(responseCode == 200){
System.out.println("POST was successful.");
}
else if(responseCode == 401){
System.out.println("Wrong password.");
}
}
}
该程序询问用户是否要获取或设置用户数据。如果他们想获取数据,那么程序从用户那里获取一个名称,然后调用该getPersonData() 函数。该函数使用HttpUrlConnection 类,它只是标准 API 中可用的常规 Java 类。该类允许您向服务器发出请求,并且该getPersonData() 函数使用它向GET 我们的 REST API 发出请求(确保在运行此代码时您的服务器正在运行)。响应是我们的 REST API 输出的 JSON,然后该客户端程序使用 JSON 库对其进行解析以输出到命令行。
如果用户想要设置数据,那么程序会从用户那里得到一个名字、出生年份和一个关于的句子,然后调用该setPersonData() 函数。该函数使用HttpUrlConnection 该类向我们的服务器发送POST 请求。代码使用该URLEncoder.encode() 函数对我们的String 值进行编码,以防它们包含像& & 符号这样会破坏数据格式的字符。然后我们的服务器处理该请求并存储数据。
Welcome to the Person Info Command Line Editor.
(PICLER for short.)
Do you want to get or set a person's info?
(Type 'get' or 'set' now.)
get
Whose info do you want to get?
(Type a person's name now.)
Ada
Ada was born in 1815.
Ada Lovelace was the first programmer.
Thanks for using PICLER.
我们现在有两个独立的应用程序:在服务器中运行的 REST API,以及在命令行中运行的客户端应用程序。我们可以使用非常相似的代码来创建桌面应用程序或 Android 应用程序,甚至是另一个提供用户界面的服务器。
简单的 JavaScript REST 客户端
同样,这是一个简单的 HTML 网页,它使用 JavaScript 使用我们的 REST API 获取或设置人员数据:
<!DOCTYPE html>
<html>
<head>
<title>PIJON</title>
<script>
function getPersonInfo(){
var name = document.getElementById('name').value;
var ajaxRequest = new XMLHttpRequest();
ajaxRequest.onreadystatechange = function(){
if(ajaxRequest.readyState == 4){
if(ajaxRequest.status == 200){
var person = JSON.parse(ajaxRequest.responseText);
document.getElementById('birthYear').value = person.birthYear;
document.getElementById('about').value = person.about;
}
}
}
ajaxRequest.open('GET', 'http://localhost:8080/people/' + name);
ajaxRequest.send();
}
function setPersonInfo(){
var name = document.getElementById('name').value;
var about = document.getElementById('about').value;
var birthYear = document.getElementById('birthYear').value;
var postData = 'name=' + name;
postData += '&about=' + encodeURIComponent(about);
postData += '&birthYear=' + birthYear;
var ajaxRequest = new XMLHttpRequest();
ajaxRequest.open('POST', 'http://localhost:8080/people/' + name);
ajaxRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
ajaxRequest.send(postData);
}
</script>
</head>
<body>
<h1>PIJON</h1>
<p>(Person Info in JavaScript Object Notation)</p>
<p><input type="text" value="Ada" id="name"><button type="button" onclick="getPersonInfo()">Get</button></p>
<p>Birth year:</p>
<input type="text" id="birthYear">
<p>About:</p>
<textarea id="about"></textarea>
<p><button type="button" onclick="setPersonInfo()">Save</button></p>
</body>
</html>
此网页显示三个输入框。如果用户键入名称并按下Get 按钮,该getPersonInfo() 函数将使用 AJAXGET 向我们的 REST API 发出请求。然后它解析 JSON 响应并填充其他文本框。如果用户随后修改了这些文本框并单击了Save 按钮,该setPersonInfo() 函数将使用 AJAXPOST 向我们的 REST API 发出请求。
尝试在 JavaScript 客户端中进行更改,然后在命令行应用程序中查看它!
CORS
根据您的设置,您在尝试运行上述 JavaScript 时可能会遇到错误。就像是:
XMLHttpRequest cannot load http://localhost:8080/people/Ada. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
这是因为默认情况下,不允许一台服务器上的 JavaScript 访问另一台服务器上的内容。在我们的例子中,我们的 JavaScript 甚至不在服务器上(我们只是.html 在浏览器中打开了文件),而我们的 REST API 位于localhost:8080 .?关键是我们的 JavaScript 不允许访问我们的服务器。
为了解决这个问题,我们需要在我们的服务器中启用跨域资源共享或 CORS。如何执行此操作取决于您使用的服务器。因此,如果您的真正托管是在 AWS Elastic Beanstalk 上,您可能想要在 Google 上搜索“AWS Elastic Beanstalk enable CORS”之类的内容。
我们使用的是本地 Jetty 服务器,因此要首先启用 CORS,我们需要将jetty-servlets.jar 和添加jetty-util.jar 到我们的类路径中。然后我们需要在我们的web.xml 文件中添加一个过滤器:
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>PersonServlet</servlet-name>
<servlet-class>PersonServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PersonServlet</servlet-name>
<url-pattern>/people/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>cross-origin</filter-name>
<filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
<init-param>
<param-name>allowedOrigins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>allowedMethods</param-name>
<param-value>GET,POST</param-value>
</init-param>
<init-param>
<param-name>allowedHeaders</param-name>
<param-value>X-Requested-With,Content-Type,Accept,Origin,Authorization</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>cross-origin</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
<filter> 和标签为我们的<filter-mapping> 本地服务器启用 CORS。
同样,如何为服务器启用 CORS 取决于您使用的服务器!
处理 REST 中的登录
我们已经了解到我们可以使用会话在我们的服务器上存储每个用户的信息,这使我们能够处理诸如登录之类的事情。但是在创建 REST API 时,每个请求都应该是独立的,这意味着我们不应该在服务器上存储每个用户的信息。换句话说,我们不应该使用会话来跟踪 REST 服务器中的用户登录。
但是,如果我们想限制对某些数据的访问呢?例如,如果我们只希望用户能够编辑自己的信息怎么办?如果我们可以在服务器上存储每个用户的数据,我们就可以跟踪用户是否登录。但是由于我们不能在服务器上存储数据,我们必须做些别的事情。
我们通过在每个请求中发送用户的登录信息(用户名和密码)来解决这个问题。
为了证明这一点,让password 我们在我们的类中添加一个字段Person :
public class Person {
private String name;
private String about;
private int birthYear;
private String password;
public Person(String name, String about, int birthYear, String password) {
this.name = name;
this.about = about;
this.birthYear = birthYear;
this.password = password;
}
public String getName() {
return name;
}
public String getAbout() {
return about;
}
public int getBirthYear() {
return birthYear;
}
public String getPassword(){
return password;
}
}
(请记住,在现实生活中,我们不会直接存储密码;我们会存储密码的哈希值。)
DataStore 然后让我们在类中为我们的假虚拟数据添加一些密码
personMap.put("Ada", new Person("Ada", "Ada Lovelace was the first programmer.", 1815, "password one"));
personMap.put("Kevin", new Person("Kevin", "Kevin is the author of HappyCoding.io.", 1986, "password two"));
personMap.put("Stanley", new Person("Stanley", "Stanley is Kevin's cat.", 2007, "password three"));
在现实生活中,您会在用户注册时从用户那里获得这些密码(同样是密码的哈希值)。
最后,让我们改变一下doPost() 我们PersonServlet 类的功能:
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String name = request.getParameter("name");
String about = request.getParameter("about");
String password = request.getParameter("password");
int birthYear = Integer.parseInt(request.getParameter("birthYear"));
Person personToEdit = DataStore.getInstance().getPerson(name);
if(personToEdit != null && personToEdit.getPassword().equals(password)){
DataStore.getInstance().putPerson(new Person(name, about, birthYear, password));
}
else{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
现在此函数从请求中获取密码,并在提交编辑之前检查以确保其有效。如果无效,则返回错误代码。
这只是一个简单的示例,向您展示基础知识,但对于更复杂的项目,想法是相同的:您在每个请求中传递登录数据,服务器在进行任何处理之前对其进行检查。
请记住,GET 请求通常具有通过 URL 本身传递的参数,通常是通过查询参数。因此,如果我们想让用户只能查看自己的信息而不能查看其他用户的信息,我们必须使用这样的 URL:
http://localhost:8080/people/Ada?password=MyPasswordHere
我们可以采用这种方法并修改doGet() 我们类中的函数以PeopleServlet 从查询参数中获取密码并在返回任何用户数据之前对其进行检查。然后我们将修改我们的 REST 客户端应用程序以构建 URL,使其包含查询参数。这与我们为请求所做的非常相似POST ,只是我们在 URL 中使用查询参数而不是请求正文中的数据。
没有什么能阻止我们这样做,它会奏效。但一般来说,通过 URL 传递登录信息被认为是一个坏主意。URL 可能会被缓存或记录,这可能使黑客能够获取我们的登录信息。从设计的角度来看,它有点混乱。这是相当主观的,但我们将登录信息与请求信息混合在一起,这并不理想。我们想将请求(what)与请求信息(how)分开。换句话说,我们不想将登录信息混入我们的请求 URL。
大多数 REST API 不是将登录信息放在 URL 本身中,而是使用授权标头来处理登录信息。请求标头允许请求包含有关自身的信息:浏览器版本和缓存数据等信息。授权标头只是与请求一起出现的用户名和密码。
我们是编写代码的人,所以我们如何表示用户名和密码取决于我们自己。但传统上,它表示为64 位编码的字符串。这听起来可能很复杂,但这只是一种存储用户名和密码的方式,因此它不会弄乱请求的格式,就像我们已经在上面的代码中看到URLEncoder.encode() 的那样。JSONObject.quote() 我们可以使用Base64 Java 附带的标准类来做到这一点。
在一个 servlet 函数中,首先我们得到authorization 头文件:
String authHeader = request.getHeader("authorization");
该authHeader 变量将包含类似Basic dXNlcm5hbWU6cGFzc3dvcmQ= 的内容,它告诉我们如何进行身份验证(Basic 仅表示使用用户名和密码),然后是以 base 64 编码的用户名和密码。接下来我们需要隔离编码部分,我们可以使用substring() 功能:
String encodedAuth = authHeader.substring(authHeader.indexOf(' ') + 1);
这得到了String 在空格之后开始的部分,它只给了我们编码的用户名和密码。接下来我们需要对编码后的值进行解码:
String decodedAuth = new String(Base64.getDecoder().decode(encodedAuth));
这解码dXNlcm5hbWU6cGFzc3dvcmQ= 为类似username:password .?所以现在我们需要将用户名和密码分开:
String username = decodedAuth.substring(0, decodedAuth.indexOf(':'));
String password = decodedAuth.substring(decodedAuth.indexOf(':')+1);
这使用该substring() 函数将冒号String 之前和之后的部分分开。: 请注意,这意味着您的用户名不应包含冒号!
把它们放在一起,它看起来像这样:
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String authHeader = request.getHeader("authorization");
String encodedAuth = authHeader.substring(authHeader.indexOf(' ')+1);
String decodedAuth = new String(Base64.getDecoder().decode(encodedAuth));
String username = decodedAuth.substring(0, decodedAuth.indexOf(':'));
String password = decodedAuth.substring(decodedAuth.indexOf(':')+1);
String nameToEdit = request.getParameter("name");
String about = request.getParameter("about");
int birthYear = Integer.parseInt(request.getParameter("birthYear"));
Person personToEdit = DataStore.getInstance().getPerson(nameToEdit);
Person loggedInPerson = DataStore.getInstance().getPerson(username);
//make sure user is in our data
if(personToEdit == null || loggedInPerson == null){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//don't let users edit other users
if(!nameToEdit.equals(loggedInPerson.getName())){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//make sure password is valid
//use hashed passwords in real life!
if(!password.equalsIgnoreCase(loggedInPerson.getPassword())){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//if we made it here, everything is okay, save the user
DataStore.getInstance().putPerson(new Person(nameToEdit, about, birthYear, password));
}
现在我们的doPost() 函数期望用户名和密码在请求标头中以base?64编码。authorization 它仍然希望数据在请求正文中,并使用该request.getParameter() 函数来获取这些内容。然后它使用几个if 语句来确保登录信息符合要求。如果不是,则返回错误。如果登录信息有效,那么它将来自请求的信息存储在DataStore 类中(这可能会进入现实生活中的数据库)。
在我们的 REST 客户端中,我们必须做同样的事情,只是反过来:
String authString = name + ":" + password;
String encodedAuth = Base64.getEncoder().encodeToString(authString.getBytes());
String authHeader = "Basic " + encodedAuth;
connection.setRequestProperty ("Authorization", authHeader);
此代码采用用户名和密码,并通过简单地将它们与: 中间的冒号组合来从中创建一个授权字符串。然后它以 base 64 对该授权字符串进行编码。然后它通过添加Basic 到开头来创建一个标题字符串,因此服务器知道需要一个用户名和密码。最后,它设置Authorization 要发送到服务器的标头。
把它们放在一起,它看起来像这样:
public static void setPersonData(String name, String birthYear, String about, String password) throws IOException{
HttpURLConnection connection = (HttpURLConnection) new URL("http://localhost:8080/people/" + name).openConnection();
connection.setRequestMethod("POST");
String authString = name + ":" + password;
String encodedAuth = Base64.getEncoder().encodeToString(authString.getBytes());
String authHeader = "Basic " + encodedAuth;
connection.setRequestProperty ("Authorization", authHeader);
String postData = "name=" + URLEncoder.encode(name);
postData += "&about=" + URLEncoder.encode(about);
postData += "&birthYear=" + birthYear;
connection.setDoOutput(true);
OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream());
wr.write(postData);
wr.flush();
int responseCode = connection.getResponseCode();
if(responseCode == 200){
System.out.println("POST was successful.");
}
else if(responseCode == 401){
System.out.println("Wrong password.");
}
}
请注意,我们使用 base 64 作为编码,而不是加密!换句话说,使用 base 64 不会让黑客更难获取用户名和密码!我们只使用它,因此我们不会使用用户名或密码中的特殊字符破坏请求的格式。为了防止黑客获取用户名和密码,您需要使用HTTPS和我们已经介绍过的其他预防措施。
另请注意,您也可以从 JavaScript 创建使用授权标头的请求,包括 base 64 编码。
令牌认证
使用上述所有方法,我们需要在每个请求中发送用户名和密码,这意味着客户端需要始终将用户名和密码保存在内存中。这为黑客提供了更多获取用户名和密码的机会:他们可能会找到破解我们客户端的方法,或者他们可能会找到查看我们服务器日志或任何其他攻击媒介的方法。
无需在每个请求中发送用户名和密码,我们只需将它们提交给我们的 REST API 一次,并让 REST API 返回所谓的令牌,它只是一个随机String 值。服务器存储该令牌,以及它映射到的用户名,以及到期时间(通常在未来 15 分钟左右)。然后,客户端将令牌而不是密码存储在内存中,并将令牌与每个请求一起发送。服务器使用该令牌来查找它映射到的用户名,并确保使用该令牌的请求仅在到期时间之前有效。
使用这种方法,如果攻击者获得令牌,他们只有 15 分钟的时间来造成任何破坏。这比黑客获取用户密码要好得多。
在服务器端,我们首先创建一种存储令牌信息的方法:
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Class that holds token data. In real life this might use a database.
*/
public class TokenStore {
//Map of token (ranodm string) to TokenData (username and expiration time)
private Map<String, TokenData> tokenMap = new HashMap<>();
//this class is a singleton and should not be instantiated directly!
private static TokenStore instance = new TokenStore();
public static TokenStore getInstance(){
return instance;
}
//private constructor so people know to use the getInstance() function instead
private TokenStore(){}
/**
* Generates a token for the username, stores that token along with an
* expiration time, and then returns the token so clients can store it.
*/
public String putToken(String username){
String token = UUID.randomUUID().toString();
tokenMap.put(token, new TokenData(username));
return token;
}
/**
* Returns the username mapped to the username, or null
* if the token isn't found or has expired.
*/
public String getUsername(String token){
if(tokenMap.containsKey(token)){
if(tokenMap.get(token).expirationTime > System.currentTimeMillis()){
return tokenMap.get(token).username;
}
else{
//the token has expired, delete it
tokenMap.remove(token);
}
}
return null;
}
/**
* Internal class that holds a username and an expiration time.
*/
private static class TokenData{
String username;
long expirationTime;
private TokenData(String username){
this.username = username;
//15 minutes from now
expirationTime = System.currentTimeMillis() + 15 * 60 * 1000;
}
}
}
此类包含一个Map 令牌(只是随机String 值)到TokenData 实例(只是用户名和过期时间)。该putToken() 函数使用UUID 该类(它只是一个普通的 Java 类)生成一个随机字符串以用作令牌,然后将该令牌与用户名和到期时间一起存储,最后返回该令牌以便客户端可以使用它。该getUserName() 函数接受一个令牌并检查该令牌是否有效且未过期,如果是,则返回关联的用户名。否则返回null 。
然后我们将创建一个允许客户端发布用户名和密码以获取令牌的 servlet 类:
import java.io.IOException;
import java.util.Base64;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AuthServlet extends HttpServlet {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String authHeader = request.getHeader("authorization");
String encodedAuth = authHeader.substring(authHeader.indexOf(' ')+1);
String decodedAuth = new String(Base64.getDecoder().decode(encodedAuth));
String username = decodedAuth.substring(0, decodedAuth.indexOf(':'));
String password = decodedAuth.substring(decodedAuth.indexOf(':')+1);
Person loggedInPerson = DataStore.getInstance().getPerson(username);
//make sure user is in our data
if(loggedInPerson == null){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//make sure password is valid
//use hashed passwords in real life!
if(!password.equalsIgnoreCase(loggedInPerson.getPassword())){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String token = TokenStore.getInstance().putToken(username);
//the body of the response is just the token
response.getOutputStream().print(token);
}
}
这个 servlet 类只包含一个doPost() 函数,它从授权头中获取用户名和密码。然后它根据个人数据检查用户名和密码,如果不匹配则返回错误。如果匹配,则使用TokenStore 该类生成一个令牌,并将该令牌作为响应的正文返回。现在客户端可以向我们映射到此 servlet 的 URL 发出 POST 请求以获取令牌。
接下来,我们需要让 API 的其余部分接受一个令牌进行身份验证:
import java.io.IOException;
import java.util.Base64;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
public class PersonServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String requestUrl = request.getRequestURI();
String name = requestUrl.substring("/people/".length());
Person person = DataStore.getInstance().getPerson(name);
if(person != null){
String json = "{\n";
json += "\"name\": " + JSONObject.quote(person.getName()) + ",\n";
json += "\"about\": " + JSONObject.quote(person.getAbout()) + ",\n";
json += "\"birthYear\": " + person.getBirthYear() + "\n";
json += "}";
response.getOutputStream().println(json);
}
else{
//That person wasn't found, so return an empty JSON object. We could also return an error.
response.getOutputStream().println("{}");
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String authHeader = request.getHeader("authorization");
String encodedToken = authHeader.substring(authHeader.indexOf(' ')+1);
String decodedToken = new String(Base64.getDecoder().decode(encodedToken));
String username = TokenStore.getInstance().getUsername(decodedToken);
//token is invalid or expired
if(username == null){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String nameToEdit = request.getParameter("name");
String about = request.getParameter("about");
int birthYear = Integer.parseInt(request.getParameter("birthYear"));
Person loggedInPerson = DataStore.getInstance().getPerson(username);
//don't let users edit other users
if(!nameToEdit.equals(loggedInPerson.getName())){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
loggedInPerson.setAbout(about);
loggedInPerson.setBirthYear(birthYear);
//if we made it here, everything is okay, save the user
DataStore.getInstance().putPerson(loggedInPerson);
}
}
唯一改变的是我们的doPost() 函数,它现在需要授权标头中的编码令牌。然后它使用该令牌对用户进行身份验证。我们不再需要在这里检查密码,因为这已经由获取令牌的用户处理了!如果令牌无效或过期,或者用户试图更新另一个用户的数据,我们会向客户端返回错误。
这是一个与我们新的基于令牌的 REST API 交互的 JavaScript 客户端:
<!DOCTYPE html>
<html>
<head>
<title>PIJON</title>
<script>
var name;
function login(){
name = document.getElementById('name').value;
var password = document.getElementById('password').value;
document.getElementById('password').value = '';
var authString = name + ':' + password;
var encodedAuth = btoa(authString);
var authHeader = 'Basic ' + encodedAuth;
var ajaxRequest = new XMLHttpRequest();
ajaxRequest.onreadystatechange = function(){
if(ajaxRequest.readyState == 4){
if(ajaxRequest.status == 200){
token = ajaxRequest.responseText;
getPersonInfo();
}
}
};
ajaxRequest.open('POST', 'http://localhost:8080/auth');
ajaxRequest.setRequestHeader("authorization", authHeader);
ajaxRequest.send();
}
function getPersonInfo(){
var encodedAuth = btoa(token);
var authHeader = 'Bearer ' + encodedAuth;
var ajaxRequest = new XMLHttpRequest();
ajaxRequest.onreadystatechange = function(){
if(ajaxRequest.readyState == 4){
if(ajaxRequest.status == 200){
var person = JSON.parse(ajaxRequest.responseText);
document.getElementById('birthYear').value = person.birthYear;
document.getElementById('about').value = person.about;
// hide login, show content
document.getElementById('login').style.display = 'none';
document.getElementById('content').style.display = 'block';
}
}
};
ajaxRequest.open('GET', 'http://localhost:8080/people/' + name);
ajaxRequest.setRequestHeader("authorization", authHeader);
ajaxRequest.send();
}
function setPersonInfo(){
var about = document.getElementById('about').value;
var birthYear = document.getElementById('birthYear').value;
var postData = 'name=' + name;
postData += '&about=' + encodeURIComponent(about);
postData += '&birthYear=' + birthYear;
var encodedAuth = btoa(token);
var authHeader = 'Bearer ' + encodedAuth;
var ajaxRequest = new XMLHttpRequest();
ajaxRequest.onreadystatechange = function(){
if(ajaxRequest.readyState == 4){
if(ajaxRequest.status != 200){
// hide content, show login
document.getElementById('content').style.display = 'none';
document.getElementById('login').style.display = 'block';
}
}
};
ajaxRequest.open('POST', 'http://localhost:8080/people/' + name);
ajaxRequest.setRequestHeader("authorization", authHeader);
ajaxRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
ajaxRequest.send(postData);
}
</script>
</head>
<body>
<h1>PIJON</h1>
<p>(Person Info in JavaScript Object Notation)</p>
<div id="login">
<p>Name: <input type="text" value="Ada" id="name" /></p>
<p>Password: <input type="password" id="password" /></p>
<p><button type="button" onclick="login()">Login</button></p>
</div>
<div id="content" style="display:none">
<p>Birth year:</p>
<input type="text" id="birthYear">
<p>About:</p>
<textarea id="about"></textarea>
<p><button type="button" onclick="setPersonInfo()">Save</button></p>
</div>
</body>
</html>
这个 JavaScript 客户端首先显示一个登录屏幕。当用户单击Login 按钮时,该客户端POST 向我们的 REST API 发出请求以获取令牌。这允许用户保持登录状态,而客户端无需将密码保存在内存中。然后显示编辑屏幕,用户可以在其中编辑自己的信息。当他们单击Save 按钮时,令牌在授权标头中发送,服务器使用它进行身份验证。十五分钟后,用户必须再次登录才能获得新令牌!
这只是基于令牌的 REST API 和客户端的简单示例。您可以进行大量改进:您可以发送“令牌刷新”请求,这样用户就不必每十五分钟登录一次,或者您可以在 REST API 中添加其他端点 (URL) 以获得更多功能,或者您可以让客户看起来更漂亮。但这些基础知识旨在向您展示基于令牌的 REST API 如何工作。
概括
这可能需要考虑很多,但 REST 归结为一些想法:
- URL(又名“端点”)代表您的数据。例如,URL
/people/Ada 指向一个名为 Ada 的人。 - HTTP 方法喜欢
GET 并POST 允许客户端与这些 URL 上的数据进行交互。您可以GET 请求/people/Ada 获取 Ada 的数据,或POST 请求/people/Ada 设置 Ada 的数据。 - 格式由你决定!也许您想使用 JSON、XML、属性字符串或其他格式。
- 每个请求都应该是独立的。服务器不应存储有关请求的信息。这意味着每个请求都应包含形成响应所需的所有信息。没有会议!
- 客户端可以通过在每个请求中发送用户名和密码来进行身份验证。或者你的 REST API 可以提供一个令牌系统,客户端会在每个请求中发送令牌。
这些想法旨在使您更容易将业务逻辑与演示文稿分开。换句话说,它使您可以将底层数据与用户交互的视图分开处理。这也使得创建多个客户端变得更加容易,例如网站、Android 应用程序和桌面应用程序。
但就像我在顶部所说的那样,这完全取决于你!如果你只想在你的服务器中使用这些“规则”的一个子集,REST 警察不会踢你的门。做任何对您和您的应用程序最有意义的事情!
在家工作
- 将您的服务器转换为 REST API,然后创建两个与之交互的不同客户端。
|