Fork me on GitHub

Spring Security Integration Tutorial

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 required
  • GET /security/me — requires authentication; injects the current user via @AuthenticationPrincipal
  • GET /security/roles — requires authentication; reads the user's granted authorities
  • GET /security/auth — requires authentication; receives Authentication as a plain method parameter
  • GET /security/bean — requires authentication; receives a Spring Security bean (UserDetailsService) by type
  • GET /security/preauthorize — requires ADMIN role, enforced by @PreAuthorize
  • GET /security/secured — requires ADMIN role, enforced by @Secured
  • GET /security/rolesallowed — requires ADMIN role, enforced by @RolesAllowed

Tutorial Source

Maven dependencies

The 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>

Application class

@SpringBootApplication
public class SpringRestSecurityApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestSecurityApplication.class, args);
	}
}

Security configuration

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.

Public endpoint

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!");
	}
}

Accessing the authenticated user — @AuthenticationPrincipal

For 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.

Reading granted authorities

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);
	}
}

Receiving Authentication as a direct parameter

The 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());
	}
}

Injecting Spring Security beans as method parameters

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.

Method-level security — @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.

Method-level security — @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");
	}
}

Method-level security — @RolesAllowed

The 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");
	}
}

Testing

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());
	}
}

Next

The Spring REST Validation tutorial demonstrates Bean Validation integration with OfficeFloor REST YAML composition.