How to make a Spring Boot application layout with JWT, authorizing users based on roles over the end-points using JPA, and JpaRespository
the main purpose of this project is to be able to set up a Spring Boot application using a security layer
with spring-boot-starter-security,JSON web token to protect any path we need, just allowing
the endpoints we need according to the roles who have got with the right permissions,
the roles we are going to be working with are SUPERADMIN, ADMIN AND USER.
we are also going to be using on the project spring-boot-starter-actuator,
spring-boot-starter-web, spring-boot-starter-data-jpa,spring-boot-devtools, and mysql-connector-java.
as developers we need to take advantage of time just focusing the most that we can on the business logic
of the application rather than the standard functionality of the app, it takes a while when starting a project
from scratch and even more when we are just starting in a new back-end language, in this case,
Spring Boot, so this tutorial is for those ones who are starting in Spring boot and need to start a
project from scratch wondering about how to deal with roles, how to make a login and signup end-points
for their users according to the roles, how to deal with JWT, how to protect their APIs or end-points,
in other words, how to set up the basic things to start working on the business logic of the solution
they must give on the app, so I just upload the sources of a standard base project in which you just
need to pay some attention about how it works and you are done after you do it, enabling you
to start working on the solution you have got ahead of you.
I am sure this can be helpful for many of you as me for instance with a solution which allows me to start
a new project in just than a few minutes.
You can download the whole project from GitHub at https://github.com/juandavidmarin368/SpringBoot-JWT
compile it and just take a look at how it works,
we are going to be opening https://start.spring.io/ in order to have our base project,
for this purpose, I just added these dependencies, override your dependencies by copy and pasting what I have set in here,
or if you want just get
or if you want just get
<!-- For Working with Json Web Tokens (JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- For Java 8 Date/Time Support -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
Which are not on the spring initialzr, so that's why I pasted the whole pom dependency used on the project
<dependencies>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies> |
so after downloaded the zip file, uncompressed it and open it with Visual Studio code or
any other code editor, create this folder structure like this one
any other code editor, create this folder structure like this one
I just gave this folder and file nomenclature names for easy reading, so you can use your own,
the main idea is avoiding mixing security files and folders from the application business logic or
if you want to have all mixed is up to you the folder and file structure names.
AplicationLayer = this is the package where we are going to be doing our application,
so I have created 2 subfolders JDBC and JPA, if you are going to be working with JbcTemplate under
JDBC package is where you going to create the required subpages to have a better order
And the same thing if you are going to be working Hibernate ORM using JPA.
And why JDBC on Spring Boot ?, well, sometimes it can be helpful for those ones who are used to work
more with JDBC rather than Hibernate which uses JPA, so for instance in my case the other day I did a
Hybrid application where I did some customized reports using Jdbctemplate and it was faster for me doing
that rather than using Hibernate with JPA, well I mean I used JDBC for many years, and I am sure if you are one of
those ones like me you’ll understand me later, “ I am not saying JdbcTemplate is better or not than JPA,
I am just saying you must use the tools which let you take advantage of what you are doing against time schedule “
JwtSecurityLayer = under this package is the whole security layer even with the JWT
To move forward we supposed you to have a Mysql database already up and running,
so lets open this file src/main/resources/application.properties and paste these settings.
so lets open this file src/main/resources/application.properties and paste these settings.
I have created a database and called it dataTest, replace it by yours and the same thing with
spring.datasource.username and spring.datasource.password
logging.level.org.springframework.web=DEBUG
# Database spring.datasource.driver = com.mysql.jdbc.Driver spring.datasource.url = jdbc:mysql://localhost:3306/dataTest?serverTimezone=UTC spring.datasource.username = root spring.datasource.password = spring.jpa.show-sql = true spring.jpa.hibernate.ddl-auto = update spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect |
The security layout is going to be used with the Hibernate ORM using JPA, This is the folder structure
under the package JwtSecurityLayer:
under the package JwtSecurityLayer:
Config
Controllers
Exceptions
Models
Payload
Repositories
Security
So let’s get started creating the models: for the base layout application we are going to be creating
Users to signup and login and Roles which the Users are going to be.
Users to signup and login and Roles which the Users are going to be.
DateAudit model
It will have a createdAt and an updatedAt field, this is going to be useful when creating users so that
we'll be able to know when an user was created and updated, this is going to be under the package
Models with the name audit package com.root.Generic.JwtSecurityLayer.Models.audit;
we'll be able to know when an user was created and updated, this is going to be under the package
Models with the name audit package com.root.Generic.JwtSecurityLayer.Models.audit;
package com.root.Generic.JwtSecurityLayer.Models.audit;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; import java.io.Serializable; import java.time.Instant; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @JsonIgnoreProperties( value = {"createdAt", "updatedAt"}, allowGetters = true ) public abstract class DateAudit implements Serializable { @CreatedDate @Column(nullable = false, updatable = false) private Instant createdAt; @LastModifiedDate @Column(nullable = false) private Instant updatedAt; public Instant getCreatedAt() { return createdAt; } public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } public Instant getUpdatedAt() { return updatedAt; } public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } } |
Inside the package ConfigSecurity we are going to be creating AuditingConfig which is the one that
is going to enable JPA Auditing, We’re creating a separate class because we’ll be adding more auditing
related configurations later
is going to enable JPA Auditing, We’re creating a separate class because we’ll be adding more auditing
related configurations later
package com.root.Generic.JwtSecurityLayer.Config;
import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration @EnableJpaAuditing public class AuditingConfig { // That's all here for now. We'll add more auditing configurations later. } |
Every User will have one or more roles. so according to the given role the authorized user will
have access to a particular resource in the API or not
have access to a particular resource in the API or not
User model
The User model contains the following fields -
* id: Primary Key
* username: A unique username
* email: A unique email
* password: A password which will be stored in an encrypted format.
* roles: A set of roles. (Many-To-Many relationship with Role entity)
package com.root.Generic.JwtSecurityLayer.Models;
import java.util.HashSet; import java.util.Set; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import com.root.Generic.JwtSecurityLayer.Models.audit.DateAudit; import org.hibernate.annotations.NaturalId; @Entity @Table(name = "users", uniqueConstraints = { @UniqueConstraint(columnNames = { "username" }), @UniqueConstraint(columnNames = { "email" }) }) public class User extends DateAudit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank @Size(max = 40) private String name; @NotBlank @Size(max = 15) private String username; @NaturalId @NotBlank @Size(max = 40) private String email; @NotBlank @Size(max = 100) 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 name, String username, String email, String password) { this.name = name; 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 getName() { return name; } public void setName(String name) { this.name = name; } 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; } } |
Role model
package com.root.Generic.JwtSecurityLayer.Models;
import org.hibernate.annotations.NaturalId; import javax.persistence.*; @Entity @Table(name = "roles") public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Enumerated(EnumType.STRING) @NaturalId @Column(length = 60) private RoleName name; public Role() { } public Role(RoleName name) { this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public RoleName getName() { return name; } public void setName(RoleName name) { this.name = name; } } |
RoleName enum
package com.root.Generic.JwtSecurityLayer.Models;
public enum RoleName { ROLE_USER, ROLE_ADMIN, ROLE_SUPERADMIN, } |
Repositories for accessing User and Role data
as we have got now the models, let's create the repositories to persist and get the data from them,
all repositories will be under the package Repositories
all repositories will be under the package Repositories
UserRepository
package com.root.Generic.JwtSecurityLayer.Repositories;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; import com.root.Generic.JwtSecurityLayer.Models.User; @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); Optional<User> findByUsernameOrEmail(String username, String email); List<User> findByIdIn(List<Long> userIds); Optional<User> findByUsername(String username); Boolean existsByUsername(String username); Boolean existsByEmail(String email); } |
RoleRepository
package com.root.Generic.JwtSecurityLayer.Repositories;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; import com.root.Generic.JwtSecurityLayer.Models.Role; import com.root.Generic.JwtSecurityLayer.Models.RoleName; @Repository public interface RoleRepository extends JpaRepository<Role, Long> { Optional<Role> findByName(RoleName roleName); } |
Let’s make an insert into the database to fill the roles
INSERT IGNORE INTO roles(name) VALUES('ROLE_USER');
INSERT IGNORE INTO roles(name) VALUES('ROLE_ADMIN'); INSERT IGNORE INTO roles(name) VALUES('ROLE_SUPERADMIN'); |
CREATING THE APIS TO SIGNUP AND LOGIN NEW USERS
We must create an API that registers new users with their name, username, email and password and let users log in using their username/email and password. if the credentials match, the API should generate a JWT authentication token and return the token in the response.
The clients will send back this JWT token in the Authorization header requests to access any protected resources.
we are going to be setting up Spring security to restrict access and protect resources. For example,
APIs for login, signup, and any other static resource must be accessible to everyone.
APIs to create or get any resource which requires authorization must be protected
Configure Spring security to throw a 401 unauthorized error if a client tries to access a protected resource without a valid JWT token.
Configure Role-based Authorization to protect resources on the APIs for instance:
Only users with role ADMIN can create or get any resource which belongs to that role with @PreAuthorize("hasRole('ADMIN')") and so on with USER and SUPERADMIN @PreAuthorize("hasRole('SUPERADMIN')") or @PreAuthorize("hasRole('USER')")
Setting up Spring Security and JWT
let's created the SecurityConfig.class file which is the core of our security so that it has got all the configurations that are required for our project.
package com.root.Generic.JwtSecurityLayer.Config;
import com.root.Generic.JwtSecurityLayer.Security.CustomUserDetailsService;
import com.root.Generic.JwtSecurityLayer.Security.JwtAuthenticationEntryPoint;
import com.root.Generic.JwtSecurityLayer.Security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
Autowired
CustomUserDetailsService customUserDetailsService;
Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
userDetailsService(customUserDetailsService)
passwordEncoder(passwordEncoder());
Bean(BeanIds.AUTHENTICATION_MANAGER)
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("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
permitAll()
antMatchers("/api/auth/**")
permitAll()
antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
permitAll()
antMatchers(HttpMethod.GET, "/api/users/**", "/api/users/**")
permitAll()
anyRequest()
authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
|
While you are creating all these new files you are going to be getting errors so that there are files which have not been created yet.
Let’s review some annotations which were used before
1. The @EnableWebSecurity annotation is crucial if we disable the default security configuration, in other words is the primary spring security annotation that is used to enable web security in a project.
2. By annotating the class with @EnableGlobalMethodSecurity, we can enable method level security using annotations. We can optionally configure which annotations we’ll allow. You can enable one of the following.
securedEnabled – enables the spring @Secured annotation, which you can protect your controller/service methods like this
@Secured("ROLE_ADMIN")
public User getAllUsers() {} @Secured({"ROLE_USER", "ROLE_ADMIN"}) public User getUser(Long id) {} @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public boolean isUsernameAvailable() {} |
jsr250Enabled – enables the JSR-250 standard java security annotations, and enables the @RolesAllowed annotation that can be used like this:
@RolesAllowed("ROLE_ADMIN")
public TestResource getData() {} |
prePostEnabled – enables the spring @PreAuthorize and PostAuthorize annotations enabling more complex expression based access control syntax with @PreAuthorize and @PostAuthorize annotations like this:
@PreAuthorize("isAnonymous()")
public boolean isUsernameAvailable() {} @PreAuthorize("hasRole('USER')") public TestResource getData() {} |
You can get more information about this Spring security annotations in here
3. This class implements Spring Security’s WebSecurityConfigurer interface and by extending the WebSecurityConfigurerAdapter, we can configure the endpoints that should be secured and the endpoint that should be public.
4. CustomUserDetailsService
To authenticate a User or perform various role-based checks, Spring security needs to load users details somehow.
For this purpose, It consists of an interface called UserDetailsService which has a single method that loads a user based on username
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
|
We’ll define a CustomUserDetailsService that implements UserDetailsService interface and provides the implementation for loadUserByUsername() method.
Note that, the loadUserByUsername() method returns a UserDetails object that Spring Security uses for performing various authentication and role based validations.
In our implementation, We’ll also define a custom UserPrincipal class that will implement UserDetails interface, and return the UserPrincipal object from loadUserByUsername() method.
5. JwtAuthenticationEntryPoint
This class is used to return a 401 unauthorized error to clients that try to access a protected resource without proper authentication. It implements Spring Security’s AuthenticationEntryPoint interface
6. JwtAuthenticationEntryPoint
We’ll use JWTAuthenticationFilter to implement a filter that -
* reads JWT authentication token from the Authorization header of all the requests
* validates the token
* loads the user details associated with that token.
* Sets the user details in Spring Security’s SecurityContext. Spring Security uses the User Details to perform authorization checks. We can also access the UserDetails stored in the SecurityContext in our controllers to perform our business logic.
7. AuthenticationManagerBuilder and AuthenticationManager
AuthenticationManagerBuilder is used to create an AuthenticationManager instance which is the main Spring Security interface for authenticating a user.
You can use AuthenticationManagerBuilder to build in-memory authentication, LDAP authentication, JDBC authentication, or add your custom authentication provider.
In our example, we’ve provided our customUserDetailsService and a passwordEncoder to build the AuthenticationManager.
We’ll use the configured AuthenticationManager to authenticate a user in the login API.
8. HttpSecurity configurations
The HttpSecurity configurations are used to configure security functionalities like csrf, sessionManagement, and add rules to protect resources based on various conditions.
In our example, we’re permitting access to static resources and few other public APIs to everyone and restricting access to other APIs to authenticated users only.
We’ve also added the JWTAuthenticationEntryPoint and the custom JWTAuthenticationFilter in the HttpSecurity configuration.
Creating Custom Spring Security Classes, Filters, and Annotations
In the previous section, we configured spring security with many custom classes and filters. In this section, we’ll define those classes one by one.
All the following custom security related classes will go inside a package named package com.projectOne.TestOne.JwtSecurityLayer.Security;
1. Custom Spring Security AuthenticationEntryPoint
The first spring security related class that we’ll define is JwtAuthenticationEntryPoint. It implements AuthenticationEntryPoint interface and provides the implementation for its commence() method. This method is called whenever an exception is thrown due to an unauthenticated user trying to access a resource that requires authentication.
In this case, we’ll simply respond with a 401 error containing the exception message.
package com.projectOne.TestOne.JwtSecurityLayer.Security;
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { logger.error("Responding with unauthorized error. Message - {}", e.getMessage()); httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); } } |
2. Custom Spring Security UserDetails
Next, Let’s define our custom UserDetails class called UserPrincipal. This is the class whose instances will be returned from our custom UserDetailsService. Spring Security will use the information stored in the UserPrincipal object to perform authentication and authorization.
Here is the complete UserPrincipal class:
package com.projectOne.TestOne.JwtSecurityLayer.Security;
import com.projectOne.TestOne.JwtSecurityLayer.Models.User; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public class UserPrincipal implements UserDetails { private Long id; private String name; private String username; @JsonIgnore private String email; @JsonIgnore private String password; private Collection<? extends GrantedAuthority> authorities; public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) { this.id = id; this.name = name; this.username = username; this.email = email; this.password = password; this.authorities = authorities; } public static UserPrincipal create(User user) { List<GrantedAuthority> authorities = user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName().name()) ).collect(Collectors.toList()); return new UserPrincipal( user.getId(), user.getName(), user.getUsername(), user.getEmail(), user.getPassword(), authorities ); } public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @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; UserPrincipal that = (UserPrincipal) o; return Objects.equals(id, that.id); } @Override public int hashCode() { return Objects.hash(id); } } |
3. Custom Spring Security UserDetailsService
Now let’s define the custom UserDetailsService which loads a user’s data given its username:
package com.projectOne.TestOne.JwtSecurityLayer.Security;
import com.projectOne.TestOne.JwtSecurityLayer.Exceptions.ResourceNotFoundException; import com.projectOne.TestOne.JwtSecurityLayer.Models.User; import com.projectOne.TestOne.JwtSecurityLayer.Repositories.UserRepository; 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; @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired UserRepository userRepository; @Override @Transactional public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException { // Let people login with either username or email User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail) .orElseThrow(() -> new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail) ); return UserPrincipal.create(user); } @Transactional public UserDetails loadUserById(Long id) { User user = userRepository.findById(id).orElseThrow( () -> new ResourceNotFoundException("User", "id", id) ); return UserPrincipal.create(user); } } |
The first method loadUserByUsername() is used by Spring security. Notice the use of findByUsernameOrEmail method. This allows users to log in using either username or email.
The second method loadUserById() will be used by JWTAuthenticationFilter that we’ll define shortly.
4. Utility class for generating and verifying JWT
The following utility class will be used for generating a JWT after a user logs in successfully, and validating the JWT sent in the Authorization header of the requests:
package com.projectOne.TestOne.JwtSecurityLayer.Security;
import io.jsonwebtoken.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtTokenProvider { private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class); @Value("${app.jwtSecret}") private String jwtSecret; @Value("${app.jwtExpirationInMs}") private int jwtExpirationInMs; public String generateToken(Authentication authentication) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); return Jwts.builder() .setSubject(Long.toString(userPrincipal.getId())) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } public Long getUserIdFromJWT(String token) { Claims claims = Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody(); return Long.parseLong(claims.getSubject()); } public boolean validateToken(String authToken) { try { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); return true; } catch (SignatureException ex) { logger.error("Invalid JWT signature"); } catch (MalformedJwtException ex) { logger.error("Invalid JWT token"); } catch (ExpiredJwtException ex) { logger.error("Expired JWT token"); } catch (UnsupportedJwtException ex) { logger.error("Unsupported JWT token"); } catch (IllegalArgumentException ex) { logger.error("JWT claims string is empty."); } return false; } } |
The utility class reads the JWT secret and expiration time from properties.
Let’s add the jwtSecret and jwtExpirationInMs properties in the application.properties file
app.jwtSecret= JWTSuperSecretKey
app.jwtExpirationInMs = 7200000 |
The JWT will expire each 2 hours
5. Custom Spring Security AuthenticationFilter
Finally, Let’s write the JWTAuthenticationFilter to get the JWT token from the request, validate it, load the user associated with the token, and pass it to Spring Security:
package com.projectOne.TestOne.JwtSecurityLayer.Security;
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.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider tokenProvider; @Autowired private CustomUserDetailsService customUserDetailsService; private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { Long userId = tokenProvider.getUserIdFromJWT(jwt); UserDetails userDetails = customUserDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { logger.error("Could not set user authentication in security context", ex); } 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; } } |
In the above filter, We’re first parsing the JWT retrieved from the Authorization header of the request and obtaining the user’s Id. After that, We’re loading the user’s details from the database and setting the authentication inside spring security’s context.
Note that, the database hit in the above filter is optional. You could also encode the user’s username and roles inside JWT claims and create the UserDetails object by parsing those claims from the JWT. That would avoid the database hit.
However, Loading the current details of the user from the database might still be helpful. For example, you might wanna disallow login with this JWT if the user’s role has changed, or the user has updated his password after the creation of this JWT.
6. Custom annotation to access currently logged in user
Spring security provides an annotation called @AuthenticationPrincipal to access the currently authenticated user in the controllers.
The following CurrentUser annotation is a wrapper around @AuthenticationPrincipal annotation.
package com.projectOne.TestOne.JwtSecurityLayer.Security;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import java.lang.annotation.*; @Target({ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @AuthenticationPrincipal public @interface CurrentUser { } |
We’ve created a meta-annotation so that we don’t get too much tied up of with Spring Security related annotations everywhere in our project. This reduces the dependency on Spring Security. So if we decide to remove Spring Security from our project, we can easily do it by simply changing the CurrentUser annotation:
Writing the Login and Signup APIs
Before creating the APIs, we’ll need to define the request and response payloads that the APIs will use. So let’s define these payloads first.
All the request and response payloads will go inside a package named com.root.Generic.JwtSecurityLayer.Payload;
1. LoginRequest
package com.root.Generic.JwtSecurityLayer.Payload;
import javax.validation.constraints.NotBlank; public class LoginRequest { @NotBlank private String usernameOrEmail; @NotBlank private String password; public String getUsernameOrEmail() { return usernameOrEmail; } public void setUsernameOrEmail(String usernameOrEmail) { this.usernameOrEmail = usernameOrEmail; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } |
2. SignUpRequest
package com.root.Generic.JwtSecurityLayer.Payload;
import javax.validation.constraints.*; public class SignUpRequest { @NotBlank @Size(min = 4, max = 40) private String name; @NotBlank @Size(min = 3, max = 15) private String username; @NotBlank @Size(max = 40) private String email; @NotBlank @Size(min = 6, max = 20) private String password; public String getName() { return name; } public void setName(String name) { this.name = name; } 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; } } |
Response Payloads
1. JwtAuthenticationResponse
package com.root.Generic.JwtSecurityLayer.Payload;
public class JwtAuthenticationResponse { private String accessToken; private String tokenType = "Bearer"; public JwtAuthenticationResponse(String accessToken) { this.accessToken = accessToken; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getTokenType() { return tokenType; } public void setTokenType(String tokenType) { this.tokenType = tokenType; } } |
2. ApiResponse
package com.root.Generic.JwtSecurityLayer.Payload;
public class ApiResponse { private Boolean success; private String message; public ApiResponse(Boolean success, String message) { this.success = success; this.message = message; } public Boolean getSuccess() { return success; } public void setSuccess(Boolean success) { this.success = success; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } |
Custom Business Exceptions
The APIs will throw exceptions if the request is not valid or some unexpected situation occurs.
We would also want to respond with different HTTP status codes for different types of exceptions.
Let’s define these exceptions along with the corresponding @ResponseStatus (All the exception classes will go inside a package named com.example.polls.exception)
1. AppException
package com.root.Generic.JwtSecurityLayer.Exceptions;
import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public class AppException extends RuntimeException { public AppException(String message) { super(message); } public AppException(String message, Throwable cause) { super(message, cause); } } |
2. BadRequestException
package com.root.Generic.JwtSecurityLayer.Exceptions;
import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.BAD_REQUEST) public class BadRequestException extends RuntimeException { public BadRequestException(String message) { super(message); } public BadRequestException(String message, Throwable cause) { super(message, cause); } } |
3. ResourceNotFoundException
package com.root.Generic.JwtSecurityLayer.Exceptions;
import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { private String resourceName; private String fieldName; private Object fieldValue; public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) { super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); this.resourceName = resourceName; this.fieldName = fieldName; this.fieldValue = fieldValue; } public String getResourceName() { return resourceName; } public String getFieldName() { return fieldName; } public Object getFieldValue() { return fieldValue; } } |
Enabling CORS
We’ll be accessing the APIs from the ReactJS or VueJS client that will run on its own development server. To allow cross origin requests from the clients, create the following WebMvcConfig class inside com.root.Generic.JwtSecurityLayer.Config
package com.root.Generic.JwtSecurityLayer.Config;
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final long MAX_AGE_SECS = 3600; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE") .maxAge(MAX_AGE_SECS); } } |
Authentication Controller
Finally, Here is the complete code for the AuthController that contains APIs for login and signup
package com.root.Generic.JwtSecurityLayer.Controllers;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; 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.PathVariable; 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 org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.validation.Valid; import com.root.Generic.JwtSecurityLayer.Exceptions.AppException; import com.root.Generic.JwtSecurityLayer.Models.Role; import com.root.Generic.JwtSecurityLayer.Models.RoleName; import com.root.Generic.JwtSecurityLayer.Models.User; import com.root.Generic.JwtSecurityLayer.Payload.ApiResponse; import com.root.Generic.JwtSecurityLayer.Payload.JwtAuthenticationResponse; import com.root.Generic.JwtSecurityLayer.Payload.LoginRequest; import com.root.Generic.JwtSecurityLayer.Payload.SignUpRequest; import com.root.Generic.JwtSecurityLayer.Repositories.RoleRepository; import com.root.Generic.JwtSecurityLayer.Repositories.UserRepository; import com.root.Generic.JwtSecurityLayer.Security.JwtTokenProvider; import java.net.URI; import java.util.Collections; /** * Created by rajeevkumarsingh on 02/08/17. */ @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired AuthenticationManager authenticationManager; @Autowired UserRepository userRepository; @Autowired RoleRepository roleRepository; @Autowired PasswordEncoder passwordEncoder; @Autowired JwtTokenProvider tokenProvider; @PostMapping("/signin") public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsernameOrEmail(), loginRequest.getPassword() )); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = tokenProvider.generateToken(authentication); return ResponseEntity.ok(new JwtAuthenticationResponse(jwt)); } // when using the endpoint if you are going to create an user in a localhost, just do it in this way // end-user: http://localhost:7078/api/auth/signup/user // admin: http://localhost:7078/api/auth/signup/admin // superadmin: http://localhost:7078/api/auth/signup/superadmin @PostMapping("/signup/{role}") public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest, @PathVariable("role") String role) { if (userRepository.existsByUsername(signUpRequest.getUsername())) { return new ResponseEntity(new ApiResponse(false, "Username is already taken!"), HttpStatus.BAD_REQUEST); } if (userRepository.existsByEmail(signUpRequest.getEmail())) { return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"), HttpStatus.BAD_REQUEST); } User user = new User(signUpRequest.getName(), signUpRequest.getUsername(), signUpRequest.getEmail(), signUpRequest.getPassword()); user.setPassword(passwordEncoder.encode(user.getPassword())); Role userRole; User result = null; if (role.equals("user")) { userRole = roleRepository.findByName(RoleName.ROLE_USER) .orElseThrow(() -> new AppException("User Role not set.")); user.setRoles(Collections.singleton(userRole)); result = userRepository.save(user); } if (role.equals("admin")) { userRole = roleRepository.findByName(RoleName.ROLE_ADMIN) .orElseThrow(() -> new AppException("User Role not set.")); user.setRoles(Collections.singleton(userRole)); result = userRepository.save(user); } if (role.equals("superadmin")) { userRole = roleRepository.findByName(RoleName.ROLE_SUPERADMIN) .orElseThrow(() -> new AppException("User Role not set.")); user.setRoles(Collections.singleton(userRole)); result = userRepository.save(user); } URI location = ServletUriComponentsBuilder.fromCurrentContextPath().path("/users/{username}") .buildAndExpand(result.getUsername()).toUri(); return ResponseEntity.created(location).body(new ApiResponse(true, "User ->"+role+"<- registered successfully")); } } |
If you have done the whole steps so far, so your folder and files structure must be like this:
You can run the application opening a terminal going to the path the app is and typing mvn spring-boot:run
Testing the Login and Signup APIs
Let’s now test the login and signup APIs with Postman.
SignUp user
SignUp-admin
SignUp superadmin
Login
Calling Protected APIs
Once you’ve obtained the access token using the login API, you can call any protected API by passing the accessToken in the Authorization header of the request like this
Authorization: Bearer <accessToken>
|
The JwtAuthenticationFilter will read the accessToken from the header, verify it, and allow/deny access to the API.
To test the protected APIs, we are going to create a new controller with 4 endpoints in order to show you who to protect the APIs with one or more than one role
Let’s go to the JwtSecurityLayer package and under Controllers and let’s create a new one called
TestController.java
TestController.java
package com.root.Generic.JwtSecurityLayer.Controllers;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.root.Generic.JwtSecurityLayer.Security.UserPrincipal; @RestController @RequestMapping(value = { "/test" }) public class TestController{ //--------------CODE FOR GETTING THE INFO DATA FROM THE CURRENT LOGED IN USER FROM THE CONTEXT Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String currentPrincipalName = ((UserPrincipal) authentication.getPrincipal()).getEmail(); @PreAuthorize("hasRole('SUPERADMIN') OR hasRole('USER')") @RequestMapping({ "/superadmin" }) public String methodOne() { System.out.println("this is the USER --> "+currentPrincipalName); return "Hello FROM SUPER ADMIN OR USER --> "+currentPrincipalName; } @PreAuthorize("hasRole('ADMIN')") @RequestMapping({ "/admin" }) public String methodTwo() { System.out.println("this is the USER --> "+currentPrincipalName); return "Hello FROM ADMIN --> "+currentPrincipalName; } @PreAuthorize("hasRole('USER')") @RequestMapping({ "/user" }) public String methodThird() { System.out.println("this is the USER --> "+currentPrincipalName); return "Hello FROM USER --> "+currentPrincipalName; } } |
So now Let’s test it, let’s login first as and end user and after that let’s use the endpoint which allows
getting the data if it has got the right authorization
getting the data if it has got the right authorization
So now that we have a login with an endUser let’s copy the token and let’s use it on the endpoint
So we now can the answer back from the controller, if you try to get that endpoint with an user who
belongs to another role won’t be possible.
belongs to another role won’t be possible.
The project is based half of it on
https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-1/
and https://memorynotfound.com/spring-security-method-level-annotations-example/
https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-1/
and https://memorynotfound.com/spring-security-method-level-annotations-example/
In the next posts we are going to be showing how to create a CRUD using Hibernate with JPA and
JdbcTemplate all of it connected with VueJS taking advantage of all the things we have done until now.
Comments
Post a Comment