This tutorial demonstrates that Spring Security integrates with OfficeFloor REST YAML composition without any extra plumbing. Adding spring-boot-starter-security to the project is all that is required — Spring Security's filters, authentication context, and annotations are all immediately available inside OfficeFloor service methods, exactly as they would be in a regular Spring MVC controller method.
The tutorial uses eight endpoints to show this:
GET /security/public — open to everyone; no authentication requiredGET /security/me — requires authentication; injects the current user via @AuthenticationPrincipalGET /security/roles — requires authentication; reads the user's granted authoritiesGET /security/auth — requires authentication; receives Authentication as a plain method parameterGET /security/bean — requires authentication; receives a Spring Security bean (UserDetailsService) by typeGET /security/preauthorize — requires ADMIN role, enforced by @PreAuthorizeGET /security/secured — requires ADMIN role, enforced by @SecuredGET /security/rolesallowed — requires ADMIN role, enforced by @RolesAllowedThe only change compared to a non-secured application is adding spring-boot-starter-security alongside the OfficeFloor starter. No additional bridges, adapters, or OfficeFloor-specific security modules are needed:
<dependency> <groupId>net.officefloor.springboot</groupId> <artifactId>officefloor-rest-spring-boot-starter</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-security</artifactId> </dependency>
@SpringBootApplication
public class SpringRestSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestSecurityApplication.class, args);
}
}
The SecurityFilterChain is a standard Spring Security configuration. @EnableMethodSecurity is added to activate method-level security annotations across the application — including on OfficeFloor service methods. securedEnabled = true enables @Secured and jsr250Enabled = true enables @RolesAllowed; @PreAuthorize is on by default:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/security/public").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
@SuppressWarnings("deprecation")
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user").password("password").roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin").password("password").roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
Spring Security's filter chain runs before OfficeFloor's handler interceptor, so unauthenticated requests to protected paths are rejected by Spring Security before they ever reach the service method. No OfficeFloor configuration is involved.
A method that needs no authentication is written exactly as any other OfficeFloor service method. The YAML simply names the class — no security annotation is needed:
service: class: net.officefloor.tutorial.springrestsecurity.PublicGreetingService
public class PublicGreetingService {
public void service(ObjectResponse<String> response) {
response.send("Hello, World!");
}
}
@AuthenticationPrincipalFor a protected endpoint, the YAML is the same — just a class name:
service: class: net.officefloor.tutorial.springrestsecurity.CurrentUserService
The service method receives the authenticated user by declaring a UserDetails parameter annotated with @AuthenticationPrincipal. OfficeFloor recognises this as a Spring argument annotation and delegates its resolution to Spring MVC's argument resolvers, which read the value from the SecurityContext:
public class CurrentUserService {
public void service(@AuthenticationPrincipal UserDetails user, ObjectResponse<String> response) {
response.send("Hello, " + user.getUsername() + "!");
}
}
This is the same mechanism Spring MVC uses internally — OfficeFloor does not implement a separate security integration; it reuses Spring's existing argument resolution infrastructure.
Granted authorities are accessed through the same UserDetails object:
service: class: net.officefloor.tutorial.springrestsecurity.UserRolesService
public class UserRolesService {
public void service(@AuthenticationPrincipal UserDetails user, ObjectResponse<String> response) {
String roles = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.sorted()
.collect(Collectors.joining(", "));
response.send(roles);
}
}
Authentication as a direct parameterThe Authentication object from the SecurityContext is also available as a plain method parameter — no annotation required. OfficeFloor recognises it by type and supplies it automatically:
service: class: net.officefloor.tutorial.springrestsecurity.AuthenticationService
public class AuthenticationService {
public void service(Authentication authentication, ObjectResponse<String> response) {
response.send("Authenticated as: " + authentication.getName());
}
}
OfficeFloor registers every bean in the Spring application context as a managed object available by type. Spring Security beans — including UserDetailsService, PasswordEncoder, and the SecurityFilterChain itself — are therefore available as plain parameters with no annotation:
service: class: net.officefloor.tutorial.springrestsecurity.SecurityBeanService
public class SecurityBeanService {
public void service(UserDetailsService userDetailsService, ObjectResponse<String> response) {
UserDetails user = userDetailsService.loadUserByUsername("user");
response.send("Loaded: " + user.getUsername());
}
}
This is the same mechanism used for any Spring @Service or @Component — there is no special handling for security beans.
@PreAuthorize@PreAuthorize on a service method is evaluated by OfficeFloor before the method is invoked. The full Spring Security SpEL expression language is available — hasRole(), hasAuthority(), isAuthenticated(), and any custom expressions configured via a custom MethodSecurityExpressionHandler:
service: class: net.officefloor.tutorial.springrestsecurity.PreAuthorizeService
public class PreAuthorizeService {
@PreAuthorize("hasRole('ADMIN')")
public void service(ObjectResponse<String> response) {
response.send("Admin access via @PreAuthorize");
}
}
If the expression evaluates to false, OfficeFloor returns 403 Forbidden before the service method body executes.
@Secured@Secured accepts one or more role names (including the ROLE_ prefix). Enable it in the security configuration with securedEnabled = true:
service: class: net.officefloor.tutorial.springrestsecurity.SecuredService
public class SecuredService {
@Secured("ROLE_ADMIN")
public void service(ObjectResponse<String> response) {
response.send("Admin access via @Secured");
}
}
@RolesAllowedThe JSR-250 @RolesAllowed annotation works the same way. Enable it with jsr250Enabled = true:
service: class: net.officefloor.tutorial.springrestsecurity.RolesAllowedService
public class RolesAllowedService {
@RolesAllowed("ADMIN")
public void service(ObjectResponse<String> response) {
response.send("Admin access via @RolesAllowed");
}
}
Tests use the standard Spring Security test support. No OfficeFloor-specific test utilities are required.
@WithMockUser creates a synthetic user in the SecurityContext without touching UserDetailsService. @WithUserDetails("user") instead loads the real UserDetails object from the configured UserDetailsService — useful when the service method inspects granted authorities or other fields populated by the real implementation.
The tests cover all eight endpoints, both test annotations, and access/denial cases for each method-level security annotation:
@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestSecurityTest {
@Autowired
private MockMvc mvc;
@Test
public void public_endpoint_no_auth_required() throws Exception {
mvc.perform(get("/security/public"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, World!"));
}
@Test
public void protected_endpoint_requires_authentication() throws Exception {
mvc.perform(get("/security/me"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "daniel", roles = "USER")
public void current_user_via_authentication_principal() throws Exception {
mvc.perform(get("/security/me"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, daniel!"));
}
@Test
@WithUserDetails("user")
public void with_user_details_loads_real_user() throws Exception {
mvc.perform(get("/security/me"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, user!"));
}
@Test
@WithMockUser(username = "daniel", roles = {"USER", "ADMIN"})
public void user_roles_from_granted_authorities() throws Exception {
mvc.perform(get("/security/roles"))
.andExpect(status().isOk())
.andExpect(content().string("ROLE_ADMIN, ROLE_USER"));
}
@Test
@WithMockUser(username = "daniel", roles = "USER")
public void authentication_as_direct_parameter() throws Exception {
mvc.perform(get("/security/auth"))
.andExpect(status().isOk())
.andExpect(content().string("Authenticated as: daniel"));
}
@Test
@WithMockUser(username = "daniel", roles = "USER")
public void spring_security_bean_injected_as_parameter() throws Exception {
mvc.perform(get("/security/bean"))
.andExpect(status().isOk())
.andExpect(content().string("Loaded: user"));
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
public void pre_authorize_grants_admin() throws Exception {
mvc.perform(get("/security/preauthorize"))
.andExpect(status().isOk())
.andExpect(content().string("Admin access via @PreAuthorize"));
}
@Test
@WithMockUser(username = "user", roles = "USER")
public void pre_authorize_denies_non_admin() throws Exception {
mvc.perform(get("/security/preauthorize"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
public void secured_grants_admin() throws Exception {
mvc.perform(get("/security/secured"))
.andExpect(status().isOk())
.andExpect(content().string("Admin access via @Secured"));
}
@Test
@WithMockUser(username = "user", roles = "USER")
public void secured_denies_non_admin() throws Exception {
mvc.perform(get("/security/secured"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
public void roles_allowed_grants_admin() throws Exception {
mvc.perform(get("/security/rolesallowed"))
.andExpect(status().isOk())
.andExpect(content().string("Admin access via @RolesAllowed"));
}
@Test
@WithMockUser(username = "user", roles = "USER")
public void roles_allowed_denies_non_admin() throws Exception {
mvc.perform(get("/security/rolesallowed"))
.andExpect(status().isForbidden());
}
}
The Spring REST Validation tutorial demonstrates Bean Validation integration with OfficeFloor REST YAML composition.