Implementing Dynamic Permission Verification in Spring Boot
Background Requirements
Custom login authentication is required where successful authentication generates a token managed by Redis. Post-login, interface-level permission verification must be performed on user-accessed endpoints. Spring Security's annotation-based permission checking is suitable for systems with fixed roles where credentials are static. For example:
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
Here, ROLE_TELLER is hardcoded. Backend system access requests fall into these categories:
- Login/logout endpoints (customizable URLs)
- Interfaces accessible to anonymous users (static resources, demo endpoints)
- Other endpoints requiring authentication and additional permission checks
Environment Setup
Required dependencies include Spring Security, Redis, and Redis Session support:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
Note: Spring Boot version 2.3.4.RELEASE is used. Version compatibility should be verified independently. Swagger is included for testing convenience.
Security Configuration Class
Create SecurityConfiguration.java extending WebSecurityConfigurerAdapter to configure access rules for anonymous endpoints:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/error",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/demo/**", "/about/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/custom-login");
}
}
This configuration allows static resource access without authentication.
Custom Authentication Components
Spring Security uses filter-based authentication. Custom implementations are needed for:
- Authentication Filter: Intercepts login requests and delegates to authentication manager
- Success Handler: Processes successful authentication (returns JSON response)
- Failure Handler: Manages authentication failures
- Authentication Provider: Pefrorms actual credential verification and authorization
Success Handler Implementation
Implement AuthenticationSuccessHandler:
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(LoginSuccessHandler.class);
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication auth) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String jsonResponse = JsonUtils.toJson(Response.success(auth));
if (logger.isDebugEnabled()) {
logger.debug("Authentication successful!");
}
response.getWriter().write(jsonResponse);
}
}
Failure Handler Implementation
Implement AuthenticationFailureHandler:
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
private static final Logger logger = LoggerFactory.getLogger(LoginFailureHandler.class);
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException ex) throws IOException {
String errorMessage = StringUtils.hasText(ex.getMessage()) ?
ex.getMessage() : ErrorCode.AUTH_FAILED.getMessage();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String jsonResponse = JsonUtils.toJson(Response.failure(ErrorCode.AUTH_FAILED, errorMessage));
if (logger.isDebugEnabled()) {
logger.debug("Authentication failed!");
}
response.getWriter().write(jsonResponse);
}
}
Authentication Provider Implementation
Implement AuthenticationProvider for database-based verification:
@Component
public class DatabaseAuthenticationProvider implements AuthenticationProvider {
private PasswordEncoder encoder;
@Autowired
private UserService userService;
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
String username = (String) auth.getPrincipal();
String password = (String) auth.getCredentials();
UserWithRoles userData = userService.findUserWithRolesByUsername(username);
String storedPassword = userData.getPassword();
String decodedPassword = new String(Base64.getDecoder().decode(password), StandardCharsets.UTF_8);
encoder = new MD5PasswordEncoder();
if (!encoder.matches(decodedPassword, storedPassword)) {