Spring Boot + Spring Security + JWT Authentication and Authorization Example
Hello everyone, Hope you are doing well, Today we will learn how to handle authentication and authorization on RESTful APIs written with Spring Boot. The GitHub repository link is provided at the end of this tutorial. You can download the source code.
A little bit of Background
Spring Boot
Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can "just run". More Info - https://spring.io/projects/spring-boot
Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can "just run".
More Info - https://spring.io/projects/spring-boot
Spring Security
Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first-class support for securing both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first-class support for securing both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.
JSON Web Token(JWT)
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.More Info - https://jwt.io/introduction
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
More Info - https://jwt.io/introduction
MongoDB
MongoDB is a document database designed for ease of application development and scaling.More Info - https://www.mongodb.com/docs/manual/
MongoDB is a document database designed for ease of application development and scaling.
More Info - https://www.mongodb.com/docs/manual/
Token-based Authentication system flow diagram
Final Project Directory
Maven[pom.xml]
Puts spring-boot-starter-data-mongodb, spring-boot-starter-web, spring-boot-starter-security, jjwt dependencies.
<?xml version="1.0" encoding="UTF-8"?>
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev</groupId>
<artifactId>spring-boot-security-jwt-mongodb</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-security-jwt-mongodb</name>
<description>Demo project for Spring Boot </description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</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>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</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>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yaml
knf:
app:
jwtExpirationMs: 76300000
jwtSecret: knowledgeFactory
spring:
data:
mongodb:
database: knfDemoDb
host: localhost
port: 27017
Creat Employee Document
@Document(collection = "employees")
public class Employee {
@Id
private String id;
private String employeename;
private String email;
private String password;
@DBRef
private Set<Role> roles = new HashSet<>();
public Employee() {
}
public Employee(String employeename,
String email, String password) {
super();
this.employeename = employeename;
this.email = email;
this.password = password;
}
public String getEmployeename() {
return employeename;
}
public void setEmployeename(String employeename) {
this.employeename = employeename;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
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;
}
}
- @Id annotation is currently used by Spring to support mapping for other non-relational persistence databases or frameworks that do not have a defined common persistence API like JPA.
- @Document is an annotation provided by the Spring Data project. It is used to identify a domain object, which is persisted to MongoDB. So you can use it to map a Java class into a collection inside MongoDB.
Create Role Document
@Document(collection = "roles")
public class Role {
@Id
private String id;
@Indexed(unique = true)
private ERole name;
public Role() {
}
public Role(ERole name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public ERole getName() {
return name;
}
public void setName(ERole name) {
this.name = name;
}
}
Create Enum ERole
public enum ERole {
ROLE_EMPLOYEE, ROLE_ADMIN
}
Create Employee Repository
public interface EmployeeRepository
extends MongoRepository<Employee, String> {
Optional<Employee> findByEmployeename(String employeename);
Boolean existsByEmployeename(String employeename);
Boolean existsByEmail(String email);
}
MongoRepository is just a specialized PagingAndSortingRepository suited to Mongo, which in turn is a specialized CrudRepository.
Create Role Repository
public interface RoleRepository
extends MongoRepository<Role, String> {
Optional<Role> findByName(ERole name);
}
Create Service EmployeeDetailsImpl
public class EmployeeDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private String id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public EmployeeDetailsImpl(String 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 EmployeeDetailsImpl build(Employee user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority
(role.getName().name()))
.collect(Collectors.toList());
return new EmployeeDetailsImpl(user.getId(),
user.getEmployeename(), user.getEmail(),
user.getPassword(),authorities);
}
@Override
public Collection<? extends
GrantedAuthority> getAuthorities() {
return authorities;
}
public String 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;
EmployeeDetailsImpl user = (EmployeeDetailsImpl) o;
return Objects.equals(id, user.id);
}
}
Create Service EmployeeDetailsServiceImpl
@Service
public class EmployeeDetailsServiceImpl
implements UserDetailsService {
@Autowired
EmployeeRepository employeeRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String employeename)
throws UsernameNotFoundException {
Employee employee = employeeRepository
.findByEmployeename(employeename)
.orElseThrow(() -> new UsernameNotFoundException
("Employee Not Found with username: "
+ employeename));
return EmployeeDetailsImpl.build(employee);
}
}
Create Jwt Utility
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory
.getLogger(JwtUtils.class);
@Value("${knf.app.jwtExpirationMs}")
private int jwtExpirationMs;
@Value("${knf.app.jwtSecret}")
private String jwtSecret;
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret)
.parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} 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 generateJwtToken(Authentication authentication) {
EmployeeDetailsImpl employeePrincipal =
(EmployeeDetailsImpl) authentication.getPrincipal();
return Jwts.builder().setSubject((employeePrincipal
.getUsername())).setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime()
+ jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
}
public String getEmployeeNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret)
.parseClaimsJws(token).getBody().getSubject();
}
}
Create AuthTokenFilter
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private EmployeeDetailsServiceImpl employeeDetailsService;
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 employeename = jwtUtils
.getEmployeeNameFromJwtToken(jwt);
UserDetails employeeDetails = employeeDetailsService
.loadUserByUsername(employeename);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
employeeDetails, null,
employeeDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set employee authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth)
&& headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
Create AuthEntryPointJwt
@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");
}
}
Create Web Security Configuration
WebSecurityConfigurerAdapter is deprecated, So going forward, the recommended way of doing this is registering a SecurityFilterChain bean like below.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Autowired
EmployeeDetailsServiceImpl employeeDetailsService;
@Bean
public SecurityFilterChain filterChain(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);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
@Bean
public AuthenticationManager authenticationManager
(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration
.getAuthenticationManager();
}
}
Create Login Request
public class LoginRequest {
private String employeename;
private String password;
public String getEmployeename() {
return employeename;
}
public void setEmployeename(String employeename) {
this.employeename = employeename;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Create Signup Request
public class SignupRequest {
private String employeename;
private String email;
private Set<String> roles;
private String password;
public String getEmployeename() {
return employeename;
}
public void setEmployeename(String employeename) {
this.employeename = employeename;
}
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<String> getRoles() {
return this.roles;
}
public void setRole(Set<String> roles) {
this.roles = roles;
}
}
Create Jwt Response
public class JwtResponse {
private String token;
private String type = "Bearer";
private String id;
private String employeename;
private String email;
private List<String> roles;
public JwtResponse(String accessToken,
String id, String employeename,
String email, List<String> roles) {
this.token = accessToken;
this.id = id;
this.employeename = employeename;
this.email = email;
this.roles = roles;
}
public String getAccessToken() {
return token;
}
public void setAccessToken(String accessToken) {
this.token = accessToken;
}
public String getTokenType() {
return type;
}
public void setTokenType(String tokenType) {
this.type = tokenType;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getEmployeename() {
return employeename;
}
public void setEmployeename(String employeename) {
this.employeename = employeename;
}
public List<String> getRoles() {
return roles;
}
}
Create Message Response
public class MessageResponse {
private String message;
public MessageResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Create Auth Controller
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
EmployeeRepository employeeRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@PostMapping("/signin")
public ResponseEntity<?> authenticateEmployee
(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken
(loginRequest.getEmployeename(),
loginRequest.getPassword()));
SecurityContextHolder.getContext()
.setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
EmployeeDetailsImpl employeeDetails = (EmployeeDetailsImpl)
authentication.getPrincipal();
List<String> roles = employeeDetails.getAuthorities()
.stream().map(item -> item.getAuthority())
.collect(Collectors.toList());
return ResponseEntity.ok(new JwtResponse(jwt,
employeeDetails.getId(),
employeeDetails.getUsername(),
employeeDetails.getEmail(), roles));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser
(@RequestBody SignupRequest signUpRequest) {
if (employeeRepository.existsByEmployeename
(signUpRequest.getEmployeename())) {
return ResponseEntity.badRequest()
.body(new MessageResponse
("Error: Employeename is already taken!"));
}
if (employeeRepository
.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new MessageResponse
("Error: Email is already in use!"));
}
// Create new employee account
Employee employee = new Employee(signUpRequest
.getEmployeename(), signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRoles();
Set<Role> roles = new HashSet<>();
if (strRoles == null) {
Role employeeRole = roleRepository
.findByName(ERole.ROLE_EMPLOYEE)
.orElseThrow(() -> new RuntimeException
("Error: Role is not found."));
roles.add(employeeRole);
} 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;
default:
Role defaultRole = roleRepository
.findByName(ERole.ROLE_EMPLOYEE)
.orElseThrow(() -> new RuntimeException
("Error: Role is not found."));
roles.add(defaultRole);
}
});
}
employee.setRoles(roles);
employeeRepository.save(employee);
return ResponseEntity.ok(new MessageResponse
("Employee registered successfully!"));
}
}
Create Employee Controller
@CrossOrigin(origins = "*", maxAge = 4800)
@RestController
@RequestMapping("/api/test")
public class EmployeeController {
@GetMapping("/all")
public MessageResponse allAccess() {
return new MessageResponse("Public ");
}
@GetMapping("/employee")
@PreAuthorize("hasRole('EMPLOYEE') ")
public MessageResponse employeeAccess() {
return new MessageResponse("Employee zone");
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public MessageResponse adminAccess() {
return new MessageResponse("Admin zone");
}
}
Spring Boot Main Driver
@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
RoleRepository roleRepository;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
/* Add some rows into roles collection
* before assigning any role to Employee. */
@Override
public void run(String... args) throws Exception {
try {
if (roleRepository.findAll().isEmpty()) {
Role role = new Role();
role.setName(ERole.ROLE_EMPLOYEE);
roleRepository.save(role);
Role role2 = new Role();
role2.setName(ERole.ROLE_ADMIN);
roleRepository.save(role2);
} else {
}
} catch (Exception e) {
}
}
}
Local Setup and Run the application
Step 1: Download or clone the source code from GitHub to the local machine - Click here
Step 2: mvn clean install
Step 3: Run the Spring Boot application -
mvn spring-boot:run or Run as Spring Boot application.
Step 1: Download or clone the source code from GitHub to the local machine - Click here
Step 2: mvn clean install
Step 3: Run the Spring Boot application -
mvn spring-boot:run or Run as Spring Boot application.
Register employee
http://localhost:8080/api/auth/signup
Employee Sign in to an account
http://localhost:8080/api/auth/signin
Using accessToken access ROLE_EMPLOYEE resource
http://localhost:8080/api/test/employee
Register Admin
http://localhost:8080/api/auth/signup
Admin Sign in to an account
http://localhost:8080/api/auth/signin
http://localhost:8080/api/test/admin
More related topics,
More related topics,