Added caching for user authorities and refactored filters:
- Introduced `UserAuthorityCacheService` with support for caching user authorities using Caffeine. - Added `CacheConfig` with Caffeine configuration for authority caching. - Implemented `AuthorityRefreshFilter` to dynamically refresh user roles in the security context. - Refactored filter package structure by moving existing filters to `config.filter`. - Updated `SecurityConfig` to integrate the new `AuthorityRefreshFilter`. - Modified `UserService` to invalidate user authority cache upon user updates.
This commit is contained in:
parent
62e911caf5
commit
a17d50b9a6
9 changed files with 208 additions and 6 deletions
6
pom.xml
6
pom.xml
|
|
@ -177,6 +177,12 @@
|
||||||
<version>4.0.2</version>
|
<version>4.0.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>3.1.8</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
|
||||||
26
src/main/java/de/avatic/lcc/config/CacheConfig.java
Normal file
26
src/main/java/de/avatic/lcc/config/CacheConfig.java
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package de.avatic.lcc.config;
|
||||||
|
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableCaching
|
||||||
|
public class CacheConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CacheManager cacheManager() {
|
||||||
|
CaffeineCacheManager cacheManager = new CaffeineCacheManager("userAuthorities");
|
||||||
|
cacheManager.setCaffeine(Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(5, TimeUnit.MINUTES)
|
||||||
|
.maximumSize(1000)
|
||||||
|
.recordStats());
|
||||||
|
return cacheManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package de.avatic.lcc.config;
|
package de.avatic.lcc.config;
|
||||||
|
|
||||||
|
import de.avatic.lcc.config.filter.AuthorityRefreshFilter;
|
||||||
|
import de.avatic.lcc.config.filter.DevUserEmulationFilter;
|
||||||
|
import de.avatic.lcc.config.filter.SelfIssuedJwtFilter;
|
||||||
import de.avatic.lcc.model.db.users.User;
|
import de.avatic.lcc.model.db.users.User;
|
||||||
import de.avatic.lcc.repositories.users.GroupRepository;
|
import de.avatic.lcc.repositories.users.GroupRepository;
|
||||||
import de.avatic.lcc.repositories.users.UserRepository;
|
import de.avatic.lcc.repositories.users.UserRepository;
|
||||||
|
|
@ -77,7 +80,7 @@ public class SecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("!dev & !test") // Only active when NOT in dev profile
|
@Profile("!dev & !test") // Only active when NOT in dev profile
|
||||||
public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http, JwtTokenService jwtTokenService) throws Exception {
|
public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http, JwtTokenService jwtTokenService, AuthorityRefreshFilter authorityRefreshFilter) throws Exception {
|
||||||
http
|
http
|
||||||
.cors(cors -> cors.configurationSource(prodCorsConfigurationSource())) // Production CORS
|
.cors(cors -> cors.configurationSource(prodCorsConfigurationSource())) // Production CORS
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
|
@ -123,6 +126,7 @@ public class SecurityConfig {
|
||||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||||
.csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
|
.csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
|
||||||
)
|
)
|
||||||
|
.addFilterAfter(authorityRefreshFilter, BearerTokenAuthenticationFilter.class)
|
||||||
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
|
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
|
||||||
.addFilterBefore(
|
.addFilterBefore(
|
||||||
new SelfIssuedJwtFilter(jwtTokenService),
|
new SelfIssuedJwtFilter(jwtTokenService),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
package de.avatic.lcc.config.filter;
|
||||||
|
|
||||||
|
import de.avatic.lcc.config.LccOidcUser;
|
||||||
|
import de.avatic.lcc.service.users.UserAuthorityCacheService;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("!dev & !test")
|
||||||
|
public class AuthorityRefreshFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AuthorityRefreshFilter.class);
|
||||||
|
private static final String AUTHORITIES_CHECKED_ATTR = "authorities_checked";
|
||||||
|
|
||||||
|
private final UserAuthorityCacheService userAuthorityService;
|
||||||
|
|
||||||
|
public AuthorityRefreshFilter(UserAuthorityCacheService userAuthorityService) {
|
||||||
|
this.userAuthorityService = userAuthorityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(@NotNull HttpServletRequest request,
|
||||||
|
@NotNull HttpServletResponse response,
|
||||||
|
@NotNull FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
if (request.getAttribute(AUTHORITIES_CHECKED_ATTR) == null) {
|
||||||
|
refreshAuthorities();
|
||||||
|
request.setAttribute(AUTHORITIES_CHECKED_ATTR, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshAuthorities() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (auth != null && auth.getPrincipal() instanceof LccOidcUser oidcUser) {
|
||||||
|
Integer userId = oidcUser.getSqlUserId();
|
||||||
|
|
||||||
|
if (userId != null) {
|
||||||
|
Set<GrantedAuthority> cachedAuthorities = userAuthorityService.getUserAuthorities(userId);
|
||||||
|
|
||||||
|
if (!authoritiesEqual(cachedAuthorities, oidcUser.getAuthorities())) {
|
||||||
|
log.debug("Authorities changed for user {}, updating SecurityContext", userId);
|
||||||
|
updateSecurityContext(auth, oidcUser, cachedAuthorities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean authoritiesEqual(Set<GrantedAuthority> cached,
|
||||||
|
java.util.Collection<? extends GrantedAuthority> current) {
|
||||||
|
Set<String> cachedRoles = cached.stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.filter(auth -> auth.startsWith("ROLE_"))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Set<String> currentRoles = current.stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.filter(auth -> auth.startsWith("ROLE_"))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
return cachedRoles.equals(currentRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSecurityContext(Authentication auth, LccOidcUser oidcUser,
|
||||||
|
Set<GrantedAuthority> newRoleAuthorities) {
|
||||||
|
Set<GrantedAuthority> updatedAuthorities = new HashSet<>();
|
||||||
|
|
||||||
|
oidcUser.getAuthorities().stream()
|
||||||
|
.filter(authority -> authority.getAuthority().startsWith("SCOPE_"))
|
||||||
|
.forEach(updatedAuthorities::add);
|
||||||
|
|
||||||
|
updatedAuthorities.addAll(newRoleAuthorities);
|
||||||
|
|
||||||
|
LccOidcUser updatedUser = new LccOidcUser(
|
||||||
|
updatedAuthorities,
|
||||||
|
oidcUser.getIdToken(),
|
||||||
|
oidcUser.getUserInfo(),
|
||||||
|
"preferred_username",
|
||||||
|
oidcUser.getSqlUserId()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (auth instanceof OAuth2AuthenticationToken oauthToken) {
|
||||||
|
Authentication newAuth = new OAuth2AuthenticationToken(
|
||||||
|
updatedUser,
|
||||||
|
updatedAuthorities,
|
||||||
|
oauthToken.getAuthorizedClientRegistrationId()
|
||||||
|
);
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(newAuth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package de.avatic.lcc.config;
|
package de.avatic.lcc.config.filter;
|
||||||
|
|
||||||
|
|
||||||
|
import de.avatic.lcc.config.LccOidcUser;
|
||||||
import de.avatic.lcc.model.db.users.User;
|
import de.avatic.lcc.model.db.users.User;
|
||||||
import de.avatic.lcc.repositories.users.UserRepository;
|
import de.avatic.lcc.repositories.users.UserRepository;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package de.avatic.lcc.config;
|
package de.avatic.lcc.config.filter;
|
||||||
import de.avatic.lcc.service.apps.JwtTokenService;
|
import de.avatic.lcc.service.apps.JwtTokenService;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package de.avatic.lcc.controller.dev;
|
package de.avatic.lcc.controller.dev;
|
||||||
|
|
||||||
import de.avatic.lcc.config.DevUserEmulationFilter;
|
import de.avatic.lcc.config.filter.DevUserEmulationFilter;
|
||||||
import de.avatic.lcc.dto.error.CalculationJobDumpDTO;
|
import de.avatic.lcc.dto.error.CalculationJobDumpDTO;
|
||||||
import de.avatic.lcc.dto.users.UserDTO;
|
import de.avatic.lcc.dto.users.UserDTO;
|
||||||
import de.avatic.lcc.repositories.error.DumpRepository;
|
import de.avatic.lcc.repositories.error.DumpRepository;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package de.avatic.lcc.service.users;
|
||||||
|
|
||||||
|
|
||||||
|
import de.avatic.lcc.model.db.users.User;
|
||||||
|
import de.avatic.lcc.repositories.users.UserRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserAuthorityCacheService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(UserAuthorityCacheService.class);
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public UserAuthorityCacheService(UserRepository userRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "userAuthorities", key = "#userId")
|
||||||
|
public Set<GrantedAuthority> getUserAuthorities(Integer userId) {
|
||||||
|
log.debug("Loading authorities from database for user {}", userId);
|
||||||
|
User user = userRepository.getById(userId);
|
||||||
|
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
user.getGroups().forEach(group ->
|
||||||
|
authorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName().toUpperCase()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict(value = "userAuthorities", key = "#userId")
|
||||||
|
public void invalidateUserAuthorities(Integer userId) {
|
||||||
|
log.debug("Invalidating authority cache for user {}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict(value = "userAuthorities", allEntries = true)
|
||||||
|
public void invalidateAllAuthorities() {
|
||||||
|
log.debug("Invalidating entire authority cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,10 +19,12 @@ public class UserService {
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final UserTransformer userTransformer;
|
private final UserTransformer userTransformer;
|
||||||
|
private final UserAuthorityCacheService userAuthorityCacheService;
|
||||||
|
|
||||||
public UserService(UserRepository userRepository, UserTransformer userTransformer) {
|
public UserService(UserRepository userRepository, UserTransformer userTransformer, UserAuthorityCacheService userAuthorityCacheService) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.userTransformer = userTransformer;
|
this.userTransformer = userTransformer;
|
||||||
|
this.userAuthorityCacheService = userAuthorityCacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,7 +44,8 @@ public class UserService {
|
||||||
* @param user the UserDTO containing updated user information
|
* @param user the UserDTO containing updated user information
|
||||||
*/
|
*/
|
||||||
public void updateUser(UserDTO user) {
|
public void updateUser(UserDTO user) {
|
||||||
userRepository.update(userTransformer.fromUserDTO(user));
|
var userId = userRepository.update(userTransformer.fromUserDTO(user));
|
||||||
|
userAuthorityCacheService.invalidateUserAuthorities(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSuper() {
|
public boolean isSuper() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue