Overview
Security config is where CORS, CSRF, and session management are configured. It autowires:
UserDetailsService— loads a user by username and returns aUserDetailsobject usable for authentication (usually your own implementation).AuthenticationEntryPoint— catches authentication errors (usually your own implementation).
Then it defines these beans: AuthTokenFilter, DaoAuthenticationProvider,
AuthenticationManager (wraps the DaoAuthenticationProvider, using UserDetailsService and
PasswordEncoder to validate a UsernamePasswordAuthenticationToken), PasswordEncoder, and
SecurityFilterChain.
OncePerRequestFilter executes once per request. Its doFilterInternal() method is where you
parse and validate the JWT, load user details via UserDetailsService, and check authorization
via UsernamePasswordAuthenticationToken.
Implementation
1. Create User, Role, and ERole models — ERole is an enum of the roles a user can
have, linked to the name field on Role.
2. Implement repositories:
UserRepository—Optional<User> findByUsername,Boolean existsByUsername,Boolean existsByEmailRoleRepository—Optional<Role> findByName(ERole name)
3. Configure Spring Security (WebSecurityConfig):
- Autowire your
UserDetailsServiceimplementation andAuthEntryPointJwt. - Register beans:
AuthTokenFilter(authenticationJwtTokenFilter()),DaoAuthenticationProvider(wired to theUserDetailsServiceandPasswordEncoder),AuthenticationManager(fromAuthenticationConfiguration),PasswordEncoder, and aSecurityFilterChainthat disables CSRF, sets the exception handler, makes sessions stateless, permits/api/auth/**and/api/test/**, requires auth for everything else, registers the authentication provider, and adds the JWT filter beforeUsernamePasswordAuthenticationFilter.
@Configuration@EnableMethodSecuritypublic class WebSecurityConfig { @Autowired UserDetailsServiceImpl userDetailsService;
@Autowired private AuthEntryPointJwt unauthorizedHandler;
@Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); }
@Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder());
return authProvider; }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/test/**").permitAll() .anyRequest().authenticated() );
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build(); }}4. Implement UserDetails and UserDetailsService. After a successful authentication, you
can pull the user’s info off the Authentication object:
Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password) );
UserDetails userDetails = (UserDetails) authentication.getPrincipal();UserDetails implementation — note the conversion from the user’s roles into
List<GrantedAuthority>, which is what Spring Security’s Authentication object works with:
public class UserDetailsImpl implements UserDetails { private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) { this.id = id; this.username = username; this.email = email; this.password = password; this.authorities = authorities; }
public static UserDetailsImpl build(User user) { List<GrantedAuthority> authorities = user.getRoles().stream() .map(role -> new SimpleGrantedAuthority(role.getName().name())) .collect(Collectors.toList());
return new UserDetailsImpl( user.getId(), user.getUsername(), user.getEmail(), user.getPassword(), authorities); }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
public Long getId() { return id; }
public String getEmail() { return email; }
@Override public String getPassword() { return password; }
@Override public String getUsername() { return username; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserDetailsImpl user = (UserDetailsImpl) o; return Objects.equals(id, user.id); }}@Servicepublic class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserRepository userRepository;
@Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(user); }}5. Filter the requests. AuthTokenFilter runs once per request, overriding
doFilterInternal():
public class AuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils;
@Autowired private UserDetailsServiceImpl userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("Cannot set user authentication: {}", e); }
filterChain.doFilter(request, response); }
private String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7); }
return null; }}Inside doFilterInternal(): get the JWT from the Authorization header (stripping the Bearer
prefix); if present, validate it and parse out the username; load UserDetails for that username
to build an Authentication object; set it in the SecurityContext. From then on, anywhere you
need the current user:
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();6. JWT utility class — generates a token, extracts the username, and validates a token:
@Componentpublic class JwtUtils { private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${jwtSecret}") private String jwtSecret;
@Value("${jwtExpirationInMilliSeconds}") 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(key(), SignatureAlgorithm.HS256) .compact(); }
private Key key() { return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); }
public String getUserNameFromJwtToken(String token) { return Jwts.parserBuilder().setSigningKey(key()).build() .parseClaimsJws(token).getBody().getSubject(); }
public boolean validateJwtToken(String authToken) { try { Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); return true; } catch (MalformedJwtException e) { logger.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { logger.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { logger.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { logger.error("JWT claims string is empty: {}", e.getMessage()); }
return false; }}7. Handle authentication exceptions with AuthEntryPointJwt, triggered whenever an
unauthenticated user requests a secured resource:
@Componentpublic class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { logger.error("Unauthorized error: {}", authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); }}HttpServletResponse.SC_UNAUTHORIZED is the 401 status code. To customize the response body, use
an ObjectMapper:
@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { logger.error("Unauthorized error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map<String, Object> body = new HashMap<>(); body.put("status", HttpServletResponse.SC_UNAUTHORIZED); body.put("error", "Unauthorized"); body.put("message", authException.getMessage()); body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), body);}8. Define request/response payloads and controllers:
- Requests:
LoginRequest {username, password},SignUpRequest {username, email, password} - Responses:
JwtResponse {token, type, id, username, email, roles},MessageResponse {message}
The auth controller provides /api/auth/signup (checks existing username/email, creates a new
user with ROLE_USER by default, saves it) and /api/auth/signin (authenticates, updates the
SecurityContext, generates a JWT, and returns it with the user’s details):
@CrossOrigin(origins = "*", maxAge = 3600)@RestController@RequestMapping("/api/auth")public class AuthController { @Autowired AuthenticationManager authenticationManager;
@Autowired UserRepository userRepository;
@Autowired RoleRepository roleRepository;
@Autowired PasswordEncoder encoder;
@Autowired JwtUtils jwtUtils;
@PostMapping("/signin") public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); List<String> roles = userDetails.getAuthorities().stream() .map(item -> item.getAuthority()) .collect(Collectors.toList());
return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), roles)); }
@PostMapping("/signup") public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) { if (userRepository.existsByUsername(signUpRequest.getUsername())) { return ResponseEntity .badRequest() .body(new MessageResponse("Error: Username is already taken!")); }
if (userRepository.existsByEmail(signUpRequest.getEmail())) { return ResponseEntity .badRequest() .body(new MessageResponse("Error: Email is already in use!")); }
// Create new user's account User user = new User(signUpRequest.getUsername(), signUpRequest.getEmail(), encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRole(); Set<Role> roles = new HashSet<>();
if (strRoles == null) { Role userRole = roleRepository.findByName(ERole.ROLE_USER) .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); roles.add(userRole); } else { strRoles.forEach(role -> { switch (role) { case "admin": Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN) .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); roles.add(adminRole);
break; case "mod": Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR) .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); roles.add(modRole);
break; default: Role userRole = roleRepository.findByName(ERole.ROLE_USER) .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); roles.add(userRole); } }); }
user.setRoles(roles); userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!")); }}