Introduction
In this post we will see an example on Spring Security authentication and role based authorization using JWT (JSON Web Token) on REST or RESTful services. I won’t explain here about JWT as there is already very good article on JWT. We will implement Spring Security’s UserDetailsService
to load user from database.
We will use Spring JDBC API to perform database operations for fetching or saving user and roles into database. We will use H2 in-memory database to build a quick Spring Boot application. We will also use MySQL database to store the user information. We are going to use build tools gradle and maven to build our application.
Authentication and Authorization Flow
Here I will tell you how authentication and authorization work in this application that we are going to implement in with Spring Boot and JWT APIs in subsequent sections.
- User signup at end-point
/signup
with username, password and role(s). - The user information are stored into database.
- User signin at end-point
/signin
using the username and password, which user used at step 1. - User receives JWT (JSON Web Token) on successful signin.
- User continues to access the end-points for which user has role(s) as long as the token is valid. User must send JWT in HTTP header with key/value as
Authorization/Bearer <generated JWT on signin>
.
Note: The JWT is valid for 3 minutes, so token gets expired automatically after 3 minutes. Therefore it’s not mandatory to verify the token expiration but optionally you can check for expiration as you will see in this example.
Prerequisites
Eclipse 2019-12, Java at least 1.8, Gradle 6.1.1, Maven 3.6.3, Spring Boot 2.2.4, H2 1.4.196, MySQL 8.0.17
Creating Project
Create a gradle based project in Eclipse with Group Id: com.roytuts
and Artifact Id: spring-security-api-auth-jwt
.
Replace the default generated build.gradle file with below content to include the required dependencies.
buildscript {
ext {
springBootVersion = '2.2.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
plugins {
id 'java-library'
id 'org.springframework.boot' version '2.2.4.RELEASE'
}
sourceCompatibility = 12
targetCompatibility = 12
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-security:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
implementation("io.jsonwebtoken:jjwt:0.9.1")
implementation('javax.servlet:jstl:1.2')
implementation('mysql:mysql-connector-java:8.0.17')
//required only if jdk 9 or higher version is used
runtimeOnly('javax.xml.bind:jaxb-api:2.4.0-b180830.0359')
//runtime("com.h2database:h2:1.4.196")
}
If you are creating maven based project in Eclipse then you can use below pom.xml file:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.roytuts</groupId>
<artifactId>spring-security-api-auth-jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<!--required only if jdk 9 or higher version is used-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.4.0-b180830.0359</version>
<scope>runtime</scope>
</dependency>
<!--<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
<scope>runtime</scope>
</dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>at least 8</source>
<target>at least 8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Creating SQL Scripts
We will store user information – user details and user role details – into H2 database. Therefore we need to create tables using SQL scripts as we are using in-memory database.
If you are using MySQL database then you should execute the below table creation statements into MySQL server and you don’t need to create any SQL script under classpath directory.
Creating Table – user
We will store username, password and active flag in the below table.
Create a file user.sql under src/main/resources folder with below content.
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_name` varchar(30) NOT NULL,
`user_pass` varchar(255) NOT NULL,
`enable` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`user_name`)
);
Creating Table – user_role
We will store user role along with username as a foreign key that references to the above user table.
Create a file user-role.sql under src/main/resources folder with below content.
/*Table structure for table `user_role` */
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_name` varchar(30) NOT NULL,
`user_role` varchar(15) NOT NULL,
FOREIGN KEY (`user_name`) REFERENCES `user` (`user_name`)
);
Creating Database Config Class
We will define DataSource
and JdbcTemplate
beans to perform database operations.
We are using H2 in-memory database as well as MySQL as an external database for our application.
If you are using in-memory database then you don’t need to use username, password, database URL, database name because Spring Boot will use default values for these parameters.
But for MySQL database we need such configurations, therefore I have created two separate datasources for each type of database.
package com.roytuts.spring.security.api.auth.jwt.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
@Configuration
public class DatabaseConfig {
@Autowired
private Environment environment;
/*@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2) // .H2 or .DERBY, etc.
.addScript("user.sql").addScript("user-role.sql").build();
return db;
}*/
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(environment.getProperty("jdbc.driverClassName"));
dataSource.setUrl(environment.getProperty("jdbc.url"));
dataSource.setUsername(environment.getProperty("jdbc.username"));
dataSource.setPassword(environment.getProperty("jdbc.password"));
return dataSource;
}
@Bean
public JdbcTemplate getJdbcTemplate() throws ClassNotFoundException {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource());
return jdbcTemplate;
}
}
Creating Model Classes
We will create model classes to map our data to database table or vice versa.
Class – User
Class User
will map Java class to user table in database.
package com.roytuts.spring.security.api.auth.jwt.model;
public class User {
private String username;
private String userpwd;
//getters and setters
}
Class – Role
Class Role
maps to the user_role table in the database.
package com.roytuts.spring.security.api.auth.jwt.model;
public class Role {
private String role;
//getter and setter
}
Class – UserRole
This class carries user and role details for a particular user.
package com.roytuts.spring.security.api.auth.jwt.model;
import java.util.Set;
public class UserRole {
private String username;
private String userpwd;
private Set<String> roles;
//getters and setters
}
Creating RowMapper
RowMapper
class is required to map table’s row value to Java object.
The following class maps user table row data to User
class.
package com.roytuts.spring.security.api.auth.jwt.rowmapper;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
import com.roytuts.spring.security.api.auth.jwt.model.User;
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setUsername(rs.getString("user_name"));
user.setUserpwd(rs.getString("user_pass"));
return user;
}
}
Creating DAO Class
We will create DAO class to fetch or save user information.
We autowire JdbcTemplate
in the below DAO class.
We fetch a user detail for a given username using getUser()
method.
A user may have multiple roles, so we are fetching a list of roles for a given username using getRoles()
method. This method shows how to select multiple rows using Spring JDBC API.
When user signs up or register then we save user information into the table using saveUser()
method.
package com.roytuts.spring.security.api.auth.jwt.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.stereotype.Repository;
import com.roytuts.spring.security.api.auth.jwt.model.Role;
import com.roytuts.spring.security.api.auth.jwt.model.User;
import com.roytuts.spring.security.api.auth.jwt.model.UserRole;
import com.roytuts.spring.security.api.auth.jwt.rowmapper.UserRowMapper;
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public User getUser(String username) {
return jdbcTemplate.queryForObject("select user_name, user_pass from user where user_name = ?",
new Object[] { username }, new UserRowMapper());
}
public List<Role> getRoles(String username) {
List<Map<String, Object>> results = jdbcTemplate
.queryForList("select user_role from user_role where user_name = ?", new Object[] { username });
List<Role> roles = results.stream().map(m -> {
Role role = new Role();
role.setRole(String.valueOf(m.get("user_role")));
return role;
}).collect(Collectors.toList());
return roles;
}
public void saveUser(UserRole user) {
jdbcTemplate.update("insert into user(user_name, user_pass) values(?, ?)",
new Object[] { user.getUsername(), user.getUserpwd() });
user.getRoles().forEach(r -> jdbcTemplate.update(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement(
"insert into user_role(user_name, user_role) values(?, ?)",
new String[] { "user_name", "user_role" });
ps.setString(1, user.getUsername());
ps.setString(2, r);
return ps;
}
}));
}
}
Creating VO Classes
It is always good idea to create request/response objects separately instead of using entity or model classes for non-DAO classes.
Class – JwtRequest
This class is used as a RequestBody
for /signin
end-point for sending credentials for user authentication and generating token.
package com.roytuts.spring.security.api.auth.jwt.vo;
public class JwtRequest {
private String username;
private String userpwd;
// getters and setters
}
Class – JwtResponse
This class is used to wrap the generated token to send as a response to the client. You may simply send the generated token as a string.
package com.roytuts.spring.security.api.auth.jwt.vo;
public class JwtResponse {
private String token;
public JwtResponse(String token) {
this.token = token;
}
public String getToken() {
return token;
}
}
Class – UserVo
This class is used to hold user information, such as, username, password and roles.
package com.roytuts.spring.security.api.auth.jwt.vo;
import java.util.Set;
public class UserVo {
private String username;
private String userpwd;
private Set<String> roles;
// getters and setters
}
Related posts:
Creating Service Class
We will implement Spring Security’s UserDetailsService
to load user by username using loadUserByUsername()
method.
This service class is required to authenticate and role based authorization.
We have also added additional method saveUser()
to save new user’s information when a user signs up.
package com.roytuts.spring.security.api.auth.jwt.service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.roytuts.spring.security.api.auth.jwt.dao.UserDao;
import com.roytuts.spring.security.api.auth.jwt.model.Role;
import com.roytuts.spring.security.api.auth.jwt.model.User;
import com.roytuts.spring.security.api.auth.jwt.model.UserRole;
import com.roytuts.spring.security.api.auth.jwt.vo.UserVo;
@Service
public class UserAuthService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.getUser(username);
if (user == null) {
throw new UsernameNotFoundException("User '" + username + "' not found.");
}
List<Role> roles = userDao.getRoles(username);
List<GrantedAuthority> grantedAuthorities = roles.stream().map(r -> {
return new SimpleGrantedAuthority(r.getRole());
}).collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getUserpwd(),
grantedAuthorities);
}
public UserVo getUserByUsername(String username) {
User user = userDao.getUser(username);
if (user != null) {
List<Role> roles = userDao.getRoles(username);
Set<String> rls = roles.stream().map(r -> r.getRole()).collect(Collectors.toSet());
UserVo userVo = new UserVo();
userVo.setUsername(user.getUsername());
userVo.setUserpwd(user.getUserpwd());
userVo.setRoles(rls);
return userVo;
}
return null;
}
public void saveUser(UserVo userVo) {
UserRole user = new UserRole();
user.setUsername(userVo.getUsername());
user.setUserpwd(passwordEncoder.encode(userVo.getUserpwd()));
user.setRoles(userVo.getRoles());
userDao.saveUser(user);
}
}
Creating Config File
Create an application.properties file under src/main/resources folder with below content.
This file includes JWT secret key, JWT expiry time and exclusion for null fields in JSON response.
We have also put the MySQL database details in case you are using MySQL database for your application.
#datasource
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/roytuts
jdbc.username=root
jdbc.password=root
#secret key - should be encrypted
jwt.secret=secretkey
#3 minutes validity
jwt.token.validity=180000
#ignore null fields in json
spring.jackson.default-property-inclusion=NON_NULL
Creating Few Exception Classes
We will create our own exception class to wrap the message and throw it.
DisabledUserException
This exception is thrown when user is inactive.
package com.roytuts.spring.security.api.auth.jwt.exception;
public class DisabledUserException extends RuntimeException {
private static final long serialVersionUID = 1L;
public DisabledUserException(String msg) {
super(msg);
}
}
InvalidUserCredentialsException
This exception is thrown when user credentials are invalid.
package com.roytuts.spring.security.api.auth.jwt.exception;
public class InvalidUserCredentialsException extends RuntimeException {
private static final long serialVersionUID = 1L;
public InvalidUserCredentialsException(String msg) {
super(msg);
}
}
JwtTokenMalformedException
This exception is thrown when JWT is malformed or tempered in some way.
package com.roytuts.spring.security.api.auth.jwt.exception;
import org.springframework.security.core.AuthenticationException;
public class JwtTokenMalformedException extends AuthenticationException {
private static final long serialVersionUID = 1L;
public JwtTokenMalformedException(String msg) {
super(msg);
}
}
JwtTokenMissingException
This exception is thrown when JWT is not found in the HTTP header.
package com.roytuts.spring.security.api.auth.jwt.exception;
import org.springframework.security.core.AuthenticationException;
public class JwtTokenMissingException extends AuthenticationException {
private static final long serialVersionUID = 1L;
public JwtTokenMissingException(String msg) {
super(msg);
}
}
JWT (JSON Web Token)
Now we will see how to generate token using generateToken()
method. This method takes UserVo
object and generates token based on username and roles.
We will also see how to get the user information back from JWT using getUser()
method. This method takes generated JWT as a parameter.
We will create an optional method validateToken()
to validate token. This method takes generated JWT as a parameter and throws appropriate exception based on issues in token.
Why validateToken() is optional?
If you do not check the token expiry then also application will throw exception because it is automatically taken care by JWT API.
package com.roytuts.spring.security.api.auth.jwt.util;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.roytuts.spring.security.api.auth.jwt.exception.JwtTokenMalformedException;
import com.roytuts.spring.security.api.auth.jwt.exception.JwtTokenMissingException;
import com.roytuts.spring.security.api.auth.jwt.vo.UserVo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.token.validity}")
private long tokenValidity;
public UserVo getUser(final String token) {
try {
Claims body = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
UserVo user = new UserVo();
user.setUsername(body.getSubject());
Set<String> roles = Arrays.asList(body.get("roles").toString().split(",")).stream().map(r -> new String(r))
.collect(Collectors.toSet());
user.setRoles(roles);
return user;
} catch (Exception e) {
System.out.println(e.getMessage() + " => " + e);
}
return null;
}
public String generateToken(UserVo u) {
Claims claims = Jwts.claims().setSubject(u.getUsername());
claims.put("roles", u.getRoles());
long nowMillis = System.currentTimeMillis();
long expMillis = nowMillis + tokenValidity;
Date exp = new Date(expMillis);
return Jwts.builder().setClaims(claims).setIssuedAt(new Date(nowMillis)).setExpiration(exp)
.signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
}
public void validateToken(final String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
} catch (SignatureException ex) {
throw new JwtTokenMalformedException("Invalid JWT signature");
} catch (MalformedJwtException ex) {
throw new JwtTokenMalformedException("Invalid JWT token");
} catch (ExpiredJwtException ex) {
throw new JwtTokenMalformedException("Expired JWT token");
} catch (UnsupportedJwtException ex) {
throw new JwtTokenMalformedException("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
throw new JwtTokenMissingException("JWT claims string is empty.");
}
}
}
Now we will look into Security part of the application.
Authentication Entry Point
Instead of triggering the authentication process by redirecting to a login page when a client requests a secured resource, the REST server authenticates all requests using the data available in the request itself, the JWT token in this case. If such an authentication fails, redirection makes no sense. The REST API simply sends an HTTP code 401 Unauthorized
response and clients should know what to do.
This class just returns HTTP code 401 Unauthorized
when authentication fails, overriding default Spring’s redirecting.
package com.roytuts.spring.security.api.auth.jwt.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class ApiAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Extending OncePerRequestFilter
Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. As of Servlet 3.0, a filter may be invoked as part of a REQUEST
or ASYNC
dispatches that occur in separate threads. You may read more on OncePerRequestFilter
.
A common use case is in Spring Security, where authentication and access control functionality is typically implemented as a filter that sits in front of the main application Servlet. When a request is dispatched using a request dispatcher, it has to go through the filter chain again (or possibly a different one) before it gets to the Servlet that is going to deal with it. The problem is that some of the security filter actions should only be performed once for a request. Hence the need for OncePerRequestFilter
.
We must override the method doFilterInternal()
method in order to extend the functionalities of this filter.
We check for Authorization
with Bearer
token (JWT) in the HTTP header and if JWT presents then we extract it for further operations.
We optionally validate the token before extracting user information from this token.
Further we load user details from database and set authentication into SecurityContext
.
package com.roytuts.spring.security.api.auth.jwt.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.roytuts.spring.security.api.auth.jwt.exception.JwtTokenMissingException;
import com.roytuts.spring.security.api.auth.jwt.service.UserAuthService;
import com.roytuts.spring.security.api.auth.jwt.util.JwtUtil;
import com.roytuts.spring.security.api.auth.jwt.vo.UserVo;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserAuthService userAuthService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer")) {
throw new JwtTokenMissingException("No JWT token found in the request headers");
}
String token = header.substring(7);
// Optional - verification
jwtUtil.validateToken(token);
UserVo userVo = jwtUtil.getUser(token);
UserDetails userDetails = userAuthService.loadUserByUsername(userVo.getUsername());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
filterChain.doFilter(request, response);
}
}
Creating Security Config
Now it’s time to configure Spring Security part.
We have three main methods – configure(WebSecurity web)
, configure(AuthenticationManagerBuilder auth)
and configure(HttpSecurity http)
.
configure(HttpSecurity http)
We disable csrf
because it’s not a form based authentication. We disable security for endpoints – /signin
and /signup
. Because these end-points should accessed without any security or authentication.
All other end-points must go through authentication process and we have configured authenticationEntryPoint()
.
We do not store anything in the session so we have stateless session.
For authentication verification we have configured authentication filter using addFilterBefore()
.
configure(AuthenticationManagerBuilder auth)
This is required to configure database authentication and role based authorization using Spring JDBC on UserDetailsService
.
configure(WebSecurity web)
We had already excluded the /signin
and /signup
end-points in the configure(HttpSecurity http)
but it still requires in WebSecurity
.
RegistrationBean
Spring Boot automatically creates beans for us on the fly. As a result JwtAuthenticationFilter
class was being automatically put into the filter chain by Spring Boot, and also being included in the Security filter chain when it was declared it in Spring Security config.
Although /signin
and /signup
end-points were excluded in Spring Security config, that wasn’t enough to stop the filter from happening in the context of Spring Boot itself.
The solution was to configure a bean that explicitly prevents it from being added by Spring Boot.
package com.roytuts.spring.security.api.auth.jwt.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.RegistrationBean;
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.builders.WebSecurity;
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.roytuts.spring.security.api.auth.jwt.filter.JwtAuthenticationFilter;
import com.roytuts.spring.security.api.auth.jwt.service.UserAuthService;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserAuthService userAuthService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private ApiAuthenticationEntryPoint authenticationEntryPoint;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/signin", "/signup");
}
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userAuthService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/signin", "/signup").permitAll().anyRequest()
.authenticated().and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public RegistrationBean jwtAuthFilterRegister(JwtAuthenticationFilter filter) {
FilterRegistrationBean<JwtAuthenticationFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Creating REST End-Points
We will create two REST controller classes.
JwtRestController
This REST controller class exposes two end-points – /signup
that a user will use to register himself/herself and /signin
that a user will login using same credentials used during signup to get the JWT.
If user already exists in the database then we will show a message that user already exists.
package com.roytuts.spring.security.api.auth.jwt.rest.controller;
import java.util.Set;
import java.util.stream.Collectors;
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.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.roytuts.spring.security.api.auth.jwt.exception.DisabledUserException;
import com.roytuts.spring.security.api.auth.jwt.exception.InvalidUserCredentialsException;
import com.roytuts.spring.security.api.auth.jwt.service.UserAuthService;
import com.roytuts.spring.security.api.auth.jwt.util.JwtUtil;
import com.roytuts.spring.security.api.auth.jwt.vo.JwtRequest;
import com.roytuts.spring.security.api.auth.jwt.vo.JwtResponse;
import com.roytuts.spring.security.api.auth.jwt.vo.UserVo;
@RestController
public class JwtRestController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserAuthService userAuthService;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/signin")
public ResponseEntity<JwtResponse> generateJwtToken(@RequestBody JwtRequest jwtRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(jwtRequest.getUsername(), jwtRequest.getUserpwd()));
} catch (DisabledException e) {
throw new DisabledUserException("User Inactive");
} catch (BadCredentialsException e) {
throw new InvalidUserCredentialsException("Invalid Credentials");
}
UserDetails userDetails = userAuthService.loadUserByUsername(jwtRequest.getUsername());
String username = userDetails.getUsername();
String userpwd = userDetails.getPassword();
Set<String> roles = userDetails.getAuthorities().stream().map(r -> r.getAuthority())
.collect(Collectors.toSet());
UserVo user = new UserVo();
user.setUsername(username);
user.setUserpwd(userpwd);
user.setRoles(roles);
String token = jwtUtil.generateToken(user);
return new ResponseEntity<JwtResponse>(new JwtResponse(token), HttpStatus.OK);
}
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody UserVo userVo) {
UserVo u = userAuthService.getUserByUsername(userVo.getUsername());
if (u == null) {
userAuthService.saveUser(userVo);
return new ResponseEntity<String>("User successfully registered", HttpStatus.OK);
} else {
return new ResponseEntity<String>("User already exists", HttpStatus.CONFLICT);
}
}
}
ApiRestController
This REST controller class exposes few end-points based on user roles and if user has the appropriate role, then user will be able to access the corresponding end-point.
package com.roytuts.spring.security.api.auth.jwt.rest.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ApiRestController {
@PreAuthorize("hasRole('USER')")
@GetMapping("/greet/user")
public ResponseEntity<String> greetingUser() {
return new ResponseEntity<String>("Welcome, you have USER role", HttpStatus.OK);
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/greet/admin")
public ResponseEntity<String> greetingAdmin() {
return new ResponseEntity<String>("Welcome, you have ADMIN role", HttpStatus.OK);
}
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
@GetMapping("/greet/userOrAdmin")
public ResponseEntity<String> greetingUserOrAdmin() {
return new ResponseEntity<String>("Welcome, you have USER and ADMIN role", HttpStatus.OK);
}
@PreAuthorize("hasRole('ANONYMOUS')")
@GetMapping("/greet/anonymous")
public ResponseEntity<String> greetingAnonymous() {
return new ResponseEntity<String>("Welcome, you have USER and ADMIN role", HttpStatus.OK);
}
}
Creating Main Class
Creating a main would be sufficient to deploy our application into the embedded Tomcat server.
This is a great advantage that you just need to let Spring know that it is your Spring Boot Application using @SpringBootApplication
and main class.
package com.roytuts.spring.security.api.auth.jwt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringSecurityApiAuthJwtApp {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApiAuthJwtApp.class, args);
}
}
Testing the Application
Now if you run the above main class, your application will be deployed into embedded Tomcat server on port 8080
.
Now test your application using Postman tool or any REST client.
Signup
Method – POST
URL – http://localhost:8080/signup
Request Body
{
"username":"admin",
"userpwd":"admin",
"roles":["ROLE_USER", "ROLE_ADMIN"]
}
Response – User successfully registered
Signin
Method – POST
URL – http://localhost:8080/signin
Request Body
{
"username":"admin",
"userpwd":"admin"
}
Response
{
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwiaWF0IjoxNTY1MDg5OTM5LCJleHAiOjE1NjUwOTAxMTl9.jD3dla-3E2ivj2otU1xDaVk1y4pwBVIByve2TELS6dz9mU1CycnYtHwHkJ1gkYAYgZwfm_BZcvBoDuaHfUkCeg"
}
Accessing End-Points
Method – GET
URL – http://localhost:8080/greet/admin
Header
Key – Authorization
Value
Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwiaWF0IjoxNTY1MDg5OTM5LCJleHAiOjE1NjUwOTAxMTl9.jD3dla-3E2ivj2otU1xDaVk1y4pwBVIByve2TELS6dz9mU1CycnYtHwHkJ1gkYAYgZwfm_BZcvBoDuaHfUkCeg
Response – Welcome, you have ADMIN role
You can test for all end-points with different roles.
If your token expires then you will get Unauthorized
error.
Source Code
Thanks for reading.
For me, it is coming to JwtAuthenticationFilter :: doFilterInternal method which is OncePerRequest Filter
Inside doFilterInternal Method where we are hitting the database for each request.
Is this expected behavior or it should go into this filter while login only ?
This filter will be called for each request