🧠 Authorization with Spring Security (Spring 5.3.x + Jakarta EE 8, Legacy WAR)
Audience: Intermediate-to-Advanced Java Developers
Duration: ~3 hours (Lecture + Hands-on exercises)
Stack:
- Spring Framework 5.3.x (no Spring Boot)
- Servlet 4.x / JSP / WAR deploy
- Jakarta EE 8 APIs
- Spring Security 5.7.10
- Tomcat 9.x or Jetty 10+
- JPA-based persistence with
RegistrationRepositoryJPA - JDBC-based auth using
JdbcUserDetailsManager
🎯 Learning Objectives
By the end of this session, participants will:
- Apply URL-based and method-level security using Spring Security.
- Use
@PreAuthorize,@Secured, and JSP<sec:authorize>tags for role checks. - Customize security logic using expressions and
AccessDecisionVoters. - Configure a database-backed
UserDetailsService. - Implement fine-grained object-level authorization with ACLs.
🧭 1. Introduction
Goals:
- Introduce Spring Security in Jakarta EE 8
- Clarify the app architecture (WAR deployment, manual servlet config, mixed XML + JavaConfig)
- Demo the working app in Tomcat or Jetty, showcasing the
Registrationentity management
Talking Points:
Spring Security integrates into servlet-based Jakarta EE apps using manual filter registration and beans. You don’t need Spring Boot to build secure enterprise apps. In this project, we manage
Registrationentities, secured for admin-only operations like deletion.
🔐 2. Checking Authorization Rules in Code
Topics:
- Use
SecurityContextHolderto inspect roles - Show
hasRole,hasAuthority, and SpEL expressions
Hands-On:
Modify the deleteRegistration() method in DefaultRegistrationService:
public void deleteRegistration(long registrationId) {
if (SecurityContextHolder.getContext().getAuthentication()
.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"))) {
if (registrationRepositoryJPA.existsById(registrationId)) {
registrationRepositoryJPA.remove(registrationId);
} else {
throw new IllegalArgumentException("Registration with ID " + registrationId + " not found");
}
} else {
throw new AccessDeniedException("Not an admin");
}
}
- Task: Implement the above in
DefaultRegistrationService, ensuring it usesRegistrationRepositoryJPAto delete aRegistrationentity. - Test: Log in as an admin user (from
USER_ADMINtable) and verify deletion works programmatically.
🧱 3. Declaring URL and Method Security
Use actual SecurityConfig:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/resources/**").permitAll()
.antMatchers("/chat", "/registration/**").authenticated()
.antMatchers("/registration/delete/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/registration/list")
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout")
.and()
.csrf().disable();
return http.build();
}
Hands-On:
- Add URL pattern
/registration/delete/**to requireROLE_ADMINinSecurityConfig. - Test using JDBC users from the
USER_ADMINtable. - In
RegistrationController, add a delete endpoint:@PostMapping(value = "delete/{registrationId}") public String deleteRegistration(@PathVariable("registrationId") long registrationId) { registrationService.deleteRegistration(registrationId); return "redirect:/registration/list"; }
🏷 4. Using Spring Security Annotations
Concepts:
@Secured,@PreAuthorize,@PostAuthorize@RolesAllowedsupport (via JSR-250)
Enable annotations:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
// Existing configuration
}
JSP Tags:
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authorize access="hasRole('ADMIN')">
<form action="/registration/delete/${registration.id}" method="post">
<button type="submit">Delete</button>
</form>
</sec:authorize>
Hands-On:
- Update
deleteRegistration()inDefaultRegistrationServiceto use@PreAuthorize:@Override @Transactional @PreAuthorize("hasRole('ADMIN')") public void deleteRegistration(long registrationId) { if (registrationRepositoryJPA.existsById(registrationId)) { registrationRepositoryJPA.remove(registrationId); } else { throw new IllegalArgumentException("Registration with ID " + registrationId + " not found"); } } - Add the conditional “Delete” button to
WEB-INF/views/registration/list.jspas shown above. - Test: Ensure the “Delete” button is visible only to
ROLE_ADMINusers and that deletion is restricted to admins.
🧠 5. Understanding Authorization Decisions (30 minutes)
Concepts:
AccessDecisionManager- Built-in voters:
RoleVoter,AuthenticatedVoter,WebExpressionVoter - Custom
AccessDecisionVoterlogic
Example: Business hours voter
public class BusinessHoursVoter implements AccessDecisionVoter<Object> {
public int vote(Authentication auth, Object object, Collection<ConfigAttribute> attrs) {
LocalTime now = LocalTime.now();
return (now.isAfter(LocalTime.of(9, 0)) && now.isBefore(LocalTime.of(17, 0)))
? ACCESS_GRANTED : ACCESS_DENIED;
}
}
Register:
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<constructor-arg>
<list>
<bean class="org.springframework.security.access.vote.RoleVoter" />
<bean class="com.nicordesigns.security.BusinessHoursVoter" />
</list>
</constructor-arg>
</bean>
Hands-On:
- Implement
BusinessHoursVoterand register it inapplicationContext-security.xml. - Test by attempting to delete a registration outside business hours (9 AM–5 PM) and verify access is denied.
🗂 6. Access Control Lists (ACLs) for Object Security
Concepts:
- ACLs vs. role-based access
Sid,Acl,PermissionEvaluator, and object identity
Configuration:
<bean id="aclPermissionEvaluator" class="org.springframework.security.acls.AclPermissionEvaluator">
<constructor-arg ref="aclService"/>
</bean>
<bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<property name="permissionEvaluator" ref="aclPermissionEvaluator"/>
</bean>
JavaConfig:
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator());
return handler;
}
Hands-On:
- Implement a sample
RegistrationAclServicefor WRITE permission toRegistration:public interface RegistrationAclService { void addWritePermission(Registration registration, String username); boolean hasWritePermission(Registration registration, String username); } - Update
deleteRegistration()to use ACLs:@Override @Transactional @PreAuthorize("hasPermission(#registrationId, 'com.nicordesigns.site.Registration', 'WRITE')") public void deleteRegistration(long registrationId) { if (registrationRepositoryJPA.existsById(registrationId)) { registrationRepositoryJPA.remove(registrationId); } else { throw new IllegalArgumentException("Registration with ID " + registrationId + " not found"); } } - Test: Assign WRITE permission to a specific user for a
Registrationand verify only they can delete it.
✅ 7. Wrap-Up
Recap:
- Declarative and programmatic security for
Registrationoperations - JDBC authentication with
USER_ADMINtable - Taglibs and annotations for securing
deleteRegistration() - Method security and ACLs for fine-grained access control
Best Practices:
- Prefer annotations (
@PreAuthorize) for clean code - Externalize configuration in
applicationContext-security.xmlwhere possible - Use ACLs sparingly—only where fine-grained access to
Registrationentities is essential
📋 Instructor Notes: Setup Summary
Core Files:
web.xml: Registers Spring context,DispatcherServlet, andspringSecurityFilterChainapplicationContext.xml: Core Spring beans (e.g.,DataSource,entityManagerFactory)applicationContext-security.xml: Optional overrides or ACL-specific beansSecurityConfig.java: DeclaresSecurityFilterChain, JDBC user service,PasswordEncoderRegistrationController.java: Handles HTTP requests for listing, viewing, creating, and deletingRegistrationentitiesDefaultRegistrationService.java: Implements business logic withRegistrationRepositoryJPARegistrationRepositoryJPA.java: JPA repository forRegistrationpersistenceWEB-INF/views/registration/list.jsp: Displays registration list with conditional “Delete” button
Maven Dependencies:
- No Spring Boot; use explicit Spring 5.3.x and Security 5.7.x dependencies
- Include
spring-security-taglibsfor JSP tags:<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>5.7.10</version> </dependency>
Database Setup:
- Ensure
registrationstable exists incharityDB(MariaDB or H2):CREATE TABLE registrations ( id BIGINT PRIMARY KEY AUTO_INCREMENT, userName VARCHAR(255), subject VARCHAR(255), body TEXT, dateCreated DATETIME ); USER_ADMINtable for JDBC authentication, as configured inJdbcUserDetailsManager
Testing Instructions
- Deploy: Deploy the WAR to Tomcat 9.x or Jetty 10+.
- Section 2: Test programmatic security by calling
deleteRegistration()as an admin and non-admin user. - Section 3: Verify
/registration/delete/**is restricted toROLE_ADMINvia URL access. - Section 4: Confirm the “Delete” button in
list.jspis visible only toROLE_ADMINusers and that@PreAuthorizerestricts deletion. - Section 5: Test
BusinessHoursVoterby attempting deletion outside 9 AM–5 PM. - Section 6: Assign WRITE permission to a
Registrationand verify ACL-based deletion.
Troubleshooting Tips
- JSP Issues: If the
<sec:authorize>tag fails, verify thespring-security-taglibsdependency and taglib URI. - Security Issues: If non-admin users can delete, check
SecurityFilterChainand@EnableGlobalMethodSecurity. - Database Errors: Ensure the
registrationstable matches theRegistrationentity and thathibernate.hbm2ddl.auto=validatedoesn’t fail. - Repository Errors: Confirm
RegistrationRepositoryJPAis correctly wired with@Qualifier("registrationRepositoryJPA").