diff options
| author | Kamal Wickramanayake <kamal@inbox.lk> | 2026-06-13 21:03:31 +0530 |
|---|---|---|
| committer | Kamal Wickramanayake <kamal@inbox.lk> | 2026-06-13 21:03:31 +0530 |
| commit | f0001c5c8d8a33729b93d8d6e10d3af40bfabed6 (patch) | |
| tree | 6042e5bfdd48671daa95baf29c5041e526f5b62e /spring-boot | |
| parent | 6ae21cd4e51756ea14b43b82a9d500bca9fb8032 (diff) | |
Added sample application 09-spring-security-db-authentication
Diffstat (limited to 'spring-boot')
19 files changed, 529 insertions, 0 deletions
diff --git a/spring-boot/09-spring-security-db-authentication/.gitignore b/spring-boot/09-spring-security-db-authentication/.gitignore new file mode 100644 index 0000000..3df278e --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/.gitignore @@ -0,0 +1,2 @@ +target +.vscode diff --git a/spring-boot/09-spring-security-db-authentication/README b/spring-boot/09-spring-security-db-authentication/README new file mode 100644 index 0000000..6e50354 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/README @@ -0,0 +1,23 @@ +This sample project shows how to authenticate users based on credentials maintained in a Postgresql database. + +Passwords are hashed using Argon2. Look at the SecurityConfig.java for Argon2 input parameters used. + +To run with support for hot swap during development, use the dev profile as follows: + + mvn spring-boot:run -Dspring-boot.run.profiles=dev + +User/Pasword (as defined in resources/data.sql) + + admin: abc123 + user1: abc123 + +Key points: + +1. pom.xml dependencies +2. config/SecurityConfig.java - Can be used to modify Spring Security behavior. +3. user package Java source files + - serivice/CustomUserDetailsService.java is the key source file that defines a UserDetailsService bean that fetches user details from UserRepository (which fetches data from database using JPA) + +Misc: + +Online Argon2 hash generator: https://argon2.online/
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/pom.xml b/spring-boot/09-spring-security-db-authentication/pom.xml new file mode 100644 index 0000000..96e4a92 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/pom.xml @@ -0,0 +1,68 @@ +<?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>com.example.spring.boot</groupId> + <artifactId>base-config</artifactId> + <version>0.0.1-SNAPSHOT</version> + <relativePath>../00-config</relativePath> + </parent> + + <artifactId>thymeleaf-common-theme</artifactId> + + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + + <!-- Below is not always required when Spring Security is used. + But in layout/main.html, the logout link definition uses a capability in this + library (the sec:authorize attribyte)--> + <dependency> + <groupId>org.thymeleaf.extras</groupId> + <artifactId>thymeleaf-extras-springsecurity6</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <version>42.7.11</version> + </dependency> + + <!-- Bouncy Castle Provider (Required by Spring's Argon2 implementation) --> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk18on</artifactId> + <version>1.84</version> <!-- Use the latest stable version available --> + </dependency> + + <!--This + is to automatically reload web pages during development --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-devtools</artifactId> + <optional>true</optional> + </dependency> + + </dependencies> + +</project>
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/Application.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/Application.java new file mode 100644 index 0000000..4393f3f --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/Application.java @@ -0,0 +1,12 @@ +package com.example.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) throws Exception { + SpringApplication.run(Application.class, args); + } +}
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/HomeController.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/HomeController.java new file mode 100644 index 0000000..d6ead56 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/HomeController.java @@ -0,0 +1,13 @@ +package com.example.spring; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class HomeController { + + @RequestMapping("/") + public String home() { + return "index"; + } +} diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/config/SecurityConfig.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/config/SecurityConfig.java new file mode 100644 index 0000000..1f400d5 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/config/SecurityConfig.java @@ -0,0 +1,16 @@ +package com.example.spring.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + // An Argon2 password encoder with a salt length of 16 bytes, a hash length of 32 bytes, parallelism of 1, memory cost of 1 << 14 (i.e. 16MB) and 2 iterations. + return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + } +}
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/contact/controller/ContactController.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/contact/controller/ContactController.java new file mode 100644 index 0000000..6d5e9b4 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/contact/controller/ContactController.java @@ -0,0 +1,34 @@ +package com.example.spring.contact.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import jakarta.validation.Valid; + +@Controller +public class ContactController { + + @GetMapping("/contact") + public String showForm(Model model) { + model.addAttribute("contactForm", new ContactForm()); + return "contact/form"; + } + + @PostMapping("/contact") + public String processSubmission(@Valid @ModelAttribute ContactForm contactForm, BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + return "contact/form"; + } + + // Do whatever needed with the submitted data here. + // For example, invoke a method in a service/componet bean to send an email to contact person. + + return "contact/result"; + } + +} diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/contact/controller/ContactForm.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/contact/controller/ContactForm.java new file mode 100644 index 0000000..925999b --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/contact/controller/ContactForm.java @@ -0,0 +1,45 @@ +package com.example.spring.contact.controller; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public class ContactForm { + + @Pattern(regexp = "^[ a-zA-Z]+$", message = "Name should only include English letters") + @NotBlank(message = "Name is required") + private String name; + + @Email(message = "Must be a valid email") + @NotBlank(message = "Email is required") + private String email; + + @Size(min = 10, message = "Message must be at least 10 characters long") + private String message; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + +} diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/entity/User.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/entity/User.java new file mode 100644 index 0000000..a8ec6d2 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/entity/User.java @@ -0,0 +1,53 @@ +package com.example.spring.user.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/* + @Table - Used to set the database table name explicitely. +*/ +@Entity +@Table(name="user_account") +public class User implements UserDetails { + + /* + GenerationType.IDENTITY - Auto generate value based on database's native auto increment feature + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String username; + + private String password; + + /* + Roles and permissions can be represented by GrantedAuthority instances. + */ + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return new ArrayList<>(); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + +} diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/repository/UserRepository.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/repository/UserRepository.java new file mode 100644 index 0000000..f719637 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.example.spring.user.repository; + +import java.util.Optional; + +import org.springframework.data.repository.ListCrudRepository; +import org.springframework.stereotype.Repository; + +import com.example.spring.user.entity.User; + +@Repository +public interface UserRepository extends ListCrudRepository<User, Long> { + Optional<User> findByUsername(String username); +} diff --git a/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/service/CustomUserDetailsService.java b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/service/CustomUserDetailsService.java new file mode 100644 index 0000000..c61649e --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/java/com/example/spring/user/service/CustomUserDetailsService.java @@ -0,0 +1,25 @@ +package com.example.spring.user.service; + +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; + +import com.example.spring.user.repository.UserRepository; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + +}
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/application.properties b/spring-boot/09-spring-security-db-authentication/src/main/resources/application.properties new file mode 100644 index 0000000..8a8c4c2 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/application.properties @@ -0,0 +1 @@ +# Look at resources/config/application-dev.properties for settings for dev profile.
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/config/application-dev.properties b/spring-boot/09-spring-security-db-authentication/src/main/resources/config/application-dev.properties new file mode 100644 index 0000000..50973c7 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/config/application-dev.properties @@ -0,0 +1,33 @@ +# This file contains settings used during development. +# If using Maven to run, try the following command: +# mvn spring-boot:run -Dspring-boot.run.profiles=dev + +# Web related settings + +# Disable caching for static resources +spring.web.resources.cache.period=0 +spring.thymeleaf.cache=false + +# Optional: Tell DevTools to watch changes but never restart the backend server for CSS +spring.devtools.restart.exclude=static/**,public/**,templates/** + + +# Database related settings + +spring.datasource.url=jdbc:postgresql://localhost:5432/springboot +spring.datasource.username=springboot +spring.datasource.password=abc123 + +# Create/update/drop database tables automatically? +# In production, setting value to none is better. +# Possible values: none, validate, update, create, create-drop +spring.jpa.hibernate.ddl-auto=create-drop + +# Print sql that gets executed. Good for developers. Set to false in production. +spring.jpa.show-sql=true + +# Always execute initialization scripts (resources/schema.sql, resources/data.sql) on startup +spring.sql.init.mode=always + +# Data source initialization scripts (schema.sql and data.sql) are executed after Hibernate/JPA auto-creates database tables. +spring.jpa.defer-datasource-initialization=true diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/data.sql b/spring-boot/09-spring-security-db-authentication/src/main/resources/data.sql new file mode 100644 index 0000000..4c33897 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/data.sql @@ -0,0 +1,2 @@ +INSERT INTO user_account (username, password) VALUES ('admin','$argon2id$v=19$m=16,t=2,p=1$YnJERWhhY0RxV1hGMFNWVg$OHRvhGZx1x9KqCoVZOfUD2TkJc+HQB5SamgYejw+K2Y'); +INSERT INTO user_account (username, password) VALUES ('user1','$argon2id$v=19$m=16,t=2,p=1$YnJERWhhY0RxV1hGMFNWVg$OHRvhGZx1x9KqCoVZOfUD2TkJc+HQB5SamgYejw+K2Y');
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/static/css/main.css b/spring-boot/09-spring-security-db-authentication/src/main/resources/static/css/main.css new file mode 100644 index 0000000..edd53ea --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/static/css/main.css @@ -0,0 +1,92 @@ +body { + font-size: 1.125em; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.6em; + color: rgb(32, 34, 36);; + background-color: #FFF; + padding: 10px 5px; +} + +a { + color: #1475AC; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +ul li { + padding-bottom: 0.5em; +} + +header { + text-align: left; + font-size: 1.2em; +} + +header h1 { + margin-top: 0; + margin-bottom: 20px; +} + +header a { + color: #000000; + text-decoration: none; +} + +.main-nav { + margin-top: 0; + padding: 0; +} + +.main-nav ul { + margin: 0; + padding : 0; + list-style-type: none; + background-color: #F6F8F8; + border: 1px solid #eee; +} + +.main-nav ul li { + margin-left: 0; + display: inline; + padding: 0 8px; + border-left: 1px solid #F9F9FF; + border-right: 1px solid #F9F9FF; +} + +.main-nav ul li:hover { + background-color: white; + border-left: 1px solid #addfff; + border-right: 1px solid #addfff; +} + +.main-nav ul li a { + color: #555; + font-weight: bold; +} + +.main-nav ul li ai:hover { + color: #000; +} + +footer { + margin-top: 20px; + font-size: 85%; + border-top: 1px solid #CCC; +} + +.form-field-container { + margin-top: 8px; + margin-bottom: 8px; +} + +.form-field-container label { + display: inline-block; + width: 5em; +} + +.form-field-container .validation-error { + color: red; +}
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/contact/form.html b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/contact/form.html new file mode 100644 index 0000000..7debe1f --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/contact/form.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout/main :: layout(title='Contact Us', content=~{::main})}"> + +<body> + <main> + <form th:action="@{/contact}" th:object="${contactForm}" method="post"> + <div class="form-field-container"> + <label>Name</label> + <input type="text" th:field="*{name}" placeholder="Your name" /> + <div class="validation-error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div> + </div> + + <div class="form-field-container"> + <label>Email</label> + <input type="email" th:field="*{email}" placeholder="Your email" /> + <div class="validation-error" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div> + </div> + + <div class="form-field-container"> + <label>Message</label> + <textarea th:field="*{message}" placeholder="Your message"></textarea> + <div class="validation-error" th:if="${#fields.hasErrors('message')}" th:errors="*{message}"></div> + </div> + + <button type="submit">Send</button> + </form> + </main> +</body> + +</html>
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/contact/result.html b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/contact/result.html new file mode 100644 index 0000000..d1bda9a --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/contact/result.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout/main :: layout(title='Contact Us', content=~{::main})}"> + +<body> + <main> + <p>We received your message.</p> + + <p th:text="'Thank you ' + ${contactForm.name} + '.'" /> + + <a th:href="@{/contact}">Try again</a> + </main> +</body> + +</html>
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/index.html b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/index.html new file mode 100644 index 0000000..dce7a4e --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:sec="http://thymeleaf.org/extras/spring-security" + th:replace="~{layout/main :: layout(title='Welcome', content=~{::main})}"> + +<body> + <main> + <p>Hello, <span sec:authentication="name">Username Placeholder</span>!</p> + + <p>Hello, [[${#authentication.name}]]!</p> + + <p>This is the home page content.</p> + + <p>Use the navigation links to explore other pages.</p> + </main> +</body> + +</html>
\ No newline at end of file diff --git a/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/layout/main.html b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/layout/main.html new file mode 100644 index 0000000..5f988d0 --- /dev/null +++ b/spring-boot/09-spring-security-db-authentication/src/main/resources/templates/layout/main.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:sec="http://thymeleaf.org/extras/spring-security" + th:fragment="layout(title, content)"> + +<head> + <title th:text="|${title} - ABC App|">Base title not shown</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <link rel="stylesheet" th:href="@{/css/main.css}" /> +</head> + +<body> + <header> + <h1>ABC App</h1> + </header> + + <nav class="main-nav"> + <ul> + <li><a th:href="@{/}">Home</a></li> + <li><a th:href="@{/contact}">Contact Us</a></li> + <li><a th:href="@{/profile/{name}(name=${#authentication.name})}" sec:authorize="isAuthenticated()">Profile</a></li> + <li><a th:href="@{/logout}" sec:authorize="isAuthenticated()">Logout</a></li> + </ul> + </nav> + + <h3 th:text="|${title}|">Page content area title</h3> + + <th:block th:replace="${content}" /> + + <footer> + Copyright © 2026 Example Company. All rights reserved. + </footer> +</body> + +</html>
\ No newline at end of file |
