Spring: Scope für Persistencer Bean (OpenJPA) in einem User-Rechte-System

Hey Leute,

Spring unterstützt ja von Hause aus die Scopes singleton, prototype, request, session und global-session (http://www.tutorialspoint.com/spring/spring_bean_scopes.htm).

Angenommen, ich habe ein User-Rechte-System, welches ich mit Spring Security realisiert habe. Die User-Daten werden mittels OpenJPA aus der Datenbank geholt, welche mittels eines Beans in Spring realisiert ist. Welchen von den oben genannten Scopes sollte nun dieser Bean haben, wenn ich sicherstellen möchte, dass es pro Benutzer nur eine Instanz geben soll? Wenn der Benutzer eingeloggt ist, soll beispielsweise gezählt werden, wie oft dieser eine bestimmte Aktion macht. Es soll sichergestellt werden, dass wenn der Benutzer auf mehreren Rechnern eingeloggt ist oder “zu schnell” Aktionen durchführt, dass immer noch die richtige Anzahl an Zugriffen ermittelt und geschrieben wird in die Datenbank.

Grüße
Dimash

Beschreib mal das System bessrr.
WebApp, JEE Server, usw. ?

Mindestens Prototype wenn nicht gar Session.

@maki :
Eine SpringMVC Anwendung, also mit einer Web-Oberfläche. Benutzer loggt sich über die Web-Oberfläche ein und kann dort nach bestimmten Dokumenten suchen. Diese Suche ist mittels @RequestMapping mit dem Backend verbunden, welches wiederum auf eine Lucene Architektur basiert. Innerhalb des Suchvorgangs wird auf den Persistencer Bean zugegriffen, welcher die Anzahl der Suchanfragen in die Datenbank abspeichert.
Intuitiv hätte ich auch auf den Scope session getippt. Wo wäre der Unterschied zu global session?

Versuchst du da gerade Spring Security nachzubauen?

@cmrudolph :
Spring Security wird von mir ebenfalls verwendet. Ich will jedoch zusätzlich pro Benutzer loggen können, wer wie oft die Suche benutzt.

Habe in deinem Ausgangspost übersehen, dass du bereits Spring Security nutzt.
Du gehst den falschen Weg. Du musst einen User-Provider implementieren.
Bei mir sieht das etwa so aus:

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class JpaUserDetailsManager implements UserDetailsManager {
    private final CustomUserRepository userRepository;
    @Autowired(required = false)
    private AuthenticationManager authenticationManager;

    @Autowired
    public JpaUserDetailsManager(CustomUserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void createUser(UserDetails user) {
        checkType(user);
        userRepository.save((CustomUser) user);
    }

    @Override
    public void updateUser(UserDetails user) {
        checkType(user);
        userRepository.save((CustomUser) user);
    }

    @Override
    public void deleteUser(String username) {
        userRepository.deleteByUsername(username);
    }

    @Transactional
    @Override
    public void changePassword(String oldPassword, String newPassword) throws AuthenticationException {
        final Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();

        if (currentUser == null)
            throw new AccessDeniedException("Can't change password as no Authentication object found in context " +
                    "for current user.");

        final String username = currentUser.getName();

        if (authenticationManager != null)
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword));

        final CustomUser user = userRepository.findByUsername(username);
        user.setPassword(newPassword);
        userRepository.save(user);
    }

    @Override
    public boolean userExists(String username) {
        return userRepository.findByUsername(username) != null;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        final CustomUser user = userRepository.findByUsername(username);
        if (user == null) throw new UsernameNotFoundException("user not found");
        return user;
    }

    private void checkType(UserDetails user) {
        if (!(user instanceof CustomUser)) throw new IllegalArgumentException("user must be a CustomUser");
    }
}```
```import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.annotation.Nonnull;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;

@Entity
public class CustomUser extends AbstractEarlyIdPersistable<Long> implements UserDetails {
    private static final long serialVersionUID = 7348432542539499181L;
    @Column(unique = true, nullable = false, length = 32)
    @Size(min = 3, max = 32)
    @NotNull
    private String username;
    @NotNull
    @Size(max = 60)
    @Column(nullable = false, length = 60)
    private String password;
    private LocalDateTime expirationDate = null;
    @NotNull
    private boolean enabled = true;
    @NotNull
    private boolean locked = false;
    @NotNull
    @ElementCollection(fetch = FetchType.EAGER)
    @Enumerated(EnumType.STRING)
    @JoinColumn(nullable = false)
    private Set<UserRole> authorities = EnumSet.noneOf(UserRole.class);
    @OneToOne
    private Party assignedParty;

