ChatGPT解决这个技术问题 Extra ChatGPT

How to reload authorities on user update with Spring Security

I'm doing an application with authentication by OpenID using Spring Security. When user is logged-in, some authorities are loaded in his session.

I have User with full right which can modify authorities (revoke, add roles) of others users. My question is, how to change User session authorities dynamically ? (cannot use SecurityContextHolder because I want to change another User session).

Simple way : invalidate user session, but how to ? Better way : refresh user session with new authorities, but how to ?


v
vphilipnyc

If you need to dynamically update a logged in user's authorities (when these have changed, for whatever reason), without having to log out and log in of course, you just need to reset the Authentication object (security token) in the Spring SecurityContextHolder.

Example:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

List<GrantedAuthority> updatedAuthorities = new ArrayList<>(auth.getAuthorities());
updatedAuthorities.add(...); //add your role here [e.g., new SimpleGrantedAuthority("ROLE_NEW_ROLE")]

Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);

SecurityContextHolder.getContext().setAuthentication(newAuth);

Well, it almost worked for me. This "auth" variable is concerned the logged in user (that is, me). If I am logged in as "x" and I'd like to revoke "y" authorities, how do I get Authentication's object from that specific user?
This only works for the current user. How to achieve this for another user?
I am confused why this answer has so many upvotes: it doesn't fully answer the question which clearly states that it is necessary to change the data of another user.
A
Aure77

Thanks, help me a lot ! With SessionRegistry, I can use getAllPrincipals() to compare the user to modify with the current active users in sessions. If a session exist, I can invalidate his session using : expireNow() (from SessionInformation) to force re-authentication.

But I don't understand the usefulness of securityContextPersistenceFilter ?

EDIT :

// user object = User currently updated
// invalidate user session
List<Object> loggedUsers = sessionRegistry.getAllPrincipals();
for (Object principal : loggedUsers) {
    if(principal instanceof User) {
        final User loggedUser = (User) principal;
        if(user.getUsername().equals(loggedUser.getUsername())) {
            List<SessionInformation> sessionsInfo = sessionRegistry.getAllSessions(principal, false);
            if(null != sessionsInfo && sessionsInfo.size() > 0) {
                for (SessionInformation sessionInformation : sessionsInfo) {
                    LOGGER.info("Exprire now :" + sessionInformation.getSessionId());
                    sessionInformation.expireNow();
                    sessionRegistry.removeSessionInformation(sessionInformation.getSessionId());
                    // User is not forced to re-logging
                }
            }
        }
    }
} 

securityContextPersistenceFilter by default will put SecurityContext into HttpSession in servlet environment. As you already have out-of-the-box spring SessionRegistry you do not need to customize this filter.
I am in servlet environment, what is the usefulness of customizing securityContextPersistenceFilter ?
different cases possible, e.g. HttpSessions are disabled and you don't want thread-local storage. So you can use your own implementation of securityContextRepository. If HttpSession storage suit your needs, then there is no usefulness.
I am using the code above (see EDIT) to invalidate user session. But I have a problem, user is not forced to re-logging... I think that the SecurityContextHolder is not cleared for this User. How can I perform that ?
SecurityContext for each user lies in each user's session, see details here. If you can access other user's session through registry then you can do what you want with it.
T
TwiN

If anybody is still looking into how to update the authorities of another user without forcing that user to re-authenticate, you can try to add an interceptor that reloads the authentication. This will make sure that your authorities are always updated.

However -- due to the extra interceptor, there will be some performance impacts (e.g. if you get your user roles from your database, it will be queried for every HTTP request).

@Component
public class VerifyAccessInterceptor implements HandlerInterceptor {

    // ...

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        Set<GrantedAuthority> authorities = new HashSet<>();
        if (auth.isAuthenticated()) {
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        }
    
        User userFromDatabase = getUserFromDatabase(auth.getName());
        if (userFromDatabase != null) {
            // add whatever authorities you want here
            authorities.add(new SimpleGrantedAuthority("...")); 
        }
    
        Authentication newAuth = null;
    
        if (auth.getClass() == OAuth2AuthenticationToken.class) {
            OAuth2User principal = ((OAuth2AuthenticationToken)auth).getPrincipal();
            if (principal != null) {
                newAuth = new OAuth2AuthenticationToken(principal, authorities,(((OAuth2AuthenticationToken)auth).getAuthorizedClientRegistrationId()));
            }
        }
    
        SecurityContextHolder.getContext().setAuthentication(newAuth);
        return true;
    }

}

This specific implementation uses OAuth2 (OAuth2AuthenticationToken), but you can use UsernamePasswordAuthenticationToken instead.

And now, to add your interceptor to the configuration:

@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private VerifyAccessInterceptor verifyAccessInterceptor;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(verifyAccessInterceptor).addPathPatterns("/**");
    }

}

I also made an article about this.


M
Marvo

The key point - you should be able to access users SecurityContexts.

