JWT Mechanism in Spring Security 6 (without WebSecurityConfigurerAdapter)
JWT is an important concept in authenticating and authorizing access to an application. In a Java application, especially after migrating to Spring Security 6, you may need to create or update your security configurations without using WebSecurityConfigurerAdapter.
In this article, I will discuss the issue of JWT in a Java Maven project. I will be using Spring Security 6 and of course without the WebSecurityConfigurerAdapter.
I will use PostgreSQL as the database.
My IDE will be IntelliJ IDEA and you should have the basic understanding of Spring Boot Security to better benefit from this article.
I create my project at the start.spring.io:
1. Spring Security Dependency
And now I add firstly Spring Security dependency in the pom.xml file:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.0.3</version>
</dependency>
I add the PostgreSQL configuration codes below to the application.properties
file:
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=<your passsword here>
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
After that once you run the project, you can see a generated password in the console:
But we don’t need that as we will customize the security settings of the project.
2. Security Filter Chain
The security filter is the first thing a request goes through when you send an HTTP request to a server. Spring Security handles incoming requests under the security layer and ensures that these requests are authenticated and authorized. When using JWT, incoming requests are expected to contain JWT. These tokens are validated by Spring Security and if the validation is successful, the request is processed or rejected based on the user’s authorizations.
Now I add the SecurityConfig
class under the security
package. The file looks like this:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
public static final String ADMIN = "admin";
public static final String USER = "user";
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
(authz) -> authz.requestMatchers("/api/some-path-here/**")
.permitAll()
.requestMatchers(HttpMethod.GET)
.permitAll()
.requestMatchers(HttpMethod.PUT)
.hasAuthority(USER)
.requestMatchers(HttpMethod.POST)
.permitAll()
.anyRequest()
.authenticated());
http.csrf(c -> c.disable());
return http.build();
}
}
@Configuration
marks the class as a Spring Boot configuration class and with @EnableWebSecurity
we enable Spring Security’s web features.
I defined two roles, but they can differ according to your app’s needs.
By requestMatchers
method allows you to allow or restrict paths within the application or http-method whichever you want, and to set permissions on them.
Project Structure
First, I want to add some classes and interfaces to the application that I can use to manage users and roles.
I add lombok
to the pom.xml
that provides less code:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
Now I create the controller, service, entities, dao, dto packages and associated classes under those packages.
I need two entities. One is for users another is for roles:
Under the package model
I add the classAppUser
:
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Entity
@Table(name = "app_user")
public class AppUser implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Integer id;
@Column(name = "username", columnDefinition = "text", unique = true)
private String username;
@Column(name = "password")
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> roles = new ArrayList<>();
}
And theRole
class:
@Data
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}
I named the user class as AppUser
. Because the User
is used by UserDetails
in UserDetailsService
. We will use this in our CustomUserDetailsService
class.
And now I add repositories of AppUser and Role classes under the package dao
.
public interface AppUserRepository extends JpaRepository<AppUser, Integer> {
Optional<AppUser> findByUsername(String username);
Boolean existsByUsername(String username);
}
public interface RoleRepository extends JpaRepository<Role, Integer> {
Optional<Role> findByName(String name);
}
These repositories are interfaces that provide methods for accessing data.
I add the registerDto
class at the dto
package that I will use as a data-transfer-object:
@Data
public class RegisterDto {
private String username;
private String password;
private String role;
}
Now I create the controller class under the package controller
and add the register
method:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private AppUserRepository appUserRepository;
private RoleRepository roleRepository;
private PasswordEncoder passwordEncoder;
@Autowired
public AuthController(AppUserRepository appUserRepository,
RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
this.appUserRepository = appUserRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("register")
public ResponseEntity<String> register(@RequestBody RegisterDto registerDto) {
if (appUserRepository.existsByUsername(registerDto.getUsername())) {
return new ResponseEntity<>("Username is taken!", HttpStatus.BAD_REQUEST);
}
AppUser appUser = new AppUser();
appUser.setUsername(registerDto.getUsername());
appUser.setPassword(passwordEncoder.encode((registerDto.getPassword())));
Role roles = roleRepository.findByName(registerDto.getRole()).get();
appUser.setRoles(Collections.singletonList(roles));
appUserRepository.save(appUser);
return new ResponseEntity<>("User registered success!", HttpStatus.OK);
}
}
PasswordEncoder
: This refers to the interface provided by Spring Security. It defines methods for encoding and decoding passwords securely.
And I have to add this to my SecurityConfig
class:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
public static final String ADMIN = "admin";
public static final String USER = "user";
...
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Now once the app runs, Hibernate(JPA)
creates the tables by using entities of the project.
This register
method allows you to create new users, assign them roles, and save them in the database. But roles should be in the database before creating a user. I add the roles manually to the PostgreSQL database:
And I send my first request to the Spring Boot server and I get the result below:
3. AuthenticationManager
and CustomUserDetailsService
Now I can create thelogin
method. But first, I need a LoginDto
. Under the dto folder:
@Data
public class LoginDto {
private String username;
private String password;
}
AuthenticationManager
and CustomUserDetailsService
structures are also required to be able to log in.
AuthenticationManager
: This component plays a crucial role in user login. It takes an Authentication
object (containing username and password) and attempts to authenticate the user. It relies on a UserDetailsService
to retrieve user details and compares the provided credentials with the stored information. If successful, it returns a valid Authentication
object.
I just inject this in AuthController
and create my login
method:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
...
private AuthenticationManager authenticationManager;
@Autowired
public AuthController(AppUserRepository appUserRepository, RoleRepository roleRepository,
PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
this.appUserRepository = appUserRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
}
@PostMapping("register")
public ResponseEntity<String> register(@RequestBody RegisterDto registerDto) {
...
}
@PostMapping("login")
public ResponseEntity<String> login(@RequestBody LoginDto loginDto) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String response = "You have successfully logged in!";
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
For CustomUserDetailsService
I create a service
folder and in it:
@Service
public class CustomUserDetailsService implements UserDetailsService {
private AppUserRepository appUserRepository;
@Autowired
public CustomUserDetailsService(AppUserRepository appUserRepository) {
this.appUserRepository = appUserRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser appUser = appUserRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Username not found"));
return new User(appUser.getUsername(), appUser.getPassword(), mapRolesToAuthorities(appUser.getRoles()));
}
private Collection<GrantedAuthority> mapRolesToAuthorities(List<Role> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
}
UserDeatilsService
: This is a core interface in Spring Security responsible for user authentication. Its primary method is loadUserByUsername(String username)
. This method takes a username and is expected to return an UserDetails
object containing user information for authentication.
CustomUserDetailsService
: This class extends the UserDetailsService
interface and implements its loadUserByUsername
method.
mapRolesToAuthorities
: This method takes a list of Role
objects from the AppUser
. It iterates through each Role
and creates a corresponding SimpleGrantedAuthority
object. These SimpleGrantedAuthority
objects represent the user’s authorization decisions.
After that, I add these two structures to the SecurityConfig
class.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
return http.build();
}
...
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
Now I can go to Postman and test the login
method:
Bingo! My register and login endpoints work.
And my project structure seems like this:
Ok, but when I log in, I want to get a JWT, not a string expression.
4. JWT Implementation
For the JWT implementation, I need three dependencies:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
JWT mechanism:
Firstly, I add the JwtAuthEntryPoint
under the security folder:
@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
This is a Spring Security component that handles unauthorized access attempts in a Spring Boot application that utilizes JSON Web Tokens (JWT) for authentication. It implements the AuthenticationEntryPoint
interface, which is part of Spring Security’s authentication flow.
It acts as a security checkpoint. If a request lacks a valid JWT, it rejects the request with a 401 (Unauthorized) status code and it can optionally provide an informative message.
Once it is created, it must be injected in SecurityConfig
file:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Autowired
private JwtAuthEntryPoint authEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(authEntryPoint));
http.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(c -> c.disable());
return http.build();
}
...
}
At this point, I can add sessionManagment
and exceptionHandling
to the SecurityFilterChain
as above.
sessionManagment
: This determines security policies related to HTTP sessions. These settings specify how user sessions are managed.
exceptionHandling
: This addresses exceptions that may occur during authentication and generates an appropriate response.
And now the JwtGenerator
class comes into play. Again under the security
folder:
@Component
public class JwtGenerator {
public long JWT_EXPIRATION = 70000;
private Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
public String generateToken(Authentication authentication) {
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + JWT_EXPIRATION);
// Extract roles from the authentication object
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// Build the token with roles included in claims
String token = Jwts
.builder()
.setSubject(username)
.claim("roles", roles) // Inclusion of roles in claims
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512,secretKey).compact();
return token;
}
public String getUsernameFromJWT(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (Exception ex) {
throw new AuthenticationCredentialsNotFoundException("JWT was exprired or incorrect",
ex.fillInStackTrace());
}
}
public List<String> getRolesFromJWT(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
return claims.get("roles", List.class);
}
}
JWT_EXPIRATION
: This is needed to be able to control the expiration time of the token. And it can be changed according to the application.
We have methods here that allow us to get the username, the user’s role list and to validate the token.
And we need a method that gets the token in the requests and validates and handles it. I write the codes below in the file JwtAuthenticationFilter
under the folder security
:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtGenerator tokenGenerator;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getJWTFromRequest(request);
if (StringUtils.hasText(token) && tokenGenerator.validateToken(token)) {
String username = tokenGenerator.getUsernameFromJWT(token);
List<String> roles = tokenGenerator.getRolesFromJWT(token);
List<GrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority(role.replace("ROLE_", "")))
.collect(Collectors.toList());
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
private String getJWTFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
The doFilterInternal
method:
- is called for each HTTP request.
- gets the JWT by the
getJWTFromRequest
. - validates it by
validateToken
method. - extracts the username and roles from the JWT (if it’s valid).
- loads user details based on the username and roles by
loadUserByUserName
. - creates a
UsernamePasswordAuthenticationToken
using the loaded user details and roles. - sets the created authentication details into the security context (
SecurityContextHolder
).
Finally, it proceeds to the next filter chain.
I also add this to the SecurityConfig
class. And after all the injections and additions in the SecurityClass
it seems so:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
public static final String ADMIN = "admin";
public static final String USER = "user";
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtAuthEntryPoint authEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz.requestMatchers("/api/**")
.permitAll()
.requestMatchers(HttpMethod.GET)
.permitAll()
.requestMatchers(HttpMethod.DELETE)
.hasAuthority(ADMIN)
.requestMatchers(HttpMethod.POST)
.permitAll()
.requestMatchers(HttpMethod.PUT)
.hasAuthority(USER)
.anyRequest()
.authenticated());
http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(authEntryPoint));
http.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(c -> c.disable());
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
Normally, when I log in, I get a ‘login successful’ message. But now I can get a JWT instead. For that, I create a response dto under the file dto
:
@Data
public class AuthResponseDto {
private String accessToken;
private String tokenType = "Bearer ";
public AuthResponseDto(String accessToken) {
this.accessToken = accessToken;
}
}
And then I go the AuthController
class and inject the JwtGenerator
and update the login
method:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
...
private JwtGenerator jwtGenerator;
@Autowired
public AuthController(AppUserRepository appUserRepository, RoleRepository roleRepository,
PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager,
JwtGenerator jwtGenerator) {
...
this.jwtGenerator = jwtGenerator;
}
...
@PostMapping("login")
public ResponseEntity<AuthResponseDto> login(@RequestBody LoginDto loginDto) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtGenerator.generateToken(authentication);
return new ResponseEntity<>(new AuthResponseDto(token), HttpStatus.OK);
}
}
And Now I can test it:
Conclusion
In this article, we discussed about JWT in Spring Security. We used a real database to save users and roles and to retrieve users.
We built a JWT mechanism in a Maven project with the necessary dependencies. And talked about how this mechanism works and what structures we need to build this mechanism.
The JWT mechanism in Spring Security is a little complex and some “imports” in the project files need to be seen in the example project in github. You can access the project via this link.
Thanks very much for your time and attention to my article.