Introduction to Spring Security
Spring Security provides comprehensive security services for Java EE applications. As a core component of the Spring ecosystem, it implements layered security architecture where each application layer can be protected independently. This framework enables fine-grained access control at the controller, service, and data access levels through annotation-based configuration.
The framework's modular design ensures low coupling between components, allowing developers to combine different security modules to meet specific requirements. Unlike Apache Shiro which is primarily suited for monolithic applications, Spring Security entegrates seamlessly with microservices architectures built using Spring Cloud.
Two fundamental aspects of application security are authentication (verifying identity) and authorization (determining permissions). Spring Security addresses both domains effective. The framework includes essential dependencies like spring-security-web and spring-security-config, which Spring Boot encapsulates through the spring-boot-starter-security starter module.
Implementation Example
Create a new Spring Boot project with required dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
</dependencies>
Security Configuration
Create a security configuration class extending WebSecurityConfigurerAdapter with the @EnableWebSecurity annotation:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
public UserDetailsService memoryUserDetailsService() {
InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager();
userManager.createUser(
User.withUsername("developer")
.password(new BCryptPasswordEncoder().encode("secret"))
.roles("USER")
.build()
);
userManager.createUser(
User.withUsername("administrator")
.password(new BCryptPasswordEncoder().encode("secret"))
.roles("ADMIN", "USER")
.build()
);
return userManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/assets/**", "/home").permitAll()
.antMatchers("/profile/**").hasRole("USER")
.antMatchers("/admin/**").access("hasRole('ADMIN') and hasRole('DBA')")
.and()
.formLogin()
.loginPage("/signin")
.failureForwardUrl("/signin-error")
.and()
.logout()
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("session-id")
.and()
.exceptionHandling()
.accessDeniedPage("/forbidden");
}
}
BCryptPasswordEncoder implements SHA-256 hashing with salt and secret key encryption. During registration, user passwords are hashed and stored in the database. During authentication, entered passwords are hashed using the same algorithm and compared with stored values. This one-way process ensures that even if the database is compromised, passwords remain difficult to crack.
Controller Implementation
@Controller
public class PageController {
@RequestMapping("/")
public String root() {
return "redirect:/home";
}
@RequestMapping("/home")
public String home() {
return "home";
}
@RequestMapping("/profile/dashboard")
public String userProfile() {
return "profile/dashboard";
}
@RequestMapping("/signin")
public String signin() {
return "signin";
}
@RequestMapping("/signin-error")
public String signinError(Model model) {
model.addAttribute("signinError", true);
return "signin";
}
@GetMapping("/forbidden")
public String accessForbidden() {
return "forbidden";
}
}
Template Configuration
Configure Thymeleaf in application.yml:
spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false
Sign-in template signin.html:
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Authentication</title>
<link rel="stylesheet" th:href="@{/assets/style.css}" />
</head>
<body>
<h1>User Authentication</h1>
<p>Standard user: developer / secret</p>
<p>Administrator: administrator / secret</p>
<p th:if="${signinError}" class="error">Invalid credentials</p>
<form th:action="@{/signin}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" autofocus>
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<input type="submit" value="Sign In">
</form>
<a th:href="@{/home}">Return to Home</a>
</body>
</html>
Home template home.html:
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>Spring Security Demo</title>
<meta charset="utf-8" />
<link rel="stylesheet" th:href="@{/assets/style.css}" />
</head>
<body>
<h1>Welcome to Secure Application</h1>
<p>This page is publicly accessible.</p>
<div th:fragment="userPanel" sec:authorize="isAuthenticated()">
Authenticated User: <span sec:authentication="name"></span> |
Assigned Roles: <span sec:authentication="principal.authorities"></span>
<div>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</div>
</div>
- <a th:href="@{/profile/dashboard}">Access Protected Profile Section</a>
</body>
</html>
Restricted access template forbidden.html:
<html xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<body>
<div>
<h2>Insufficient Permissions</h2>
<div sec:authorize="isAuthenticated()">
<p>User authenticated</p>
<p>Identity: <span sec:authentication="name"></span></p>
<p>Roles: <span sec:authentication="principal.authorities"></span></p>
</div>
<div sec:authorize="isAnonymous()">
<p>No active session</p>
</div>
<p>Access denied to requested resource.</p>
</div>
</body>
</html>
Protected profile template profile/dashboard.html:
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Protected Profile Area</title>
<meta charset="utf-8" />
<link rel="stylesheet" th:href="@{/assets/style.css}" />
</head>
<body>
<div th:replace="home :: userPanel"></div>
<h1>Secure Profile Dashboard</h1>
<p><a th:href="@{/home}">Back to Home</a></p>
<p><a th:href="@{/content/articles}">Manage Articles</a></p>
</body>
</html>
Method-Level Security
Enable method-level protection using @EnableGlobalMethodSecurity with prePostEnabled = true. This activates @PreAuthorize and @PostAuthorize annotations supporting Spring Expression Language:
public class Article {
private Long identifier;
private String title;
private String body;
public Article(Long identifier, String title, String body) {
this.identifier = identifier;
this.title = title;
this.body = body;
}
// Getters and setters
}
public interface ArticleService {
List<Article> getAllArticles();
void removeArticle(long identifier);
}
@Service
public class ArticleServiceImpl implements ArticleService {
private List<Article> articles = new ArrayList<>();
public ArticleServiceImpl() {
articles.add(new Article(1L, "Spring Framework Guide", "Comprehensive overview"));
articles.add(new Article(2L, "Microservices Patterns", "Architectural approaches"));
}
@Override
public List<Article> getAllArticles() {
return articles;
}
@Override
public void removeArticle(long identifier) {
articles.removeIf(article -> article.getIdentifier() == identifier);
}
}
@RestController
@RequestMapping("/content/articles")
public class ArticleController {
@Autowired
private ArticleService articleService;
@GetMapping
public ModelAndView listArticles(Model model) {
model.addAttribute("articlesCollection", articleService.getAllArticles());
return new ModelAndView("articles/list", "articleModel", model);
}
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@GetMapping(value = "/{id}/removal")
public ModelAndView delete(@PathVariable("id") Long id, Model model) {
articleService.removeArticle(id);
model.addAttribute("articlesCollection", articleService.getAllArticles());
return new ModelAndView("articles/list", "articleModel", model);
}
}
Article listing template articles/list.html:
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<body>
<p>Current User: <span sec:authentication="name"></span></p>
<p>User Roles: <span sec:authentication="principal.authorities"></span></p>
| ID | Title | Description |
|---|---|---|
| | | | <a th:href="@{'/content/articles/' + ${article.identifier}+'/removal'}"> Delete </a> |
</body>
</html>
Database Integration
Add MySQL and JPA dependencies:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Database configuration in application.yml:
spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false
datasource:
url: jdbc:mysql://localhost:3306/security_demo?useUnicode=true&characterEncoding=utf8
username: root
password: password
jpa:
hibernate:
ddl-auto: update
show-sql: true
Entity classes:
@Entity
public class SystemUser implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column
private String password;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "user_authority",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
private List<Authority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// Implement other UserDetails methods
}
@Entity
public class Authority implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Override
public String getAuthority() {
return name;
}
// Getters and setters
}
public interface UserRepository extends JpaRepository<SystemUser, Long> {
SystemUser findByUsername(String username);
}
@Service
public class SystemUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
Database schema:
CREATE DATABASE `security_demo` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
CREATE TABLE `authority` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `system_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255),
PRIMARY KEY (`id`)
);
CREATE TABLE `user_authority` (
`user_id` bigint(20) NOT NULL,
`authority_id` bigint(20) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `system_user` (`id`),
FOREIGN KEY (`authority_id`) REFERENCES `authority` (`id`)
);
Initialize test data with encoded passwords:
@RunWith(SpringRunner.class)
@SpringBootTest
public class PasswordEncodingTest {
@Test
public void generateEncodedPasswords() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
for(int i = 0; i < 5; i++) {
System.out.println(encoder.encode("secret"));
}
}
}