If you are in servlet environment and are using HttpSession as securityContextRepository in your securityContextPersistenceFilter, then it can be done with spring's SessionRegistry. To force the user to re-auth (it should be better than silent permissions revocation) invalidate his HttpSession. Don't forget to add HttpSessionEventPublisher to web.xml

<listener>
    <listener-class>
        org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener>

If you are using thread-local securityContextRepository, then you should add custom filter to springSecurityFilterChain to manage SecurityContexts registry. To do this you must the use plain-bean springSecurityFilterChain configuration (without security namespace shortcuts). With plain-bean config with custom filters you'll have full control on authentication and authorization.

Some links, they don't solve exactly your problem (no OpenID), but may be useful:

NIH session registry for servlet environment

it's plain-bean spring config working example

real life plain-bean spring config for X.509 auth, you may start with it and modify it to use OpenID instead of X.509.


H
Hasler Choo

I use the answer gived by TwiN, but I create a control variable (users_to_update_roles) to reduce performance impacts.

@Component
public class RoleCheckInterceptor implements HandlerInterceptor {
public static ArrayList<String> update_role = new ArrayList<>();

@Autowired
private IUser iuser;

public static Set<String> users_to_update_roles = new HashSet<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {

    Authentication auth = SecurityContextHolder.getContext().getAuthentication();

    try {

        CurrentUser current = (CurrentUser) auth.getPrincipal();

        String username = current.getUser().getUsername();
        if (users_to_update_roles.contains(username)) {
            updateRoles(auth, current);
            users_to_update_roles.remove(username);
        }

    } catch (Exception e) {
        // TODO: handle exception
    }

    return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
        ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
        throws Exception {

}

private void updateRoles(Authentication auth, CurrentUser current) {
    User findOne = iuser.findOne(current.getUser().getUsername());
    List<GrantedAuthority> updatedAuthorities = new ArrayList<>();
    for (Role role : findOne.getRoles()) {
        updatedAuthorities.add(new SimpleGrantedAuthority(role.name()));
    }

    Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(),
            updatedAuthorities);

    SecurityContextHolder.getContext().setAuthentication(newAuth);
}
}

and in my controller, I add the user that have they role updated

    public ModelAndView roleSave(@PathVariable long numero_documento, Funcionario funcionario) {
    ModelAndView modelAndView = new ModelAndView("funcionario/role");
    Set<Role> roles = funcionario.getPessoa().getUser().getRoles();
    funcionario = funcionarioService.funcionarioNumero_documento(numero_documento);
    funcionario.getPessoa().getUser().setRoles(roles);
    iUser.save(funcionario.getPessoa().getUser());
    RoleCheckInterceptor.users_to_update_roles.add(funcionario.getPessoa().getUser().getUsername());
    modelAndView.addObject("funcionario", funcionario);
    modelAndView.addObject("sucess", "Permissões modificadas");
    return modelAndView;
}

I like your idea, but there is a race condition on users_to_update_roles. Synchronizing on the Set (which should be a ConcurrentHashSet if accessed like this) would work, but introduces a different problem.
@RüdigerSchulz do you have a good solution / example code?
@RüdigerSchulz what if you use ConcurrentHashMap that maps username (or better user ID) to Long and put the value of AtomicLong.incrementAndGet() in it when you change, and instead of remove by ID in preHandle you use removeIf(value -> value.equals(valueRetrievedAtTheStartOfPreHandle))?
s
snieguu

I have a very specific case of above, I use Redis to track user session with https://github.com/spring-projects/spring-session. Then when admin adds some Role to the user I find user session in Redis and replace principal and authorities and then save the session.

public void updateUserRoles(String username, Set<GrantedAuthority> newRoles) {
        if (sessionRepository instanceof FindByIndexNameSessionRepository) {
            Map<String, org.springframework.session.Session> map =
                    ((FindByIndexNameSessionRepository<org.springframework.session.Session>) sessionRepository)
                            .findByPrincipalName(username);
            for (org.springframework.session.Session session : map.values()) {
                if (!session.isExpired()) {
                    SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
                    Authentication authentication = securityContext.getAuthentication();
                    if (authentication instanceof UsernamePasswordAuthenticationToken) {
                        Collection<GrantedAuthority> authorities = new HashSet<>(authentication.getAuthorities());
                        //1. Update of authorities
                        authorities.addAll(newRoles);
                        Object principalToUpdate = authentication.getPrincipal();
                        if (principalToUpdate instanceof User) {
                            //2. Update of principal: Your User probably extends UserDetails so call here method that update roles to allow
                            // org.springframework.security.core.userdetails.UserDetails.getAuthorities return updated 
                            // Set of GrantedAuthority
                            securityContext
                                    .setAuthentication(new UsernamePasswordAuthenticationToken(principalToUpdate, authentication
                                            .getCredentials(), authorities));
                            session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, securityContext);
                            sessionRepository.save(session);
                        }
                    }
                }
            }
        }
    }

Thx a lot! Searched for days now!