Welcome to the backend security documentation! If you have ZERO knowledge about how backend security works, you are in the right place.
We will learn exactly how this Java Spring Boot backend protects your data, verifies who is logging in, and how the config and user/service folders work together to make it happen.
Imagine an exclusive VIP Club:
- The Registration Desk (
UserService): You give them your details and password. They encrypt your password so nobody can read it, and save you in the database. - The ID Card Maker (
JwtUtil): When you log in with the correct password, they hand you an unbreakable, cryptographic ID Card (a JSON Web Token, or JWT). - The Bouncer (
JwtAuthenticationFilter): Every time you try to enter a restricted room (make an API request), the Bouncer stops you, checks your ID Card (JWT), and lets you in if it's valid. - The Club Manager (
SecurityConfig): The person who writes the rules for the Bouncer (e.g., "The lobby is free for everyone, but the VIP rooms require an ID Card"). - The Database Looker (
CustomUserDetailsService): Sometimes the Bouncer needs to check the master guest list to ensure the ID Card matches a real person. This service does exactly that.
Let's look at the code line-by-line!
This folder contains all the rules and tools for our security bouncers.
This file is the "Club Manager". It tells Spring Security exactly what rules to enforce globally.
14: @Configuration
15: public class SecurityConfig {- Line 14:
@Configurationtells Spring Boot: "Hey, read this file when you start up, it has important settings inside!"
23: @Bean
24: public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {- Line 24: A
SecurityFilterChainis literally the chain of rules that every single HTTP request must pass through.
30: .cors(Customizer.withDefaults())
31: .csrf(csrf -> csrf.disable())- Line 30:
corsallows our React Frontend (running on a different port) to talk to this Java Backend. - Line 31:
csrf(Cross-Site Request Forgery) protection is disabled because we are using JWT tokens instead of browser cookies.
32: .authorizeHttpRequests(auth -> auth
33: .requestMatchers("/api/users/register", "/api/users/login").permitAll()
34: .anyRequest().authenticated()
35: )- Line 33:
permitAll()means these paths are wide open. Anyone can try to register or log in (the "Lobby"). - Line 34:
anyRequest().authenticated()specifies that EVERY OTHER URL in the entire app requires a valid ID Card (the "VIP Rooms").
36: .sessionManagement(session ->
37: session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
38: )- Line 37:
STATELESSmeans the backend has amnesia. It doesn't remember who you are between requests. You MUST show your JWT ID card on every single request.
44: @Bean
45: public PasswordEncoder passwordEncoder() {
46: return new BCryptPasswordEncoder();
47: }- Line 46: This tells the system how to scramble passwords. BCrypt is an incredibly strong hashing algorithm. If a user's password is
123456, BCrypt turns it into a random string like$2a$10$BLRsPrkV...before saving it to the database.
This is the "ID Card Maker". It generates and verifies JWTs (JSON Web Tokens).
13: private static final String SECRET = "mysecretkeymysecretkeymysecretkey123";
14: private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1 hour- Line 13: The
SECRETis the highly confidential stamp the server uses to sign the tokens. If a hacker tries to forge a token, they can't sign it correctly without this exact secret. - Line 14: The token is only valid for 1 hour. After that, the user must log in again.
18: public static String generateToken(String email) {
19: return Jwts.builder()
20: .setSubject(email)
21: .setIssuedAt(new Date())
22: .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
23: .signWith(key, SignatureAlgorithm.HS256)
24: .compact();
25: }- Lines 18-25: This function creates the ID card. It writes the user's
emailon it (setSubject), writes the exact time it was created (setIssuedAt), writes the expiration date 1 hour from now (setExpiration), and then mathematically seals it using theSECRET(signWith).
This is "The Bouncer". This file intercepts every single request that comes into the API.
26: protected void doFilterInternal(HttpServletRequest request,
27: HttpServletResponse response,
28: FilterChain filterChain)- Line 26:
doFilterInternalis the bouncer's checkpoint.
31: final String authHeader = request.getHeader("Authorization");
36: if (authHeader != null && authHeader.startsWith("Bearer ")) {
37: token = authHeader.substring(7);
38: try {
39: email = JwtUtil.extractEmail(token);- Line 31 & 36: First, the bouncer checks the headers of the incoming request. Is the user holding up an ID card? (Does it start with
Bearer?) - Line 37-39: If yes, the bouncer strips away the word "Bearer " to get the raw token, and uses
JwtUtilto read the email address written on it.
45: if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
47: UserDetails userDetails = userDetailsService.loadUserByUsername(email);
49: if (JwtUtil.validateToken(token, userDetails.getUsername())) {
62: SecurityContextHolder.getContext().setAuthentication(authToken);- Lines 45-62: The bouncer says: "Okay, the ID card says your email is X". It then uses
userDetailsServiceto look in the database to see if User X actually exists. - If the user exists and the token's digital signature is valid (
validateToken), the bouncer officially records the user in theSecurityContext(letting them into the club).
Why do we have two User Service files?
UserService: Is your custom business logic (Registering, Logging in, Updating Profiles).CustomUserDetailsService: Is a forced requirement by Spring Security. Spring doesn't know what database you are using, so it forces you to implement an Interface so it knows how to "find" a user.
The "Database Looker". Spring Security uses this solely to verify a user's existence during the Bouncer check.
11: public class CustomUserDetailsService implements UserDetailsService {- Line 11:
implements UserDetailsServiceis a contract with Spring Security. Spring says: "If you want me to do security, you MUST provide a class with this exact name and interface!"
20: public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
22: User user = userRepository.findByEmail(email)
23: .orElseThrow(() -> new UsernameNotFoundException("User not found"));
25: return new CustomUserDetails(user);
26: }- Line 22: We use the
UserRepository(which talks to MySQL) to find the user by their email. - Line 23: If they don't exist in the database, we throw an error (The Bouncer kicks them out).
- Line 25: We wrap our Database
Userobject into aCustomUserDetailsobject that Spring Security can understand.
The "Translator". Spring Security has very specific expectations. It doesn't know what our User object looks like. It only understands a specific interface called UserDetails. This class translates our custom User into Spring Security's language.
12: public class CustomUserDetails implements UserDetails {
14: private final User user;- Line 12: We tell Spring, "Treat this exact class like the VIP ID Profile you are expecting".
- Line 14: We store our actual, custom
Userobject (pulled from the database) inside this wrapper.
20: @Override
21: public Collection<? extends GrantedAuthority> getAuthorities() {
22: return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
23: }- Line 21: The Bouncer needs to know what permissions this user holds.
- Line 22: We get the role from our database (e.g.,
STUDENTorADMIN), slap"ROLE_"onto the front of it (Spring requires this prefix), and hand it directly to Spring Security.
26: public String getPassword() { return user.getPassword(); }
31: public String getUsername() { return user.getEmail(); }- Lines 26 & 31: Spring explicitly asks for a
Passwordand aUsername. We translate that by simply returning ourUser's password and email.
36: public boolean isAccountNonExpired() { return true; }
41: public boolean isAccountNonLocked() { return true; }
46: public boolean isCredentialsNonExpired() { return true; }
51: public boolean isEnabled() { return true; }- Lines 36-51: Spring Security has advanced features allowing you to temporarily lock or disable accounts. Since we aren't using those advanced features yet, we hardcode them all to return
trueso nobody gets locked out accidentally!
The custom "Registration Desk". This holds the logic for our actual User endpoints (like /api/users/register).
27: public User register(RegisterRequest request) {
29: if (userRepository.findByEmail(request.getEmail()).isPresent()) {
30: throw new RuntimeException("Email already exists");
31: }- Line 29: Before creating a new account, we check the database to ensure this email isn't already used.
33: User user = new User();
34: user.setName(request.getName());
35: user.setEmail(request.getEmail());
36: user.setPassword(passwordEncoder.encode(request.getPassword()));
37: user.setRole(Role.STUDENT);
39: return userRepository.save(user);- Lines 33-35: We create a new empty User and start filling it with the data the frontend sent us.
- Line 36: CRITICAL! We do NOT save
123456to the database. We use thepasswordEncoder(BCrypt) to scramble the password forever. - Line 37: We hardcode the role to
STUDENT. (This means everyone who registers gets the lowest access level by default safely). - Line 39: We save the new, secure user into the MySQL database.
42: public String login(String email, String password) {
44: User user = userRepository.findByEmail(email)
45: .orElseThrow(() -> new RuntimeException("User not found"));
47: if (!passwordEncoder.matches(password, user.getPassword())) {
48: throw new RuntimeException("Invalid password");
49: }
51: return JwtUtil.generateToken(user.getEmail());
52: }- Line 44: Find the user by their email.
- Line 47:
passwordEncoder.matches(...)takes the plain-text password from the login screen (e.g.123456), mathematically scrambles it, and checks if the output matches the scrambled text stored in the database. - Line 51: If the password is correct, we call
JwtUtil(The ID Card Maker) to generate a brand new JWT String to hand back to the frontend.
107: public String changeUserRole(Long userId, Role newRole) {
109: String email = SecurityContextHolder.getContext().getAuthentication().getName();
113: User currentUser = userRepository.findByEmail(email).orElseThrow();
116: if (currentUser.getRole() != Role.ADMIN) {
117: throw new RuntimeException("Only admins can change roles");
118: }- Line 109: When someone tries to change a role, we ask the "Bouncer" (
SecurityContextHolder) for the email of the person currently making the request. - Line 116: We check if the person making the request is an
ADMIN. If they are aSTUDENTorALUMNI, we throw an error and block them from modifying the database!
- Users send their plain text password to UserService to register.
- The password is scrambled by SecurityConfig (BCrypt) and safely saved.
- Upon Login, JwtUtil gives the user an unbreakable JWT token.
- On every subsequent request, JwtAuthenticationFilter forces the user to prove they hold a valid JWT.
- Behind the scenes, the filter uses CustomUserDetailsService to double-check that the user still actually exists in the database.