In the previous articles, I discussed the implementation of a login system in a Spring Boot app using Spring Security and demonstrated how to implement JWT for authenticating users and granting access to specific endpoints. In this final article, we'll explore how to eliminate the InMemoryUserDetailsManager
and transition to our custom User model, integrated with our database.
This article is your guide to mastering three key aspects in three parts:
- Login via API: We'll kick things off by tackling the initial API login.
- Implementing a JWT for Persistence: Learn how to keep your users authenticated via API using the shiny new JWT.
- Connecting Database Users to Spring Security: Dive into the realm of syncing your database users seamlessly with the Spring Security system.
Integrating Database Users with Spring Security
Currently, our application hosts a pre-established user with a designated username and password. While this setup is perfectly suitable for testing or small-scale proof-of-concept applications, in many instances, we want to take a more comprehensive approach and take control over the user data stored in our database. It is assumed in this tutorial that you have successfully established a connection to a database, ensuring that works correctly. The examples in this tutorial will be demonstrated using MySQL and JPA; however, you can easily adapt the instructions to your chosen stack, such as MongoDB or opting for JDBC over JPA.
Here is the Data Definition Language (DDL) employed for our users:
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(25),
password TEXT
)
For managing database migrations, I recommend usin a version control system such as Flyway. A detailed tutorial on Flyway can be found here.
Note that our user entries are without any extra information. This tutorial doesn't require additional details, but feel free to add any fields you need (like email, street, phone). It's essential to emphasize that this schema doesn't include any role (the authorization part of Spring Security) as it's a topic not covered in this tutorial.
With all our configuration placed in the security
package before, let's proceed to create a new package called user
to establish a User
model.
User.kt
@Entity
@Table(name = "users")
class User(
@Column(nullable = false)
private val username: String,
private val password: String
): UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return mutableListOf(SimpleGrantedAuthority("USER"))
}
override fun getPassword(): String {
return password
}
override fun getUsername(): String {
return username
}
override fun isAccountNonExpired(): Boolean {
return true
}
override fun isAccountNonLocked(): Boolean {
return true
}
override fun isCredentialsNonExpired(): Boolean {
return true
}
override fun isEnabled(): Boolean {
return true
}
}
This model is annotated with @Entity
, and I've specified the @Table
name. I added the username and password in the primary constructor, while keeping the ID outside of it and initialized it as 0. The database will manage the ID, and it's not our responsibility to manage this field. If a new User is created, it can go straight as User("username", "password")
rather than User(null, "username", "password")
.
This class implements the UserDetails
interface, which establishes the connection needed between the security framework and your model. The methods implemented are a requirement from this interface.
For getPassword
and getUsername
, we provide this information, allowing the AuthenticationFilter
to retrieve the info when needed. Regarding getAuthorities
, it's related to the roles a user can have for the authority part. Since we're not covering this topic in these articles, we simply return a list with a unique role called "USER."
All the methods starting with is*
are eventually called by the filter chain to check user information for granting authentication or authorization. For simplicity, all of them return true, and this topic is not explored in the tutorial. As an example, consider isAccountNonLocked
; we could record in the database the wrong login attempts for the user. If it exceeds a certain threshold, we could return false, locking the user account and allowing login after a specified period.
With UserDetails
implemented, it's time to create a repository
that connects the model and the database. In the user
package:
UserRepository.kt
Interface UserRepository: ListCrudRepository<User, Long> {
fun findByUsername(username: String): Optional<User>
}
With JPA 3.X, the interfaces from JPA have slight differences, and ListCrudRepository
now implements the same methods as CrudRepository
but returns a List
type instead of Iterable
. Here is the schema of interfaces for JPA 3:
Now, the last part of our connection with Spring Security is missing: how the filters can find the user from our DB. We have the repository, but there's no implementation to let Spring Security know that it should use this repository for finding users. Here comes our last file in the user
package:
Here's the final part of our user configuration for Spring Security: the UserDetailsService
implementation.
UserService.kt
@Service
class UserService(private val userRepository: UserRepository) :UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
return userRepository.findByUsername(username).orElseThrow { RuntimeException("User Not Found") }
}
}
This class implements the UserDetailsService
, requiring the implementation of the loadUserByUsername
function. In this method, we inject our repository and use the findByUsername
method we created. If the user is not found, we throw a new error.
One last detail remains to be addressed. In the config file, we previously created an in-memory user:
SecurityConfig.kt
@Bean
fun userDetailsService(): UserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
We can now replace this in-memory user configuration with our new database-backed configuration. This ensures that Spring Security will use our UserDetailsService
implementation to authenticate users against the database.
Great job👏! It's wonderful to see that the new user authentication system is working successfully with users stored in the database.
When I try to log in, I get a successful response:
In this section, we learned:
- About UserDetails and UserDetailsService
- How to implement them correctly
- Why do we have all the methods implementations on UserDetails, even though we are not using them
Conclusions
sum up, we've accomplished the development of a user authentication system using Spring Security, integrating JWT to ensure secure and smooth user authorization. Despite the intricate nature of Spring Security, this tutorial has been designed to provide detailed explanations at each stage, helping to get a deeper comprehension of the implementation process.
For those looking to delve deeper into Spring Security, here are some suggested next steps:
- Refer to the official Spring Security documentation for comprehensive insights into the framework.
- Explore the topic of authorization with Marco Behler's article on authorization.
- Gain a deeper understanding of the differences between ant matches and MVC matchers with Laurențiu Spilcă by watching this interesting video.
- Check out Laurențiu Spilcă's book, Spring Security in Action, for an in-depth exploration of Spring Security concepts.
- To enhance your understanding of JWT in Kotlin/Java, explore the JWT Library.
By using these guides and learning more about Spring Security, you'll be ready to make your applications solidly secure 🦸🏻♀️. If you ever have questions or need help, just ask. Have fun coding! 💻
Top comments (0)