Spring Boot JWT API
- GitHub: https://github.com/travisluong/fullstackbook-jwt-springboot
- YouTube: Full Stack Spring Boot + NextJS JWT Authentication Tutorial
Dependencies
- Lombok
- Spring Web
- Spring Security
- Spring Data JPA
- Flyway Migration
- PostgreSQL Driver
Config
src/main/java/com/example/fullstackbookjwtspringboot/config/CorsConfiguration.java
package com.example.fullstackbookjwtspringboot.config;
import org.springframework.context.annotation.Bean;
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 CorsConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "PUT", "POST", "DELETE").allowedOrigins("http://localhost:3000");
}
};
}
}
src/main/java/com/example/fullstackbookjwtspringboot/config/SecurityConfiguration.java
package com.example.fullstackbookjwtspringboot.config;
import com.example.fullstackbookjwtspringboot.filter.AuthTokenFilter;
import com.example.fullstackbookjwtspringboot.service.AuthEntryPointJwt;
import com.example.fullstackbookjwtspringboot.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
private UserDetailsServiceImpl userDetailsService;
private AuthEntryPointJwt authEntryPointJwt;
private AuthTokenFilter authTokenFilter;
public SecurityConfiguration(UserDetailsServiceImpl userDetailsService, AuthEntryPointJwt authEntryPointJwt, AuthTokenFilter authTokenFilter) {
this.userDetailsService = userDetailsService;
this.authEntryPointJwt = authEntryPointJwt;
this.authTokenFilter = authTokenFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
@Primary
public AuthenticationManagerBuilder configureAuthenticationManagerBuilder(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(authEntryPointJwt).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Controller
src/main/java/com/example/fullstackbookjwtspringboot/controller/AuthController.java
package com.example.fullstackbookjwtspringboot.controller;
import com.example.fullstackbookjwtspringboot.dto.JwtResponse;
import com.example.fullstackbookjwtspringboot.dto.SignInRequest;
import com.example.fullstackbookjwtspringboot.dto.SignUpRequest;
import com.example.fullstackbookjwtspringboot.model.ERole;
import com.example.fullstackbookjwtspringboot.model.Role;
import com.example.fullstackbookjwtspringboot.model.User;
import com.example.fullstackbookjwtspringboot.repository.RoleRepository;
import com.example.fullstackbookjwtspringboot.repository.UserRepository;
import com.example.fullstackbookjwtspringboot.service.UserDetailsImpl;
import com.example.fullstackbookjwtspringboot.util.JwtUtil;
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.*;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private UserRepository userRepository;
private RoleRepository roleRepository;
private PasswordEncoder passwordEncoder;
private AuthenticationManager authenticationManager;
private JwtUtil jwtUtil;
public AuthController(UserRepository userRepository,
PasswordEncoder passwordEncoder,
RoleRepository roleRepository,
AuthenticationManager authenticationManager,
JwtUtil jwtUtil) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
@PostMapping("/signin")
public ResponseEntity<?> signin(@RequestBody SignInRequest signInRequest) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtil.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
JwtResponse res = new JwtResponse();
res.setToken(jwt);
res.setId(userDetails.getId());
res.setUsername(userDetails.getUsername());
res.setRoles(roles);
return ResponseEntity.ok(res);
}
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody SignUpRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("username is already taken");
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("email is already taken");
}
String hashedPassword = passwordEncoder.encode(signUpRequest.getPassword());
Set<Role> roles = new HashSet<>();
Optional<Role> userRole = roleRepository.findByName(ERole.ROLE_USER);
if (userRole.isEmpty()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("role not found");
}
roles.add(userRole.get());
User user = new User();
user.setUsername(signUpRequest.getUsername());
user.setEmail(signUpRequest.getEmail());
user.setPassword(hashedPassword);
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok("User registered success");
}
}
src/main/java/com/example/fullstackbookjwtspringboot/controller/TestController.java
package com.example.fullstackbookjwtspringboot.controller;
import com.example.fullstackbookjwtspringboot.service.UserDetailsImpl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import org.apache.coyote.Response;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
@Log4j2
public class TestController {
@GetMapping("/all")
public String allAccess() {
return "Public Content.";
}
@GetMapping("/user")
@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
public String userAccess() {
return "User Content.";
}
@GetMapping("/mod")
@PreAuthorize("hasRole('MODERATOR')")
public String moderatorAccess() {
return "Moderator Board.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board.";
}
@GetMapping("/profile")
@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
public UserDetailsImpl profile() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
log.info("username: {}", userDetails.getUsername());
return userDetails;
}
}
DTO
src/main/java/com/example/fullstackbookjwtspringboot/dto/JwtResponse.java
package com.example.fullstackbookjwtspringboot.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
public class JwtResponse {
private String token;
private String type = "Bearer";
private Long id;
private String username;
private List<String> roles;
}
src/main/java/com/example/fullstackbookjwtspringboot/dto/SignInRequest.java
package com.example.fullstackbookjwtspringboot.dto;
import lombok.Data;
@Data
public class SignInRequest {
private String username;
private String password;
}
src/main/java/com/example/fullstackbookjwtspringboot/dto/SignUpRequest.java
package com.example.fullstackbookjwtspringboot.dto;
import lombok.Data;
@Data
public class SignUpRequest {
private String username;
private String email;
private String password;
}
Filter
src/main/java/com/example/fullstackbookjwtspringboot/filter/AuthTokenFilter.java
package com.example.fullstackbookjwtspringboot.filter;
import com.example.fullstackbookjwtspringboot.service.UserDetailsServiceImpl;
import com.example.fullstackbookjwtspringboot.util.JwtUtil;
import lombok.extern.log4j.Log4j2;
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.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;
@Log4j2
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
private JwtUtil jwtUtil;
private UserDetailsServiceImpl userDetailsService;
public AuthTokenFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtil.validateJwtToken(jwt)) {
String username = jwtUtil.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
log.error("Cannot set user 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);
}
return null;
}
}
src/main/java/com/example/fullstackbookjwtspringboot/filter/TestFilter.java
package com.example.fullstackbookjwtspringboot.filter;
import org.springframework.stereotype.Component;
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;
@Component
public class TestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenHeader = request.getHeader("Authorization");
System.out.println("tokenHeader = " + tokenHeader);
filterChain.doFilter(request, response);
}
}
Model
src/main/java/com/example/fullstackbookjwtspringboot/model/ERole.java
package com.example.fullstackbookjwtspringboot.model;
public enum ERole {
ROLE_USER,
ROLE_ADMIN,
ROLE_MODERATOR
}
src/main/java/com/example/fullstackbookjwtspringboot/model/Role.java
package com.example.fullstackbookjwtspringboot.model;
import javax.persistence.*;
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column
private ERole name;
public ERole getName() {
return name;
}
}
src/main/java/com/example/fullstackbookjwtspringboot/model/User.java
package com.example.fullstackbookjwtspringboot.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String username;
@Column
private String email;
@Column
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_to_roles",
joinColumns = @JoinColumn(name="user_id"),
inverseJoinColumns = @JoinColumn(name="role_id"))
private Set<Role> roles = new HashSet<>();
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
Repository
src/main/java/com/example/fullstackbookjwtspringboot/repository/RoleRepository.java
package com.example.fullstackbookjwtspringboot.repository;
import com.example.fullstackbookjwtspringboot.model.ERole;
import com.example.fullstackbookjwtspringboot.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(ERole name);
}
src/main/java/com/example/fullstackbookjwtspringboot/repository/UserRepository.java
package com.example.fullstackbookjwtspringboot.repository;
import com.example.fullstackbookjwtspringboot.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}
Service
src/main/java/com/example/fullstackbookjwtspringboot/service/AuthEntryPointJwt.java
package com.example.fullstackbookjwtspringboot.service;
import lombok.extern.log4j.Log4j2;
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;
@Log4j2
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
src/main/java/com/example/fullstackbookjwtspringboot/service/UserDetailsImpl.java
package com.example.fullstackbookjwtspringboot.service;
import com.example.fullstackbookjwtspringboot.model.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.stream.Collectors;
public class UserDetailsImpl implements UserDetails {
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@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;
}
public Long getId() {
return id;
}
}
src/main/java/com/example/fullstackbookjwtspringboot/service/UserDetailsServiceImpl.java
package com.example.fullstackbookjwtspringboot.service;
import com.example.fullstackbookjwtspringboot.model.User;
import com.example.fullstackbookjwtspringboot.repository.UserRepository;
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;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found with username" + username));
return UserDetailsImpl.build(user);
}
}
Util
src/main/java/com/example/fullstackbookjwtspringboot/util/JwtUtil.java
package com.example.fullstackbookjwtspringboot.util;
import com.example.fullstackbookjwtspringboot.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@Log4j2
public class JwtUtil {
@Value("${fullstackbook.app.jwtSecret}")
private String jwtSecret;
@Value("${fullstackbook.app.jwtExpirationMs}")
private int jwtExpirationMs;
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder().setSubject((userPrincipal.getUsername())).setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)).signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}
App
src/main/java/com/example/fullstackbookjwtspringboot/FullstackbookJwtSpringbootApplication.java
package com.example.fullstackbookjwtspringboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FullstackbookJwtSpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(FullstackbookJwtSpringbootApplication.class, args);
}
}
Migration
src/main/resources/db/migration/V1__create_roles_table.sql
create table roles (
id serial primary key,
name text not null unique
);
src/main/resources/db/migration/V2__create_users_table.sql
create table users (
id serial primary key,
username text not null,
email text not null,
password text
);
src/main/resources/db/migration/V3__create_users_to_roles_table.sql
create table users_to_roles (
user_id int references users (id),
role_id int references roles (id)
);
src/main/resources/db/migration/V4__insert_roles.sql
insert into roles (name) values ('ROLE_USER');
insert into roles (name) values ('ROLE_MODERATOR');
insert into roles (name) values ('ROLE_ADMIN');
Properties
src/main/resources/application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/fullstackbook-jwt-springboot
spring.datasource.username=postgres
spring.datasource.password=
#spring.security.user.name=foo
#spring.security.user.password=bar
fullstackbook.app.jwtSecret=mysecret
fullstackbook.app.jwtExpirationMs=86400000