    protected CustomUser() {
    }

    CustomUser(long id, @Nonnull String username, @Nonnull String password) {
        super(id);
        this.username = checkNotNull(username);
        this.password = checkNotNull(password);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.unmodifiableSet(authorities);
    }

    public void grantAuthority(@Nonnull UserRole role) {
        authorities.add(checkNotNull(role));
    }

    public void revokeAuthority(@Nonnull UserRole role) {
        authorities.remove(checkNotNull(role));
    }

    public boolean hasAuthority(@Nonnull UserRole role) {
        return authorities.contains(checkNotNull(role));
    }

    @Override
    @Nonnull
    public String getPassword() {
        return password;
    }

    public void setPassword(@Nonnull String password) {
        this.password = checkNotNull(password);
    }

    @Override
    @Nonnull
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return expirationDate == null
                || expirationDate.isAfter(LocalDateTime.now());
    }

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}```
```public interface CustomUserRepository extends JpaRepository<CustomUser, Long> {
    void deleteByUsername(String username);

    CustomUser findByUsername(String username);
}```

*** Edit ***

Den `CustomUser` kannst du jetzt anpassen und die benötigten Daten speichern. Auf das User-Objekt kannst du mit Spring Security über den SecurityContext zugreifen. (`SecurityContextHolder.getContext().getAuthentication()` müsste das sein)

*** Edit ***

Zur Frage mit dem global session scope ein Zitat aus der [Referenzdokumentation](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-factory-scopes-global-session):

> The `global session` scope is similar to the standard HTTP `Session` scope ([described above](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-factory-scopes-session)), and applies only in the context of portlet-based web applications. The portlet specification defines the notion of a global `Session` that is shared among all portlets that make up a single portlet web application. Beans defined at the `global session` scope are scoped (or bound) to the lifetime of the global portlet `Session`.

@cmrudolph :
Danke für deine ausführliche Antwort. Ich habe es ähnlich implementiert, außer, dass ich nicht UserDetailsManager implementiert habe, sondern UserDetailsService.

Vielleicht habe ich mich etwas falsch ausgedrückt. Bezüglich Autorisierung scheint es ja von Spring Security schon geregelt zu sein.
Meine Frage ist folgende (diesmal mit bisschen Code hinterlegt):

SearchSystem:

	@Autowired CustomPersistencer persistencer;
	@RequestMapping(value = { "/members/search" }, method = RequestMethod.GET)
	public void search(HttpServletRequest request, HttpServletResponse response) {
		JSONObject result = buildSearchResult(searchRequestData);
		sendResultObjectToClient(result, response);
		persistencer.countAccess();
	}

Und der dazugehörige Bean:

	@Bean
	public CustomPersistencer getPersistencer() {
		return new CustomPersistencer();
	}

Angenommen, countAccess() erhöht für den jeweiligen Benutzer die Anzahl der Zugriffe und schreibt das in die Datenbank. Es soll nur eine Instanz des CustomPersistencer pro eingeloggten Benutzer vorhanden sein. Welchen Scope nehme ich da jetzt?

Ok, ich glaube mein Ansatz war etwas vorschnell. Die Daten direkt an das “Nutzerprofil” zu binden, passt nicht so richtig, weil die Daten wohl nicht so eng damit gekoppelt sind.
Die Session erscheint mir der beste Gültigkeitsbereich. Die Session überdauert aber den Login eines Benutzers, sodass du dich um Initialisierung beim Login / Aufräumen beim Logout selbst kümmern musst. Problematisch könnte es auch werden, wenn derselbe Nutzer mehrfach eingeloggt ist (wobei ich auch nicht weiß, ob es in diesem Fall dann mehrere UserDetails-Instanzen vom selben Nutzer gibt).

ausgrab
Ich würde den Zähler direkt im CustomUser speichern. Z.b. jeder User erzeugt bei Kontruktion 1 Zählerobjekt, zum zählen musst du über den User drauf zugreifen. Session Scope geht nicht, da ein User mehrere Sessions gleichzeitig haben kann.

Um immer auf Objekte mit “User Scope” zugreifen zu können, setze ich die Referenz auf den eingeloggten User in meiner Session-scoped Klasse. Ein Objekt dieser Klasse wire ich dann in alle Controller welche Login voraussetzen, sodass immer ein eingeloggter User von der Session gefunden werden kann. Session Scoped Objekte musst du mit ScopedProxyMode wiren.