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>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>3.1.8</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<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;
|
||||
|
||||
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.repositories.users.GroupRepository;
|
||||
import de.avatic.lcc.repositories.users.UserRepository;
|
||||
|
|
@ -77,7 +80,7 @@ public class SecurityConfig {
|
|||
|
||||
@Bean
|
||||
@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
|
||||
.cors(cors -> cors.configurationSource(prodCorsConfigurationSource())) // Production CORS
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
|
|
@ -123,6 +126,7 @@ public class SecurityConfig {
|
|||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||
.csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
|
||||
)
|
||||
.addFilterAfter(authorityRefreshFilter, BearerTokenAuthenticationFilter.class)
|
||||
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
|
||||
.addFilterBefore(
|
||||
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.repositories.users.UserRepository;
|
||||
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 jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
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.users.UserDTO;
|
||||
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 UserTransformer userTransformer;
|
||||
private final UserAuthorityCacheService userAuthorityCacheService;
|
||||
|
||||
public UserService(UserRepository userRepository, UserTransformer userTransformer) {
|
||||
public UserService(UserRepository userRepository, UserTransformer userTransformer, UserAuthorityCacheService userAuthorityCacheService) {
|
||||
this.userRepository = userRepository;
|
||||
this.userTransformer = userTransformer;
|
||||
this.userAuthorityCacheService = userAuthorityCacheService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -42,7 +44,8 @@ public class UserService {
|
|||
* @param user the UserDTO containing updated user information
|
||||
*/
|
||||
public void updateUser(UserDTO user) {
|
||||
userRepository.update(userTransformer.fromUserDTO(user));
|
||||
var userId = userRepository.update(userTransformer.fromUserDTO(user));
|
||||
userAuthorityCacheService.invalidateUserAuthorities(userId);
|
||||
}
|
||||
|
||||
public boolean isSuper() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue