Welcome, ${currentUser}!
#if> <#-- Show only to administrators --> <#if isAdmin> Admin Dashboard #if> <#-- Show only to anonymous users --> <#if !currentUser??> Login Register #if> ``` The `?? ` operator in FreeMarker checks whether a variable exists and is not null. This approach is straightforward, testable, and avoids the complexity of integrating JSP taglibs into FreeMarker. --- ## 8.11 Customizing Login and Logout Pages ### Custom Login Page Configure the login form in the security filter chain: ```java .formLogin(form -> form .loginPage("/login") .loginProcessingUrl("/perform-login") .defaultSuccessUrl("/dashboard", true) .failureUrl("/login?error=true") .permitAll() ) ``` `loginPage("/login")` specifies the URL that displays the login form — you must create a controller mapping and a template for this. `loginProcessingUrl("/perform-login")` is the URL where the form submits credentials — Spring Security handles this automatically, you do not write a controller for it. `defaultSuccessUrl("/dashboard", true)` redirects to `/dashboard` after successful login. `failureUrl("/login?error=true")` redirects back to the login page with an error parameter on failure. Create a controller that serves the login page: ```java @Controller public class AuthController { @GetMapping("/login") public String loginPage() { return "login"; } } ``` And the FreeMarker template: ```html <#import "/spring.ftl" as spring> <#import "layout.ftlh" as layout> <@layout.page title="Login">Invalid username or password.
#if> <#if RequestParameters.logout??>You have been logged out.
#if> @layout.page> ``` The CSRF token hidden field is critical — Spring Security rejects any POST request that does not include a valid CSRF token. The `_csrf` object is automatically available in the model when CSRF protection is enabled. ### Custom Logout ```java .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ) ``` The logout form must be a POST request (for CSRF protection): ```html ``` --- ## 8.12 User Registration A registration flow involves a form, validation, password encoding, and saving the user to the database: ```java @Controller public class RegistrationController { private final UserService userService; public RegistrationController(UserService userService) { this.userService = userService; } @GetMapping("/register") public String showRegistrationForm(Model model) { model.addAttribute("registrationForm", new RegistrationForm()); return "register"; } @PostMapping("/register") public String registerUser(@Valid @ModelAttribute RegistrationForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) { if (bindingResult.hasErrors()) { return "register"; } if (userService.existsByUsername(form.getUsername())) { bindingResult.rejectValue("username", "error.username", "Username already exists"); return "register"; } userService.registerUser(form.getUsername(), form.getPassword()); redirectAttributes.addFlashAttribute("success", "Registration successful! Please log in."); return "redirect:/login"; } } ``` This follows the same patterns from Chapter 3 — `@Valid` triggers validation, `BindingResult` captures errors, and `RedirectAttributes` carries a flash message through a redirect. The additional check for duplicate usernames happens after validation passes, using `bindingResult.rejectValue` to add a field-level error that the template can display like any other validation error. --- ## 8.13 Securing REST APIs REST APIs require a different security approach than server-rendered pages. APIs are typically stateless — each request carries its own credentials rather than relying on a server-side session. There is no login page — credentials come in HTTP headers. You can define a separate `SecurityFilterChain` for API endpoints: ```java @Bean public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN") .anyRequest().authenticated() ) .httpBasic(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()); return http.build(); } ``` Several things are different from the web page configuration. `securityMatcher("/api/**")` scopes this filter chain to API endpoints only. You can have multiple filter chains — one for web pages (with form login and CSRF) and another for APIs (with HTTP Basic and no CSRF). Spring Security applies the chain whose `securityMatcher` matches the incoming request. `httpBasic(Customizer.withDefaults())` enables HTTP Basic authentication, where the client sends credentials in the `Authorization` header with every request. This is simple and appropriate for server-to-server communication or development testing. `csrf(csrf -> csrf.disable())` disables CSRF protection for the API. CSRF protection defends against browser-based attacks where the browser automatically sends cookies. Stateless APIs that do not use cookies are not vulnerable to CSRF, so the protection is unnecessary. **Note:** In Spring Security 7, CSRF is enabled for all endpoints by default, including APIs. If your API is truly stateless and does not use cookie-based authentication, disabling CSRF for API routes is appropriate. --- ## 8.14 CORS Configuration **CORS (Cross-Origin Resource Sharing)** controls which domains can call your API from a browser. If your React frontend runs on `http://localhost:3000` and your Spring Boot API runs on `http://localhost:8080`, the browser blocks the API calls by default — they come from different origins. CORS configuration tells the browser which cross-origin requests are allowed. ### Global Configuration ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsConfigurationRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:3000", "https://myfrontend.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } } ``` This allows requests from the specified origins to all `/api/**` endpoints using the listed HTTP methods. `allowCredentials(true)` permits the browser to send cookies with cross-origin requests. `maxAge(3600)` tells the browser to cache the CORS preflight response for one hour, reducing the number of preflight OPTIONS requests. ### CORS in the Security Filter Chain When Spring Security is active, CORS must also be configured in the security filter chain — otherwise the security filter rejects cross-origin preflight requests before they reach the MVC CORS configuration: ```java http .cors(cors -> cors.configurationSource(corsConfigurationSource())) // ... other configuration @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:3000")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return source; } ``` ### Per-Controller CORS For simpler cases, you can use `@CrossOrigin` directly on a controller: ```java @RestController @RequestMapping("/api/products") @CrossOrigin(origins = "http://localhost:3000") public class ProductRestController { // ... } ``` This is convenient for quick development but does not scale well — global configuration is preferable when multiple controllers need the same CORS policy. --- ## 8.15 Session-Based vs. Token-Based Authentication Everything we have covered so far uses **session-based authentication** — the server creates a session after login, stores it in memory, and sends a session ID cookie to the browser. The browser sends this cookie with every subsequent request, and the server looks up the session to identify the user. **Token-based authentication** works differently. After login, the server issues a **JWT (JSON Web Token)** — a self-contained token that includes the user's identity and roles, signed by the server. The client stores the token and sends it in the `Authorization` header with every request. The server validates the token's signature and extracts user information from it — no server-side session storage is needed. A JWT consists of three Base64-encoded parts separated by dots: a header (algorithm and token type), a payload (claims — user info, roles, expiration time), and a signature (verifies the token has not been tampered with). ### When to Use Each | Session-Based | Token-Based (JWT) | |---------------|-------------------| | Server stores session state. | Server is stateless. | | Good for traditional server-rendered web applications. | Good for SPAs, mobile apps, and microservices. | | Uses cookies. | Uses the `Authorization` header. | | Simpler to implement. | More scalable (no server-side session storage). | | Logout is straightforward (invalidate session). | Logout requires token blacklisting or short expiration. | ### JWT Flow The typical JWT authentication flow works as follows: the client sends credentials to a login endpoint (e.g., `POST /api/auth/login`), the server validates the credentials and generates a signed JWT, the client stores the JWT and includes it in the `Authorization` header for subsequent requests (`Authorization: Bearer eyJhbGciOiJIUzI1...`), and the server validates the JWT signature and extracts user info from it on each request. Full JWT implementation requires additional libraries (such as `jjwt`) and a custom security filter. This is an advanced topic beyond the scope of this chapter, but understanding the concept is important — JWT is the dominant authentication mechanism for modern APIs. --- ## 8.16 Putting It All Together: A Complete Security Configuration Here is a complete configuration that combines custom user authentication, URL-based authorization, method-level security, form login, and logout: ```java @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/home", "/register").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() ) .logout(logout -> logout .logoutSuccessUrl("/login?logout") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` The `CustomUserDetailsService` from section 8.7 is automatically detected by Spring Boot because it is annotated with `@Service` and implements `UserDetailsService`. No explicit wiring is needed in the security configuration. The `@EnableMethodSecurity` annotation activates `@PreAuthorize` support so that service methods can enforce their own authorization rules beyond what URL patterns can express. Static resources (`/css/**`, `/js/**`, `/images/**`) must be explicitly permitted — otherwise Spring Security will require authentication for stylesheets and JavaScript files, which breaks the appearance and functionality of public pages including the login page. --- ## Summary This chapter covered securing a Spring Boot application with Spring Security 7. **Common vulnerabilities** — CSRF, XSS, SQL injection, broken authentication — motivate why security cannot be an afterthought. Spring Security provides defenses against many of these out of the box. **Adding the security starter** immediately secures all endpoints, generates a default user, enables a login form, activates CSRF protection, and sets security headers. **`SecurityFilterChain`** customizes access rules using `authorizeHttpRequests` with matchers like `permitAll()`, `hasRole()`, `hasAnyRole()`, and `authenticated()`. Rules are evaluated in order — specific rules first, catch-all last. **Authentication** can use in-memory users (for development), JDBC-backed users (for standard schemas), or a custom `UserDetailsService` (for full control over user loading from your own entity model). **Password encoding** with `BCryptPasswordEncoder` ensures that passwords are never stored in plain text. BCrypt provides automatic salting and configurable computational cost. **Role-based access control** enforces authorization at the URL level (in the filter chain) and at the method level (`@PreAuthorize` with `@EnableMethodSecurity`). **FreeMarker integration** with Spring Security is achieved through a `@ControllerAdvice` that exposes authentication information as model attributes, enabling conditional display of content based on the user's identity and roles. **Custom login and logout pages** are FreeMarker templates that submit to Spring Security's processing URLs. CSRF tokens must be included as hidden fields in all POST forms. **REST API security** uses a separate `SecurityFilterChain` scoped with `securityMatcher`, typically with HTTP Basic authentication and CSRF disabled for stateless endpoints. **CORS configuration** allows cross-origin requests from specified frontend domains, configured both in the MVC layer and in the security filter chain. **Token-based authentication** with JWT is an alternative to session-based authentication, suited for SPAs, mobile apps, and microservices. It eliminates server-side session storage but requires careful token management. --- ## Resources - [Spring Security Reference Documentation](https://docs.spring.io/spring-security/reference/index.html) - [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture/) - [What's New in Spring Security 7](https://docs.spring.io/spring-security/reference/whats-new.html) - [OWASP Top Ten](https://owasp.org/www-project-top-ten/) - [Baeldung: Spring Security](https://www.baeldung.com/security-spring) - [BCrypt Password Hashing](https://en.wikipedia.org/wiki/Bcrypt) - [JWT.io](https://jwt.io/) --- ## Lab Assignment: Secure Your Web Application Add authentication and authorization to your existing web application. **Requirements:** 1. **Add Spring Security** and configure a `SecurityFilterChain` with URL-based access rules: public pages (home, about, registration) accessible to everyone, protected pages requiring authentication, and admin-only pages accessible only to users with the `ADMIN` role. 2. **Implement user authentication** using a custom `UserDetailsService` backed by a JPA entity and repository. Store passwords encoded with `BCryptPasswordEncoder`. 3. **Create a user registration flow** with a registration form, Bean Validation, duplicate username checking, and password encoding before saving. 4. **Customize the login and logout pages** using FreeMarker templates. Include CSRF tokens in all POST forms. Display error and success messages (invalid credentials, logout confirmation, registration success). 5. **Add conditional content** in your templates based on authentication status — show a welcome message and logout button for authenticated users, show login and register links for anonymous users, and show admin-only navigation for administrators. 6. **Add method-level security** with `@PreAuthorize` on at least one service method — for example, restricting a delete operation to administrators.