Some additional features of Spring Boot

In my earlier examples I limited myself to only the most basic features of Spring Boot, since I wanted us to be able to implement only the most essential server features first. In these lecture notes I am going to cover some additional "nice to have" featuers of Spring Boot.

These notes make use of a simple example project that I have set up. You can download the full project by clicking the button above.

The example project consists of a set of classes to interact with a database that has just one table, a table of users. Each user has an integer id number, a name, a password, and an api key string. I will discuss the significance of the key string below.

HTTP error codes

The clients who communicate with our server will use the HTTP protocol. One of the key features of HTTP is the use of status codes. When a client sends a request the server will respond with a status code and data in the body of the response. In the case of an unsuccessful request the status code is meant to give the client some feedback on what went wrong. Also, most servers will put a more detailed error message in the body of the response if the request is not successful.

Below is an example of some Spring Boot controller code that demonstrates how to set a status code and an error message when something goes wrong.

@GetMapping(params={"name","password"})
public ResponseEntity<String> checkLogin(@RequestParam(value="name") String user,@RequestParam(value="password") String password) {
    User result = dao.findByNameAndPassword(user, password);
    if(result == null)
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid user name or password");
    return ResponseEntity.ok().body(result.getKey());
}

The method here is meant to handle a login process. The client will provide a user name and a password. The dao method that this method calls to check the login is set up to return null if the user name or password are not valid.

To handle error situations we need to both set a response code and provide a custom body with an error message. To do this we make use of a ResponseEntity object. The first step in using a ResponseEntity is to change the return type of the controller method to return a ResponseEntity instead of a regular data type. The ResponseEntity acts as a container for both the status code and the body (and even additional elements such as response headers should you need them). To construct a ResponseEntity we use a builder pattern: we start by calling a static method in the ResponseEntity class to start the process, and then call additional methods on the object returned by that first method call until we have completed the process of building the object. In this case we start by calling the status() method to set a status code in the response, and then follow that with a call to the body() method to specify a body for the request. An important restriction to pay attention to with the body() method is that the type of the parameter you pass to the body() method much match the type in the angle brackets in the ResponseEntity.

We specify which status code to use by using a constant defined in the HttpStatus enumerated type. There are a large number of status codes available to choose from in Http. This page provides a list of those status codes.

Validation and other error checking

Another type of error checking involves checking inputs that come from users to make sure that they are valid. This process of error checking is generically known as validation.

Here is a typical example of validation. Below is code for a method to handle posting new users to an application.

@PostMapping
public ResponseEntity<String> save(@RequestBody User user) {
    if(user.getName().isBlank() || user.getPassword().isBlank())
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Empty user name or password");

    String key = dao.save(user);
    if(key.equals("Duplicate"))
        return ResponseEntity.status(HttpStatus.CONFLICT).body("User with this name already exists");
    else if(key.equals("Error"))
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Can not generate key");
    return ResponseEntity.ok().body(key); 
}

An obvious validation check that needs to be preformed here is checking whether or not the provided user name or password in the User object are empty.

Validation can catch obvious errors, such as missing data or data that has the wrong format. Other errors will require more careful checking. In this example the validation check can find missing names or passwords. Additional error checking is handled by the DAO: for example, before inserting a new user into the table of users the DAO will check to see if a user with the requested name already exists. If that is the case, the DAO method will return "Duplicate". The code in the controller method will simply have to check the value returned by the DAO method to see if any errors were caught at that level of the processing.

Improving application security

In prior examples where we saw code that could handle a login request we had the application return the id number of the user in response to a successful login. Clients would then send that id number as part of later requests to specify which user the request applies to. The main problem with this approach is that user ids are too simple. A malicious user could manufacture a request asking for information on a particular user using a made-up id number.

To make the application more secure, we can modify the application to use something other than a user's database id number to identify that user. This application uses a more secure alternative.

User objects in this application have the following structure:

public class User {
    private String key;
    private String name;
    private String password;

    User() {}

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

In place of the usual integer id number, each user is assigned a special key value. A typical key value is a long, complex string like 96deda16-d989-11ee-8b70-463536709185 that is designed to be very hard to guess. During the account creation process the application will ask the databaes to generate a unique key for the new user. Here is the DAO method I use to store a new user in the database:

public String save(User user) {
    // First make sure this is not a duplicate
    String sql = "SELECT * FROM users WHERE name=?";
    RowMapper<User> rowMapper = new UserRowMapper();
    User old = null;
    try {
        old = jdbcTemplate.queryForObject(sql, rowMapper, user.getName());
    } catch(Exception ex) {

    }
    if(old != null)
        return "Duplicate";

    // Have MySQL generate a unique id
    String idSQL = "select uuid()";
    String key = null;
    try {
        key = jdbcTemplate.queryForObject(idSQL, String.class);
    } catch(Exception ex) {
        key = "Error";
    }
    if(key.equals("Error"))
        return key;

    String insertSQL = "insert into users(id,name,password) values (?, ?, ?)";
    jdbcTemplate.update(insertSQL,key,user.getName(),user.getPassword());
    return key;
}

To generate the key values I make use of the MySQL uuid() function. This function is designed to generate universally unique identifier strings, which are strings that use a clever algorithm to encode information that is designed to be universally unique. For example, we could use an algorithm to encode the combination of the time and a server's internet address. For us, the most important feature of these strings is that they are extremely hard to guess, which will improve the security of our application.

Avoid SQL Injection Attacks

Another important aspect of application security is hardening applications against common attacks used by hackers to break into systems. One of the most popular such attacks is the SQL Injection attack, which seeks to force the application to run SQL code of the attacker's choosing.

This attack targets a particular vulnerability in server code. Here is an example of some code that allows for this type of attack. In this code we use some insecure JDBC code to handle an application login process:

String SQL = "select * from users where name=\'"+name+"\' and password=\'"+password+"\'";
ResultSet rset = statement.executeQuery(SQL);

In this example a client has provided a user name and a password, which are stored in strings name and password that we use to construct the SQL statement.

A malicious attacker can now send the following two values for the name and the password:

name="admin"
password="hello';drop table users;"

Inserting these two values in the SQL string produces this result:

select * from users where name='admin' and password='hello';drop table users;'

With this, the attacker has tricked us into running two SQL statements instead of just one. The first one is an innocent select statement, while the second one is a far more destructive statement that destroys one of the tables in the database.

This trick makes it possible for an attacker to run any SQL code they choose to. Most often attackers will exploit this security hole to create new database users. After breaking into the network that houses the database, the attacker can then log in to the database system using the new user account they set up for themselves and gain full access to the database and all of its contents: this is a worst case security problem for the database owner.

The usual defense against SQL injection is to use prepared statements for all database interactions. In this approach the code looks more like this:

String SQL = "select * from users where name=? and password=?";
PreparedStatement ps = connection.prepareStatement(SQL);
ps.setString(1,name);
ps.setString(2,password);
ResultSet rset = ps.executeQuery();

This is more secure because the mechanism used to fill in the place holders in prepared statements automatically strips out special characters. In this case, the resulting SQL code will look like

select * from users where name='admin' and password='hellodrop table users'

By removing the extra punctuation from the password string we have effectively eliminated the attacker's ability to run malicious SQL code.

As you have seen, the JdbcTemplate class in Spring Boot applications makes extensive use of prepared statements. It is still possible for programmers to bypass this mechanism by doing something like this:

String SQL = "select * from users where name=\'"+name+"\' and password=\'"+password+"\'";
RowMapper<User> rowMapper = new UserRowMapper();
return jdbcTemplate.queryForObject(sql, rowMapper);

Doing this would open up your application to a SQL injection attack. The safe alternative here is the more typical code:

String sql = "select * from users where name=? and password=?";
RowMapper<User> rowMapper = new UserRowMapper();
return jdbcTemplate.queryForObject(sql, rowMapper, name, password);