Hands-On Full Stack Development with Spring Boot 2.0  and React
上QQ阅读APP看书,第一时间看更新

Spring Security

Spring Security (https://spring.io/projects/spring-security) provides security services for Java-based web applications. The Spring Security project started in 2003 and was previously named The Acegi Security System for Spring.

By default, Spring Security enables the following features:

  • An AuthenticationManager bean with an in-memory single user. The username is user and the password is printed to the console output.
  • Ignored paths for common static resource locations, such as /css, /images, and more.
  • HTTP basic security for all other endpoints.
  • Security events published to Spring ApplicationEventPublisher.
  • Common low-level features are on by default (HSTS, XSS, CSRF, and so forth).

You can include Spring Security in your application by adding the following dependency to the pom.xml file:

 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

When you start your application, you can see from the console that Spring Security has created an in-memory user with the username user. The user's password can be seen in the console output:

If you make a GET request to your API endpoint, you will see that it is now secure, and you will get a 401 Unauthorized error:

To be able to make a successful GET request, we have to use basic authentication. The following screenshot shows how to do it with Postman. Now, with authentication we can see that status is 200 OK and the response is sent:

To configure how Spring Security behaves, we have to add a new configuration class that extends the WebSecurityConfigurerAdapter. Create a new class called SecurityConfig in your application root package. The following source code shows the structure of the security configuration class. The @Configration and @EnableWebSecurity annotations switch off the default web security configuration and we can define our own configuration in this class. Inside the configure(HttpSecurity http) method, we can define which endpoints in our application are secured and which are not. We actually don't need this method yet because we can use the default settings where all endpoints are secured:

package com.packt.cardatabase;

import org.springframework.context.annotation.Configuration;
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;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

}

}

We also can add in-memory users to our application by adding the userDetailsService() method into our SecurityConfig class. The following is the source code of the method and it will create an in-memory user with the username user and password password:

  @Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();

return new InMemoryUserDetailsManager(user);
}

The usage of in-memory users is good in the development phase, but the real application should save the users in the database. To save the users to the database, you have to create a user entity class and repository. Passwords shouldn't be saved to the database in plain text format. Spring Security provides multiple hashing algorithms, such as BCrypt, that you can use to hash passwords. The following steps show how to implement that:

  1. Create a new class called User in the domain package. Activate the domain package and right click your mouse. Select New | Class from the menu and give the name User to a new class. After that, your project structure should look like the following screenshot:
  1. Annotate the User class with the @Entity annotation. Add the class fields—ID, username, password, and role. Finally, add the constructors, getters, and setters.  We will set all fields to be nullable and that the username must be unique, by using the @Column annotation. See the following User.java source code of the fields and constructors:
package com.packt.cardatabase.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;

@Column(nullable = false, unique = true)
private String username;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String role;

public User() {
}

public User(String username, String password, String role) {
super();
this.username = username;
this.password = password;
this.role = role;
}

The following is the rest of the User.java source code with the getters and setters:

  public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

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

public String getRole() {
return role;
}

public void setRole(String role) {
this.role = role;
}
}
  1. Create a new class called UserRepository in the domain package. Activate the domain package and right click your mouse. Select NewClass from the menu and give the name UserRepository to the new class. 
  2. The source code of the repository class is similar to what we have done in the previous chapter, but there is one query method, findByUsername, that we need in the next steps. See the following UserRepository source code:
package com.packt.cardatabase.domain;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}
  1. Next, we create a class that implements the UserDetailsService interface provided by Spring Security. Spring Security uses this for user authentication and authorization. Create a new package in the root package called service. Activate the root package and right click your mouse. Select New | Package from the menu and give the name service to a new package:
  1. Create a new class called UserDetailServiceImpl in the service package we just created. Now your project structure should look like the following:
  1. We have to inject the UserRepository class into the UserDetailServiceImpl class because that is needed to fetch the user from the database when Spring Security handles authentication. The loadByUsername method returns the UserDetails object, which is needed for authentication. Following is the source code of UserDetailServiceImpl.java:
package com.packt.cardatabase.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.packt.cardatabase.domain.User;
import com.packt.cardatabase.domain.UserRepository;

@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserRepository repository;


@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User currentUser = repository.findByUsername(username);
UserDetails user = new org.springframework.security.core
.userdetails.User(username, currentUser.getPassword()
, true, true, true, true,
AuthorityUtils.createAuthorityList(currentUser.getRole()));
return user;
}

}
  1. In our security configuration class, we have to define that Spring Security should use users from the database instead of in-memory users. Delete the userDetailsService() method from the SecurityConfig class to disable in-memory users. Add a new configureGlobal method to enable users from the database. We shouldn't ever save the password as plain text to the database. Therefore, we will define a password hashing algorithm in the configureGlobal method. In this example, we are using the BCrypt algorithm. This can be easily implemented with the Spring Security BCryptPasswordEncoder class. Following is the SecurityConfig.java source code. Now, the password must be hashed using BCrypt before it's saved to the database:
package com.packt.cardatabase;

import org.springframework.beans.factory.annotation.Autowired;
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.crypto.bcrypt.BCryptPasswordEncoder;

import com.packt.cardatabase.service.UserDetailServiceImpl;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailServiceImpl userDetailsService;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
  1. Finally, we can save a couple of test users to the database in our CommandLineRunner. Open the CardatabaseApplication.java file and add following code at the beginning of the class to inject UserRepository into the main class:
@Autowired 
private UserRepository urepository;
  1. Save the users to the database with hashed passwords. You can use any BCrypt calculator found on the internet:
  @Bean
CommandLineRunner runner() {
return args -> {
Owner owner1 = new Owner("John" , "Johnson");
Owner owner2 = new Owner("Mary" , "Robinson");
orepository.save(owner1);
orepository.save(owner2);

repository.save(new Car("Ford", "Mustang", "Red", "ADF-1121",
2017, 59000, owner1));
repository.save(new Car("Nissan", "Leaf", "White", "SSJ-3002",
2014, 29000, owner2));
repository.save(new Car("Toyota", "Prius", "Silver", "KKO-0212",
2018, 39000, owner2));
      // username: user password: user
urepository.save(new User("user",
"$2a$04$1.YhMIgNX/8TkCKGFUONWO1waedKhQ5KrnB30fl0Q01QKqmzLf.Zi",
"USER"));

// username: admin password: admin
urepository.save(new User("admin",
"$2a$04$KNLUwOWHVQZVpXyMBNc7JOzbLiBjb9Tk9bP7KNcPI12ICuvzXQQKG",
"ADMIN"));

};
}

After running your application, you see that there is now a user table in the database and two user records are saved:

Now, you will get a 401 Unauthorized error if you try to send a GET request to the /api endpoint without authentication. You should authenticate to be able to send a successful request. The difference to the previous example is that we are using the users from the database to authenticate. 

You can see a GET request to the /api endpoint using the admin user in the following screenshot: