In this tutorial, we’re gonna build a Spring Boot Login and Registration example (Rest API) that supports JWT with HttpOnly Cookie. You’ll know:
- Appropriate Flow for User Login and Registration with JWT and Cookies
- Spring Boot Rest Api Architecture with Spring Security
- How to configure Spring Security to work with JWT
- How to define Data Models and association for Authentication and Authorization
- Way to use Spring Data JPA to interact with MySQL Database
– Related Posts:
- Using Token in HTTP Headers
- Spring Boot, Spring Data JPA: Rest CRUD API example
- Spring Boot File upload example
- @RestControllerAdvice example in Spring Boot
- Spring Boot @ControllerAdvice & @ExceptionHandler example
- Spring Boot Unit Test for JPA Repository
- Spring Boot Unit Test for Rest Controller
- Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)
- Spring Boot Redis Cache example
- Spring Boot custom Validation example
Other Databases:
– Spring Boot Login and Registration example with JWT and H2
– Spring Boot Login and Registration example with JWT and MongoDB
Deployment:
Contents
- Overview
- Flow
- Architecture
- Technology
- Project Structure
- Setup Project
- Configuration
- Create the models
- Implement Repositories
- Configure Spring Security
- Implement UserDetails & UserDetailsService
- Filter the Requests
- Create JWT Utility class
- Handle Exception
- Define payloads
- Create Rest Controllers
- Run & Check
- Conclusion
- Source Code
- Further Reading
Overview of Spring Boot Login example
We will build a Spring Boot application in that:
- User can signup new account (registration), or login with username & password.
- By User’s role (admin, moderator, user), we authorize the User to access resources.
These are APIs that we need to provide:
Methods | Urls | Actions |
---|---|---|
POST | /api/auth/signup | signup new account |
POST | /api/auth/signin | login an account |
POST | /api/auth/signout | logout the account |
GET | /api/test/all | retrieve public content |
GET | /api/test/user | access User’s content |
GET | /api/test/mod | access Moderator’s content |
GET | /api/test/admin | access Admin’s content |
The database we will use is MySQL by configuring project dependency & datasource.
Flow of Spring Boot Login and Registration example
The diagram shows flow of how we implement User Registration, User Login and Authorization process.
A legal JWT must be stored in Cookies if Client accesses protected resources.
You may need to implement Refresh Token:
More details at: Spring Boot Refresh Token with JWT example
Spring Boot Rest API Server Architecture with Spring Security
You can have an overview of our Spring Boot Login example with the diagram below:
Now I will explain it briefly.
Spring Security
– WebSecurityConfig
is the crux of our security implementation. It configures cors, csrf, session management, rules for protected resources. We can also extend and customize the default configuration that contains the elements below.
(WebSecurityConfigurerAdapter
is deprecated from Spring 2.7.0, you can check the source code for update. More details at:
WebSecurityConfigurerAdapter Deprecated in Spring Boot)
– UserDetailsService
interface has a method to load User by username and returns a UserDetails
object that Spring Security can use for authentication and validation.
– UserDetails
contains necessary information (such as: username, password, authorities) to build an Authentication object.
– UsernamePasswordAuthenticationToken
gets {username, password} from login Request, AuthenticationManager
will use it to authenticate a login account.
– AuthenticationManager
has a DaoAuthenticationProvider
(with help of UserDetailsService
& PasswordEncoder
) to validate UsernamePasswordAuthenticationToken
object. If successful, AuthenticationManager
returns a fully populated Authentication object (including granted authorities).
– OncePerRequestFilter
makes a single execution for each request to our API. It provides a doFilterInternal()
method that we will implement parsing & validating JWT, loading User details (using UserDetailsService
), checking Authorizaion (using UsernamePasswordAuthenticationToken
).
– AuthenticationEntryPoint
will catch authentication error.
Repository contains UserRepository
& RoleRepository
to work with Database, will be imported into Controller.
Controller receives and handles request after it was filtered by OncePerRequestFilter
.
– AuthController
handles signup/login requests
– TestController
has accessing protected resource methods with role based validations.
Understand the architecture deeply and grasp the overview more easier:
Spring Boot Architecture for JWT with Spring Security
Technology
- Java 17 / 11 / 8
- Spring Boot 3 / 2 (with Spring Security, Spring Web, Spring Data JPA)
- jjwt 0.11.5
- MySQL
- Maven
Project Structure
This is folders & files structure for our Spring Boot Login example:
security: we configure Spring Security & implement Security Objects here.
WebSecurityConfig
UserDetailsServiceImpl
implementsUserDetailsService
UserDetailsImpl
implementsUserDetails
AuthEntryPointJwt
implementsAuthenticationEntryPoint
AuthTokenFilter
extendsOncePerRequestFilter
JwtUtils
provides methods for generating, parsing, validating JWT
(WebSecurityConfigurerAdapter
is deprecated from Spring 2.7.0, you can check the source code for update. More details at:
WebSecurityConfigurerAdapter Deprecated in Spring Boot)
controllers handle signup/login requests & authorized requests.
AuthController
: @PostMapping(‘/signup’), @PostMapping(‘/signin’), @PostMapping(‘/signout’)TestController
: @GetMapping(‘/api/test/all’), @GetMapping(‘/api/test/[role]’)
repository has interfaces that extend Spring Data JPA JpaRepository
to interact with Database.
UserRepository
extendsJpaRepository<User, Long>
RoleRepository
extendsJpaRepository<Role, Long>
models defines two main models for Authentication (User
) & Authorization (Role
). They have many-to-many relationship.
User
: id, username, email, password, rolesRole
: id, name
payload defines classes for Request and Response objects
We also have application.properties for configuring Spring Datasource, Spring Data JPA and App properties (such as JWT Secret string or Token expiration time).
Setup new Spring Boot Login project
Use Spring web tool or your development tool (Spring Tool Suite, Eclipse, Intellij) to create a Spring Boot project.
Then open pom.xml and add these dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
Configure Spring Datasource, JPA, App properties
Under src/main/resources folder, open application.properties, add some new lines.
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username= root
spring.datasource.password= 123456
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto= update
# App Properties
bezkoder.app.jwtCookieName= bezkoder
bezkoder.app.jwtSecret= bezKoderSecretKey
bezkoder.app.jwtExpirationMs= 86400000
spring.datasource.username
&spring.datasource.password
properties are the same as your database installation.- Spring Boot uses Hibernate for JPA implementation, we configure
MySQLDialect
for MySQL database spring.jpa.hibernate.ddl-auto
is used for database initialization. We set the value toupdate
value so that a table will be created in the database automatically corresponding to defined data model. Any change to the model will also trigger an update to the table. For production, this property should bevalidate
.
Create the models
We’re gonna have 3 tables in database: users, roles and user_roles for many-to-many relationship.
Let’s define these models.
In models package, create 3 files:
ERole
enum in ERole.java.
In this example, we have 3 roles corresponding to 3 enum.
package com.bezkoder.spring.login.models;
public enum ERole {
ROLE_USER,
ROLE_MODERATOR,
ROLE_ADMIN
}
Role
model in Role.java
package com.bezkoder.spring.login.models;
import jakarta.persistence.*;
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private ERole name;
public Role() {
}
public Role(ERole name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public ERole getName() {
return name;
}
public void setName(ERole name) {
this.name = name;
}
}
User
model in User.java.
It has 5 fields: id, username, email, password, roles.
package com.bezkoder.spring.login.models;
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "users",
uniqueConstraints = {
@UniqueConstraint(columnNames = "username"),
@UniqueConstraint(columnNames = "email")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(max = 20)
private String username;
@NotBlank
@Size(max = 50)
@Email
private String email;
@NotBlank
@Size(max = 120)
private String password;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
public User() {
}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
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 getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
Implement Repositories
Now, each model above needs a repository for persisting and accessing data. In repository package, let’s create 2 repositories.
UserRepository
There are 3 necessary methods that JpaRepository
supports.
package com.bezkoder.spring.login.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.bezkoder.spring.login.models.User;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}
RoleRepository
This repository also extends JpaRepository
and provides a finder method.
package com.bezkoder.spring.login.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.bezkoder.spring.login.models.ERole;
import com.bezkoder.spring.login.models.Role;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(ERole name);
}
Configure Spring Security
Without WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter
is deprecated from Spring 2.7.0. More details at:
WebSecurityConfigurerAdapter Deprecated in Spring Boot.
In security package, create WebSecurityConfig
class.
WebSecurityConfig.java
package com.bezkoder.spring.login.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.bezkoder.spring.login.security.jwt.AuthEntryPointJwt;
import com.bezkoder.spring.login.security.jwt.AuthTokenFilter;
import com.bezkoder.spring.login.security.services.UserDetailsServiceImpl;
@Configuration
//@EnableWebSecurity
@EnableMethodSecurity
//(securedEnabled = true,
//jsr250Enabled = true,
//prePostEnabled = true) // by default
public class WebSecurityConfig {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth ->
auth.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/test/**").permitAll()
.anyRequest().authenticated()
);
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
With WebSecurityConfigurerAdapter
In security package, create WebSecurityConfig
class that extends WebSecurityConfigurerAdapter
.
WebSecurityConfig.java
package com.bezkoder.spring.login.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.bezkoder.spring.login.security.jwt.AuthEntryPointJwt;
import com.bezkoder.spring.login.security.jwt.AuthTokenFilter;
import com.bezkoder.spring.login.security.services.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
// securedEnabled = true,
// jsr250Enabled = true,
prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
Let me explain the code above.
– @EnableWebSecurity
allows Spring to find and automatically apply the class to the global Web Security.
*For Spring Boot 2: @EnableGlobalMethodSecurity
provides AOP security on methods. It enables @PreAuthorize
, @PostAuthorize
, it also supports JSR-250. You can find more parameters in configuration in Method Security Expressions.
– @EnableGlobalMethodSecurity
is deprecated in Spring Boot 3. You can use @EnableMethodSecurity
instead. For more details, please visit Method Security.
– We override the configure(HttpSecurity http)
method from WebSecurityConfigurerAdapter
interface. It tells Spring Security how we configure CORS and CSRF, when we want to require all users to be authenticated or not, which filter (AuthTokenFilter
) and when we want it to work (filter before UsernamePasswordAuthenticationFilter
), which Exception Handler is chosen (AuthEntryPointJwt
).
– Spring Security will load User details to perform authentication & authorization. So it has UserDetailsService
interface that we need to implement.
– The implementation of UserDetailsService
will be used for configuring DaoAuthenticationProvider
by AuthenticationManagerBuilder.userDetailsService()
method.
– We also need a PasswordEncoder
for the DaoAuthenticationProvider
. If we don’t specify, it will use plain text.
Implement UserDetails & UserDetailsService
If the authentication process is successful, we can get User’s information such as username, password, authorities from an Authentication
object.
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()
If we want to get more data (id, email…), we can create an implementation of this UserDetails
interface.
security/services/UserDetailsImpl.java
package com.bezkoder.spring.login.security.services;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.bezkoder.spring.login.models.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Long getId() {
return id;
}
public String getEmail() {
return email;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}
}
Look at the code above, you can notice that we convert Set<Role>
into List<GrantedAuthority>
. It is important to work with Spring Security and Authentication
object later.
As I have said before, we need UserDetailsService
for getting UserDetails
object. You can look at UserDetailsService
interface that has only one method:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
So we implement it and override loadUserByUsername()
method.
security/services/UserDetailsServiceImpl.java
package com.bezkoder.spring.login.security.services;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.transaction.annotation.Transactional;
import com.bezkoder.spring.login.models.User;
import com.bezkoder.spring.login.repository.UserRepository;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(user);
}
}
In the code above, we get full custom User object using UserRepository
, then we build a UserDetails
object using static build()
method.
Filter the Requests
Let’s define a filter that executes once per request. So we create AuthTokenFilter
class that extends OncePerRequestFilter
and override doFilterInternal()
method.
security/jwt/AuthTokenFilter.java
package com.bezkoder.spring.login.security.jwt;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import com.bezkoder.spring.login.security.services.UserDetailsServiceImpl;
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String jwt = jwtUtils.getJwtFromCookies(request);
return jwt;
}
}
What we do inside doFilterInternal()
:
– get JWT
from the HTTP Cookies
– if the request has JWT
, validate it, parse username
from it
– from username
, get UserDetails
to create an Authentication
object
– set the current UserDetails
in SecurityContext using setAuthentication(authentication)
method.
After this, everytime you want to get UserDetails
, just use SecurityContext
like this:
UserDetails userDetails =
(UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()
Create JWT Utility class
This class has 3 main funtions:
getJwtFromCookies
: getJWT
from Cookies by Cookie namegenerateJwtCookie
: generate a Cookie containingJWT
from username, date, expiration, secretgetCleanJwtCookie
: return Cookie withnull
value (used for clean Cookie)getUserNameFromJwtToken
: get username fromJWT
validateJwtToken
: validate aJWT
with a secret
security/jwt/JwtUtils.java
package com.bezkoder.spring.login.security.jwt;
import java.security.Key;
import java.util.Date;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;
import com.bezkoder.spring.login.security.services.UserDetailsImpl;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${bezkoder.app.jwtSecret}")
private String jwtSecret;
@Value("${bezkoder.app.jwtExpirationMs}")
private int jwtExpirationMs;
@Value("${bezkoder.app.jwtCookieName}")
private String jwtCookie;
public String getJwtFromCookies(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, jwtCookie);
if (cookie != null) {
return cookie.getValue();
} else {
return null;
}
}
public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
String jwt = generateTokenFromUsername(userPrincipal.getUsername());
ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true).build();
return cookie;
}
public ResponseCookie getCleanJwtCookie() {
ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build();
return cookie;
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parserBuilder().setSigningKey(key()).build()
.parseClaimsJws(token).getBody().getSubject();
}
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
return true;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(key(), SignatureAlgorithm.HS256)
.compact();
}
}
Remember that we’ve added bezkoder.app.jwtSecret
, bezkoder.app.jwtExpirationMs
and bezkoder.app.jwtCookieName
properties in application.properties
file, and jwtSecret
has 64 characters.
Handle Authentication Exception
Now we create AuthEntryPointJwt
class that implements AuthenticationEntryPoint
interface. Then we override the commence()
method. This method will be triggerd anytime unauthenticated User requests a secured HTTP resource and an AuthenticationException
is thrown.
security/jwt/AuthEntryPointJwt.java
package com.bezkoder.spring.login.security.jwt;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
HttpServletResponse.SC_UNAUTHORIZED
is the 401 Status code. It indicates that the request requires HTTP authentication.
If you want to customize the response data, just use an ObjectMapper
like following code:
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
We’ve already built all things for Spring Security. The next sections of this tutorial will show you how to implement Controllers for our Rest APIs.
Define payloads for Authentication Controller
Let me summarize the payloads for our RestAPIs:
– Requests:
- LoginRequest: { username, password }
- SignupRequest: { username, email, password }
– Responses:
- UserInfoResponse: { id, username, email, roles }
- MessageResponse: { message }
To keep the tutorial not so long, I don’t show these POJOs here.
You can find details for payload classes in source code of the project on Github.
Create Spring Rest Controllers
Controller for Authentication
This controller provides APIs for register and login, logout actions.
– /api/auth/signup
- check existing
username
/email
- create new
User
(withROLE_USER
if not specifying role) - save
User
to database usingUserRepository
– /api/auth/signin
- authenticate { username, pasword }
- update
SecurityContext
usingAuthentication
object - generate
JWT
- get
UserDetails
fromAuthentication
object - response contains
JWT
andUserDetails
data
– /api/auth/signout
: clear the Cookie.
controllers/AuthController.java
package com.bezkoder.spring.login.controllers;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bezkoder.spring.login.models.ERole;
import com.bezkoder.spring.login.models.Role;
import com.bezkoder.spring.login.models.User;
import com.bezkoder.spring.login.payload.request.LoginRequest;
import com.bezkoder.spring.login.payload.request.SignupRequest;
import com.bezkoder.spring.login.payload.response.UserInfoResponse;
import com.bezkoder.spring.login.payload.response.MessageResponse;
import com.bezkoder.spring.login.repository.RoleRepository;
import com.bezkoder.spring.login.repository.UserRepository;
import com.bezkoder.spring.login.security.jwt.JwtUtils;
import com.bezkoder.spring.login.security.services.UserDetailsImpl;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.body(new UserInfoResponse(userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!"));
}
// Create new user's account
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRole();
Set<Role> roles = new HashSet<>();
if (strRoles == null) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
switch (role) {
case "admin":
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
break;
case "mod":
Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(modRole);
break;
default:
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}
@PostMapping("/signout")
public ResponseEntity<?> logoutUser() {
ResponseCookie cookie = jwtUtils.getCleanJwtCookie();
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new MessageResponse("You've been signed out!"));
}
}
Controller for testing Authorization
There are 4 APIs:
– /api/test/all
for public access
– /api/test/user
for users has ROLE_USER
or ROLE_MODERATOR
or ROLE_ADMIN
– /api/test/mod
for users has ROLE_MODERATOR
– /api/test/admin
for users has ROLE_ADMIN
Do you remember that we used @EnableGlobalMethodSecurity(prePostEnabled = true)
(or @EnableMethodSecurity
for Spring Boot 3) in WebSecurityConfig
class?
@Configuration
// @EnableWebSecurity
// @EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableMethodSecurity // Spring Boot 3
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }
Now we can secure methods in our Apis with @PreAuthorize
annotation easily.
controllers/TestController.java
package com.bezkoder.spring.login.controllers;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/all")
public String allAccess() {
return "Public Content.";
}
@GetMapping("/user")
@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
public String userAccess() {
return "User Content.";
}
@GetMapping("/mod")
@PreAuthorize("hasRole('MODERATOR')")
public String moderatorAccess() {
return "Moderator Board.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board.";
}
}
Run & Check
Run Spring Boot application with command: mvn spring-boot:run
Tables that we define in models package will be automatically generated in Database.
If you check MySQL database for example, you can see things like this:
mysql> describe users;
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| email | varchar(50) | YES | UNI | NULL | |
| password | varchar(120) | YES | | NULL | |
| username | varchar(20) | YES | UNI | NULL | |
+----------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
mysql> describe roles;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)
mysql> describe user_roles;
+---------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| user_id | bigint(20) | NO | PRI | NULL | |
| role_id | int(11) | NO | PRI | NULL | |
+---------+------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
We also need to add some rows into roles table before assigning any role to User.
Run following SQL insert statements:
INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');
Then check the tables:
mysql> select * from roles;
+----+----------------+
| id | name |
+----+----------------+
| 1 | ROLE_USER |
| 2 | ROLE_MODERATOR |
| 3 | ROLE_ADMIN |
+----+----------------+
3 rows in set (0.00 sec)
Register some users with /signup
API:
- admin with
ROLE_ADMIN
- mod with
ROLE_MODERATOR
andROLE_USER
- zkoder with
ROLE_USER
mysql> select * from users;
+----+--------------------+--------------------------------------------------------------+----------+
| id | email | password | username |
+----+--------------------+--------------------------------------------------------------+----------+
| 1 | [email protected] | $2a$10$XEGS8J0hIUII6kdPgT9ykuRTwPCdDiDzt24eOcodgi16kTMQPfqna | admin |
| 2 | [email protected] | $2a$10$jfLldGLZ0jTVRIt.mSqkxen.tHA3ynsr4.FWTZVM1rOp3STVLi8Ke | mod |
| 3 | [email protected] | $2a$10$IcQFF9tvJt/idNGejhbWMuh9voYXB15qTc7YXQNpLyuEyNE.oSxge | zkoder |
+----+--------------------+--------------------------------------------------------------+----------+
3 rows in set (0.00 sec)
mysql> select * from roles;
+----+----------------+
| id | name |
+----+----------------+
| 1 | ROLE_USER |
| 2 | ROLE_MODERATOR |
| 3 | ROLE_ADMIN |
+----+----------------+
3 rows in set (0.00 sec)
mysql> select * from user_roles;
+---------+---------+
| user_id | role_id |
+---------+---------+
| 2 | 1 |
| 3 | 1 |
| 2 | 2 |
| 1 | 3 |
+---------+---------+
4 rows in set (0.00 sec)
Access public resource: GET /api/test/all
Access protected resource without Login: GET /api/test/user
Login an account: POST /api/auth/signin
Check the Cookies:
Access ROLE_USER
and ROLE_MODERATOR
resource:
– GET /api/test/user
– GET /api/test/mod
Access ROLE_ADMIN
resource: GET /api/test/admin
Logout the Account: POST /api/auth/signout
Solve Problem with JDK 14
If you run this Spring Boot App with JDK 14 and get following error when trying to authenticate:
FilterChain java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
Just add following dependency to pom.xml:
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.2</version>
</dependency>
Everything’s gonna work fine.
Conclusion
Today we’ve learned so many interesting things about Spring Boot Login and Registration example with MySQL using Spring Security and JWT with HttpOnly Cookies.
For understanding the architecture deeply and grasp the overview more easier:
Spring Boot Architecture for JWT with Spring Security
You should continue to know how to implement Refresh Token:
Spring Boot Refresh Token with JWT example
Validate the signup request (password, confirm password):
Spring Boot custom Validation example
You can also know how to deploy Spring Boot App on AWS (for free) with this tutorial.
Happy learning! See you again.
Further Reading
- Spring Security Reference
- In-depth Introduction to JWT-JSON Web Token
- Architecture: Spring Boot 2 JWT Authentication with Spring Security
Related Posts:
- Spring Boot, Spring Data JPA – Building Rest CRUD API example
- Spring Boot Pagination & Filter example | Spring JPA, Pageable
- CRUD GraphQL APIs example with Spring Boot, MySQL & Spring JPA
- Spring Boot Rest XML example – Web service with XML Response
- Spring Boot File upload example with Multipart File
- @RestControllerAdvice example in Spring Boot
- Spring Boot @ControllerAdvice & @ExceptionHandler example
Deployment:
Fullstack CRUD App:
– Vue + Spring Boot example
– Angular 8 + Spring Boot example
– Angular 10 + Spring Boot example
– Angular 11 + Spring Boot example
– Angular 12 + Spring Boot example
– Angular 13 + Spring Boot example
– Angular 14 + Spring Boot example
– Angular 15 + Spring Boot example
– Angular 16 + Spring Boot example
– React + Spring Boot example
If you need a working front-end for this back-end, you can find Client App in the posts:
– Angular 12 / Angular 13 / Angular 14 / Angular 15 / Angular 16
– React / React Redux
Other Databases:
– Spring Boot Login and Registration example with JWT and H2
– Spring Boot Login and Registration example with JWT and MongoDB
Associations:
– JPA One To One example with Hibernate in Spring Boot
– JPA One To Many example with Hibernate and Spring Boot
– JPA Many to Many example with Hibernate in Spring Boot
Unit Test:
– Spring Boot Unit Test for JPA Repository
– Spring Boot Unit Test for Rest Controller
Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)
Source Code
You can find the complete source code for this tutorial on Github.
Hi BezKoder,
Can you please write a post on Angular 13 explaining how to use httponly cookie instead of local storage while working with this SpringBoot backend? It would be really helpful for us.
Hi, here you are:
Angular 13 JWT Authentication & Authorization with HttpOnly Cookie
Hi Mr Bezkoder,
Could you show me how to use both jwt with header and jwt with HttpOnly Cookie. My project use jwt with HttpOnly Cookie for web api and jwt with header for either api.
Thank you so much.
Ps Sorry for my poor English.
thanks for your tutorial, please tell me why do you use
@JsonIgnore
for property password inUserDetailsImpl
class? and Why do you use@Transactional
inloadUserByUsername()
method inUserDetailsServiceImpl
.Great tutorial! Was wondering if you could delve a little deeper into getting a working front-end for this.
I don’t know how to modify from localStorage to Cookies.
Hey! Thank you for your great tutorials 🙂
I stumbled upon an error and I can’t seem to figure out why.
When I run the application, and use postman to sign in, the console in STS says:
“Invalid JWT): JWT String argument cannot be null or empty.”
At the same time, it seems to be working on postman.
Any ideas why? Any help would be greatly appreciated.
Adding onto this, it doesn’t seem to give an exception like the one above whenever i signout, and sign back in with a different postman tab – I realize this means there is nothing wrong with my code, and I’ve been making my calls to the APIs wrong.
Sorry for the inconvenience haha
Really useful! Thanks a lot!
This is a very well-written Spring tutorial. It took me a while to find this but it was worth the time. Thanks!