-
Notifications
You must be signed in to change notification settings - Fork 59
Authentication Types
Source Folder: authentication
Tech Stack:
- spring-boot
- spring-mvc
- spring-data
- spring-security
- spring-session
- hibernate & hibernate metamodel generator
- postgresql
- hibernate-c3p0
- mapstruct (for model transformation)
- log4j2 (slf4j impl)
- project lombok
- JWT
- spring-cloud-zuul
- REDIS
In this journey, we will start with a simple Spring Boot application without any security configuration containing only one REST controller. After that, we will activate Spring Security and validate credentials by querying the database. Once we've done with the basic authentication, we will integrate Spring Session backed by REDIS so that our sessions will be kept in REDIS. Finally, we will convert our security check mechanism to a stateless structure with JWT.
We will start with creating a Spring Boot application with a simple REST controller and test it without any security considerations.
- So, create your maven project and add the following dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Optional, add if you want to use Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- Provide the application entry point
@Slf4j
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class);
log.info("Application started...");
}
}
- Create a sample DTO class
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
private String id;
private String name;
private String company;
private LocalDate birthdate;
}
- Create a sample REST Controller
@RestController
@RequestMapping("/customer")
public class CustomerController {
@GetMapping
public List<Customer> list() {
Customer c1 = Customer.builder()
.id("1").name("john doe").company("acme").birthdate(LocalDate.now().minusYears(28)).build();
Customer c2 = Customer.builder()
.id("2").name("jane doe").company("acme").birthdate(LocalDate.now().minusYears(24)).build();
List<Customer> result = new ArrayList<>();
result.add(c1);
result.add(c2);
return result;
}
}
Now you can run your application and test it. You can use your browser or any REST client like POSTMAN.
When you call http://localhost:8080/customer you should get a list of customers.
[
{
"id": "1",
"name": "john doe",
"company": "acme",
"birthdate": "1990-10-07"
},
{
"id": "2",
"name": "jane doe",
"company": "acme",
"birthdate": "1994-10-07"
}
]
The first step is completed. We now have a running Spring Boot application with a sample REST controller returning a list of customers.
In this section, we will prevent unauthenticated access to protected resources with Spring Security. We will write an authentication provider which validates credentials via querying the relational database using ORM.
Source Folder: simple-auth
- Let's start with adding spring security dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Before proceeding, test your application and see what happens. You got an unauthorized error, right?
{
"timestamp": "2018-10-07T10:12:32.111+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/customer"
}
Just because we added Spring Security dependency, we already protected our resources. But, now we have a new problem. How do we access them?
We have a very basic approach for now. We will configure Spring Security in a way that it will get provided credentials and look into the database to validate them. For that, we need a custom authentication provider:
@Slf4j
public class DbAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
if (this.validateCredentials(username, password)) {
return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private boolean validateCredentials(String username, String password) {
// TODO Query database for username and password
return true;
}
}
We also need to register this authentication provider so its authenticate method will be called automatically by the Spring Framework once the credentials are provided by the user. To do that, we extend WebSecurityConfigurerAdapter and make some configurations like the following:
@Slf4j
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationProvider authenticationProvider() {
return new DbAuthenticationProvider();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.formLogin().permitAll();
}
}
Now, we can test our application again. If you are using your browser, you will be prompted to enter your credentials. Notice that our validateCredentials method always returns true for now, meaning that you will be authenticated regardless of your credentials. If you are using a REST client like POSTMAN, just choose Basic Authentication as the authentication method instead of No Auth.
The next step is integrating our database so that our validateCredentials method will be able to query the user table.
Add the following dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
We'll use postgreSQL as a RDBMS server. If you don't have an instance, you can create a docker container easily. Official PostgreSQL Docker Image
Hibernate c3p0, on the other hand, is a connection pool library for hibernate.
We also need the following maven plugin to be able to use hibernate type-safe metamodel while writing specifications.
<build>
<plugins>
<!-- MAVEN ADD GENERATED-SOURCES TO CLASSPATH -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${maven.plugin.build-helper.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-sources/annotations</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<!-- Hibernate Type Safe metamodel -->
<plugin>
<groupId>org.bsc.maven</groupId>
<artifactId>maven-processor-plugin</artifactId>
<version>${maven.plugin.processor.version}</version>
<executions>
<execution>
<id>process</id>
<goals>
<goal>process</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<processors>
<processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
</processors>
<outputDirectory>target/generated-sources/annotations</outputDirectory>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.jpamodelgen-version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
Now we've configured maven and added necessary dependencies into classpath. The next step is to make database connection and pooling configurations.
Create a file named database.properties to src/main/resources
# CONNECTION PROPETTIES
hibernate.driver=org.postgresql.Driver
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
hibernate.db_host=jdbc:postgresql://localhost:5432
hibernate.db_url=${hibernate.db_host}/${hibernate.db_name}?currentSchema=${hibernate.db_schema}&characterEncoding=UTF-8
hibernate.db_user=postgres
hibernate.db_password=123456
hibernate.db_name=security_test
hibernate.db_schema=public
# POOLING PROPERTIES
hibernate.c3p0.acquire_increment = 1
hibernate.c3p0.initialPoolSize = 5
hibernate.c3p0.minPoolSize = 5
hibernate.c3p0.maxPoolSize = 20
hibernate.c3p0.maxIdleTime = 300
hibernate.c3p0.idleConnectionTestPeriod = 3000
hibernate.c3p0.maxStatements = 50
# HIBERNATE PROPERTIES
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.tablePrefix=none
hibernate.hbm2ddl_auto=update
hibernate.entities_scan=com.fd
hibernate.repositories_scan=com.fd
We need to load this configurations:
@Configuration
@PropertySource("database.properties")
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "${hibernate.repositories_scan}")
public class DatabaseConfig {
@Autowired
private Environment environment;
@Bean
public ComboPooledDataSource dataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass(this.environment.getProperty("hibernate.driver"));
dataSource.setJdbcUrl(this.environment.getProperty("hibernate.db_url"));
dataSource.setUser(this.environment.getProperty("hibernate.db_user"));
dataSource.setPassword(this.environment.getProperty("hibernate.db_password"));
// C3P0 Connection Pool Settings
dataSource.setAcquireIncrement(Integer.parseInt(this.environment.getProperty("hibernate.c3p0.acquire_increment")));
dataSource.setInitialPoolSize(Integer.parseInt(this.environment.getProperty("hibernate.c3p0.initialPoolSize")));
dataSource.setMinPoolSize(Integer.parseInt(this.environment.getProperty("hibernate.c3p0.minPoolSize")));
dataSource.setMaxPoolSize(Integer.parseInt(this.environment.getProperty("hibernate.c3p0.maxPoolSize")));
dataSource.setMaxIdleTime(Integer.parseInt(this.environment.getProperty("hibernate.c3p0.maxIdleTime")));
dataSource.setIdleConnectionTestPeriod(Integer.parseInt(this.environment.getProperty("hibernate.c3p0.idleConnectionTestPeriod")));
dataSource.setMaxStatements(Integer.parseInt(this.environment.getProperty("hibernate.c3p0.maxStatements")));
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws PropertyVetoException {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan(this.environment.getProperty("hibernate.entities_scan"));
factory.setDataSource(this.dataSource());
factory.setPersistenceProviderClass(HibernatePersistenceProvider.class);
factory.setJpaProperties(this.hibernateProperties());
return factory;
}
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
@Bean
public JpaTransactionManager transactionManager() throws PropertyVetoException {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(this.entityManagerFactory().getObject());
return transactionManager;
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.dialect", this.environment.getProperty("hibernate.dialect"));
properties.put("hibernate.show_sql", this.environment.getProperty("hibernate.show_sql"));
properties.put("hibernate.format_sql", this.environment.getProperty("hibernate.format_sql"));
properties.put("hibernate.hbm2ddl.auto", this.environment.getProperty("hibernate.hbm2ddl_auto"));
return properties;
}
}
The next step is to query the database. Let's first create our base Entity model and a general purpose repository for common CRUD operations:
The Entity Model:
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class EntityModel implements Serializable {
@Id
@Column(name = "DB_ID")
@GeneratedValue(strategy = GenerationType.AUTO)
protected int id;
@Column(name = "DELETED", nullable = false)
protected boolean deleted;
@CreatedBy
@Column(name = "CREATED_BY")
protected String createdBy;
@CreatedDate
@Column(name = "CREATED_DATE", nullable = false, updatable = false)
protected long createdDate;
@LastModifiedDate
@Column(name = "LAST_MODIFIED_DATE", nullable = false)
protected long lastModifiedDate;
@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
protected String lastModifiedBy;
}
The Generic Repository:
public interface GenericRepository<T extends EntityModel> extends JpaRepository<T, Integer>, JpaSpecificationExecutor<T> {
}
Now, we need to create our user model and user repository:
The User Entity
@Data
@Entity
@Table(name = "USERS")
public class UserEntity extends EntityModel {
@Column(name = "NAME")
private String name;
@Column(name = "SURNAME")
private String surname;
@Column(name = "BIRTHDATE")
private Instant birthdate;
@Column(name = "USERNAME")
private String username;
@Column(name = "PASSWORD")
private String password;
@Column(name = "EMAIL")
private String email;
}
The User Repository:
public interface UserRepository extends GenericRepository<UserEntity> {
}
Yes, that's it. We don't need any custom query methods since we will use specifications like below:
public class UserSpecifications {
public static Specification<UserEntity> withUsernameAndPassword(String username, String password) {
return (Specification<UserEntity>) (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.and(
criteriaBuilder.equal(root.get(UserEntity_.username), username),
criteriaBuilder.equal(root.get(UserEntity_.password), password));
}
}
Now that we've implemented our User repository, we need to inject it to our authentication provider so that it can be able to query the database:
The DbAuthenticationProvider:
...
private UserRepository userRepository;
public DbAuthenticationProvider(UserRepository userRepository) {
this.userRepository = userRepository;
}
...
private boolean validateCredentials(String username, String password) {
Optional<UserEntity> user = this.userRepository.findOne(UserSpecifications.withUsernameAndPassword(username, password));
if (user.isPresent()) {
return true;
}
return false;
}
And we need to modify our security configurations class where we define our authentication provider bean:
The SecurityConfig:
@Autowired
private UserRepository userRepository;
@Bean
public AuthenticationProvider authenticationProvider() {
return new DbAuthenticationProvider(this.userRepository);
}
Now we've achieved our second goal which was to activate Spring Security and validate credentials via querying the database. You can run your application and look if everything is fine. Once you open http://localhost:8080/customer in your browser again you will be prompted for credentials. Note that, there is no user in the database (if you haven't added already) so that your authentication provider will fail to validate your credentials and you won't be able to access the resources.
Add a user to the database and try again with the credentials you put into the users table! You should be able to access the resource.
The problem with this approach is that once your application crashes the sessions in the security context will be dead. All the users will need to authenticate again. Try it for yourself.
Test Steps:
- Put a breakpoint into the
authenticatemethod of your authentication provider class. - Navigate to http://localhost:8080/customer
- You will be prompted for credentials. Enter valid credentials that you saved into the database. The execution will hit the breakpoint. Once you proceed, you should be able to access the resources:
[
{
"id": "1",
"name": "john doe",
"company": "acme",
"birthdate": "1990-10-07"
},
{
"id": "2",
"name": "jane doe",
"company": "acme",
"birthdate": "1994-10-07"
}
]
-
Refresh the page. This time you should not be prompted for the credentials and the execution flow will not hit the breakpoint because the user is already authenticated and the session is still alive.
-
Don't close your browser, restart the application and try to access the same resource again. This time you will be prompted for credentials and the execution flow will hit the breakpoint.
You might ask that is that really a problem. Actually, it is. Think of a case that you want to scale your application up and down based on the load. Whenever a new instance of your application is created and the new requests are directed to that instance, a new different session will be created for each user.
What if we can share the session across applications?
In this section, we will use Spring Session backed by REDIS instead of HttpSession so that the session be shared among multiple applications and will still be alive even if the applications crash (until session timeout).
Source Folder: spring-session-auth
- Spring Session has seamless integration with HttpSession which means you can easily adapt your applications using HttpSession.
- Almost effortless clustered session implementation (sharing session across multiple nodes)
- RESTful API support (Session ID in headers)
First, we need to create a REDIS instance. If you don't have one, you can easily and quickly create a docker container for it. Official REDIS Docker Image
Add the following dependencies:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
Make the following REDIS connection and Spring Session configurations:
spring:
redis:
host: localhost
port: 6379
#password:
session:
store-type: redis
redis:
flush-mode: on-save
namespace: spring:session
server:
servlet:
session:
timeout: 60 # If a suffix is not specified, seconds will be used
That's it. Adding necessary dependencies and configurations is all we need to do. Spring Boot will take care of the rest for us.
Now we can test our application again:
- Put a breakpoint into the
authenticatemethod of your authentication provider class. - Navigate to http://localhost:8080/customer
- You will be prompted for credentials. Enter valid credentials that you saved into the database. The execution will hit the breakpoint. Once you proceed, you should be able to access the resources.
- Connect to your redis instance via redis-cli and list the keys with the
keys *command. You should see the session keys in the result list. - Refresh the page in the browser. The browser will not ask for your credentials.
- Restart the application and reload the page in the browser. Notice that the browser will still not ask for your credentials this time because the session is still alive in REDIS.
- Delete the relevant session keys from your redis instance and try to reload the page again. This time you will see that you are not authenticated.
- Home
- JPA Specification
- JMS with Spring Boot
- JMS with Spring Boot & Apache Camel
- Caching with Spring Boot & Redis
- Netflix Eureka with Spring Boot
- Netflix Hystrix & Eureka with Redis Cache
- Netflix Zuul
- Authentication Types
- Separating Unit & Integration Tests Execution Phases with Maven
- Basic CSRF Attack Simulation & Protection
- Spring Batch
- Dagger
- Concurrency with Java
- Swagger2 with Spring Boot