Cara mendapatkan UserDetails pengguna aktif

170

Di pengontrol saya, ketika saya membutuhkan pengguna aktif (login), saya melakukan hal berikut untuk mendapatkan UserDetailsimplementasi saya :

User activeUser = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
log.debug(activeUser.getSomeCustomField());

Ini bekerja dengan baik, tetapi saya akan berpikir Spring dapat membuat hidup lebih mudah dalam kasus seperti ini. Apakah ada cara untuk memiliki UserDetailsautowired ke controller atau metode?

Misalnya, sesuatu seperti:

public ModelAndView someRequestHandler(Principal principal) { ... }

Tapi alih-alih mendapatkan UsernamePasswordAuthenticationToken, saya UserDetailsmalah mendapatkan ?

Saya mencari solusi yang elegan. Ada ide?

The Awnry Bear
sumber

Jawaban:

226

Pembukaan: Sejak Spring-Security 3.2 ada penjelasan bagus yang @AuthenticationPrincipaldijelaskan di akhir jawaban ini. Ini adalah cara terbaik untuk pergi ketika Anda menggunakan Spring-Security> = 3.2.

Ketika anda:

  • menggunakan versi Spring-Security yang lebih lama,
  • perlu memuat Objek Pengguna kustom Anda dari Database dengan beberapa informasi (seperti login atau id) yang disimpan di kepala sekolah atau
  • ingin belajar bagaimana a HandlerMethodArgumentResolveratau WebArgumentResolverdapat menyelesaikan ini dengan cara yang elegan, atau hanya ingin mempelajari latar belakang @AuthenticationPrincipaldan AuthenticationPrincipalArgumentResolver(karena didasarkan pada a HandlerMethodArgumentResolver)

kemudian terus membaca - lain hanya menggunakan @AuthenticationPrincipaldan terima kasih kepada Rob Winch (Penulis @AuthenticationPrincipal) dan Lukas Schmelzeisen (untuk jawabannya).

(BTW: Jawaban saya sedikit lebih tua (Januari 2012), jadi Lukas Schmelzeisen yang muncul sebagai yang pertama dengan @AuthenticationPrincipalbasis solusi anotasi pada Spring Security 3.2.)


Kemudian Anda dapat menggunakan controller Anda

public ModelAndView someRequestHandler(Principal principal) {
   User activeUser = (User) ((Authentication) principal).getPrincipal();
   ...
}

Tidak apa-apa jika Anda membutuhkannya sekali. Tetapi jika Anda membutuhkannya beberapa kali jelek karena itu mencemari controller Anda dengan detail infrastruktur, yang biasanya harus disembunyikan oleh kerangka kerja.

Jadi yang mungkin Anda inginkan adalah memiliki pengontrol seperti ini:

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
   ...
}

Karena itu Anda hanya perlu mengimplementasikan a WebArgumentResolver. Ini memiliki metode

Object resolveArgument(MethodParameter methodParameter,
                   NativeWebRequest webRequest)
                   throws Exception

Itu mendapat permintaan web (parameter kedua) dan harus mengembalikan Userjika merasa bertanggung jawab atas argumen metode (parameter pertama).

Sejak Spring 3.1 ada konsep baru yang disebut HandlerMethodArgumentResolver. Jika Anda menggunakan Spring 3.1+ maka Anda harus menggunakannya. (Dijelaskan di bagian selanjutnya dari jawaban ini))

public class CurrentUserWebArgumentResolver implements WebArgumentResolver{

   Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) {
        if(methodParameter is for type User && methodParameter is annotated with @ActiveUser) {
           Principal principal = webRequest.getUserPrincipal();
           return (User) ((Authentication) principal).getPrincipal();
        } else {
           return WebArgumentResolver.UNRESOLVED;
        }
   }
}

Anda perlu mendefinisikan Anotasi Khusus - Anda dapat mengabaikannya jika setiap instance Pengguna harus selalu diambil dari konteks keamanan, tetapi tidak pernah menjadi objek perintah.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ActiveUser {}

Dalam konfigurasi Anda hanya perlu menambahkan ini:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"
    id="applicationConversionService">
    <property name="customArgumentResolver">
        <bean class="CurrentUserWebArgumentResolver"/>
    </property>
</bean>

@Lihat: Belajar untuk menyesuaikan argumen metode Spring MVC @Controller

Perlu dicatat bahwa jika Anda menggunakan Spring 3.1, mereka merekomendasikan HandlerMethodArgumentResolver melalui WebArgumentResolver. - Lihat komentar oleh Jay


Sama dengan HandlerMethodArgumentResolveruntuk Spring 3.1+

public class CurrentUserHandlerMethodArgumentResolver
                               implements HandlerMethodArgumentResolver {

     @Override
     public boolean supportsParameter(MethodParameter methodParameter) {
          return
              methodParameter.getParameterAnnotation(ActiveUser.class) != null
              && methodParameter.getParameterType().equals(User.class);
     }

     @Override
     public Object resolveArgument(MethodParameter methodParameter,
                         ModelAndViewContainer mavContainer,
                         NativeWebRequest webRequest,
                         WebDataBinderFactory binderFactory) throws Exception {

          if (this.supportsParameter(methodParameter)) {
              Principal principal = webRequest.getUserPrincipal();
              return (User) ((Authentication) principal).getPrincipal();
          } else {
              return WebArgumentResolver.UNRESOLVED;
          }
     }
}

Dalam konfigurasi, Anda perlu menambahkan ini

<mvc:annotation-driven>
      <mvc:argument-resolvers>
           <bean class="CurrentUserHandlerMethodArgumentResolver"/>         
      </mvc:argument-resolvers>
 </mvc:annotation-driven>

@Lihat Memanfaatkan antarmuka Spring MVC 3.1 HandlerMethodArgumentResolver


Spring-Security 3.2 Solusi

Spring Security 3.2 (jangan bingung dengan Spring 3.2) memiliki solusi build in sendiri: @AuthenticationPrincipal( org.springframework.security.web.bind.annotation.AuthenticationPrincipal). Ini dijelaskan dengan baik dalam jawaban Lukas Schmelzeisen

Itu hanya menulis

ModelAndView someRequestHandler(@AuthenticationPrincipal User activeUser) {
    ...
 }

Agar ini berfungsi, Anda harus mendaftarkan AuthenticationPrincipalArgumentResolver( org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver): baik dengan "mengaktifkan" @EnableWebMvcSecurityatau dengan mendaftarkan kacang ini dalam mvc:argument-resolvers- cara yang sama dengan yang saya jelaskan dengan solusi Spring 3.1 di atas.

@Lihat Spring Security 3.2 Referensi, Bab 11.2. @ OtentikasiPrincipal


Solusi Spring-Security 4.0

Ini bekerja seperti solusi Spring 3.2, tetapi di Spring 4.0 @AuthenticationPrincipaldan AuthenticationPrincipalArgumentResolver"dipindahkan" ke paket lain:

(Tapi kelas-kelas lama di paket-paket lamanya masih ada, jadi jangan campur mereka!)

Itu hanya menulis

import org.springframework.security.core.annotation.AuthenticationPrincipal;
ModelAndView someRequestHandler(@AuthenticationPrincipal User activeUser) {
    ...
}

Agar ini berfungsi, Anda harus mendaftarkan ( org.springframework.security.web.method.annotation.) AuthenticationPrincipalArgumentResolver: baik dengan "mengaktifkan" @EnableWebMvcSecurityatau dengan mendaftarkan kacang ini dalam mvc:argument-resolvers- cara yang sama dengan yang saya jelaskan dengan solusi Spring 3.1 di atas.

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
    </mvc:argument-resolvers>
</mvc:annotation-driven>

@Lihat Referensi Spring Security 5.0, Bab 39.3 @ OtentikasiPrincipal

Muntah
sumber
Atau cukup buat kacang yang memiliki metode getUserDetails () dan @Autowire ke dalam pengontrol Anda.
sourcedelica
Menerapkan solusi ini, saya menetapkan breakpoint di bagian atas resolArgument () tetapi aplikasi saya tidak pernah masuk ke resolver argumen web. Konfigurasi pegas Anda dalam konteks servlet bukan konteks root, kan?
Jay
@ Jay: Konfigurasi ini adalah bagian dari konteks root, bukan konteks servlet. - sepertinya saya lupa menentukan id (id="applicationConversionService")pada contoh
Ralph
11
Perlu dicatat bahwa jika Anda menggunakan Spring 3.1, mereka merekomendasikan HandlerMethodArgumentResolver melalui WebArgumentResolver. Saya mendapat HandlerMethodArgumentResolver untuk bekerja dengan mengkonfigurasi dengan <annotation-driven> dalam konteks servlet. Selain itu, saya menerapkan jawaban seperti yang diposting di sini dan semuanya bekerja dengan baik
Jay
@sourcedelica Mengapa tidak membuat metode statis saja?
Alex78191
66

Sementara Ralphs Answer memberikan solusi yang elegan, dengan Spring Security 3.2 Anda tidak perlu lagi mengimplementasikannya sendiri ArgumentResolver.

Jika Anda memiliki UserDetailsimplementasi CustomUser, Anda bisa melakukan ini:

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

    // .. find messages for this User and return them...
}

Lihat Dokumentasi Spring Security: @AuthenticationPrincipal

Lukas Schmelzeisen
sumber
2
bagi mereka yang tidak suka membaca tautan yang disediakan, ini harus diaktifkan oleh @EnableWebMvcSecurityatau dalam XML:<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven>
sodik
Bagaimana cara memeriksa apakah customUseritu nol atau tidak?
Sajad
if (customUser != null) { ... }
T3rm1
27

Spring Security dimaksudkan untuk bekerja dengan kerangka kerja non-Spring lainnya, oleh karena itu ia tidak terintegrasi dengan Spring MVC. Spring Security mengembalikan Authenticationobjek dari HttpServletRequest.getUserPrincipal()metode secara default sehingga itulah yang Anda dapatkan sebagai prinsipal. Anda dapat memperoleh UserDetailsobjek Anda langsung dari ini dengan menggunakan

UserDetails ud = ((Authentication)principal).getPrincipal()

Perhatikan juga bahwa tipe objek dapat bervariasi tergantung pada mekanisme otentikasi yang digunakan (misalnya, Anda mungkin tidak mendapatkan UsernamePasswordAuthenticationToken) dan objek yang Authenticationtidak harus berisi a UserDetails. Ini bisa berupa string atau jenis lainnya.

Jika Anda tidak ingin menelepon SecurityContextHoldersecara langsung, pendekatan yang paling elegan (yang akan saya ikuti) adalah menyuntikkan antarmuka pengakses konteks keamanan kustom Anda sendiri yang disesuaikan dengan kebutuhan dan jenis objek pengguna. Buat antarmuka, dengan metode yang relevan, misalnya:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

Anda kemudian dapat mengimplementasikan ini dengan mengakses SecurityContextHolderimplementasi standar Anda, sehingga memisahkan kode Anda dari Spring Security sepenuhnya. Kemudian menyuntikkan ini ke pengontrol yang membutuhkan akses ke informasi keamanan atau informasi pada pengguna saat ini.

Manfaat utama lainnya adalah mudah untuk membuat implementasi sederhana dengan data tetap untuk pengujian, tanpa harus khawatir tentang populasi thread-lokal dan sebagainya.

Shaun si Domba
sumber
Saya sedang mempertimbangkan pendekatan ini, tetapi saya tidak yakin a) bagaimana tepatnya melakukannya dengan cara yang benar (tm) dan b) jika ada masalah threading. Apakah Anda yakin tidak akan ada masalah di sana? Saya akan menggunakan metode anotasi yang diposting di atas, tetapi saya pikir ini masih merupakan cara yang layak untuk melakukannya. Terima kasih sudah memposting. :)
The Awnry Bear
4
Secara teknis sama dengan mengakses SecurityContextHolder langsung dari controller Anda, jadi seharusnya tidak ada masalah threading. Itu hanya membuat panggilan ke satu tempat dan memungkinkan Anda untuk dengan mudah menyuntikkan alternatif untuk pengujian. Anda juga dapat menggunakan kembali pendekatan yang sama di kelas non-web lainnya yang membutuhkan akses ke informasi keamanan.
Shaun the Sheep
Gotcha ... itulah yang saya pikirkan. Ini akan menjadi yang baik untuk kembali ke jika saya memiliki masalah pengujian unit dengan metode anotasi, atau jika saya ingin mengurangi penggandengan dengan Spring.
The Awnry Bear
@LukeTaylor bisa tolong jelaskan pendekatan ini lebih lanjut, saya sedikit baru untuk keamanan musim semi, jadi saya tidak begitu mengerti bagaimana mengimplementasikan ini. Di mana saya mengimplementasikan ini sehingga akan diakses? ke UserServiceImpl saya?
Cu7l4ss
9

Menerapkan HandlerInterceptorantarmuka, lalu menyuntikkan UserDetailske setiap permintaan yang memiliki Model, sebagai berikut:

@Component 
public class UserInterceptor implements HandlerInterceptor {
    ....other methods not shown....
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if(modelAndView != null){
            modelAndView.addObject("user", (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        }
}
kereta
sumber
1
Terima kasih @rainrain, itu berguna dan elegan. Juga, saya harus menambahkan <mvc:interceptors>file konfigurasi aplikasi saya.
Eric
Lebih baik mendapatkan pengguna resmi di templat melalui spring-security-taglibs: stackoverflow.com/a/44373331/548473
Grigory Kislin
9

Dimulai dengan Spring Security versi 3.2, fungsionalitas khusus yang telah diterapkan oleh beberapa jawaban yang lebih lama, ada di luar kotak dalam bentuk @AuthenticationPrincipalanotasi yang didukung olehAuthenticationPrincipalArgumentResolver .

Contoh sederhana penggunaannya adalah:

@Controller
public class MyController {
   @RequestMapping("/user/current/show")
   public String show(@AuthenticationPrincipal CustomUser customUser) {
        // do something with CustomUser
       return "view";
   }
}

CustomUser perlu ditugaskan dari authentication.getPrincipal()

Berikut adalah Javadocs yang sesuai dari AuthenticationPrincipal dan AuthenticationPrincipalArgumentResolver

geoand
sumber
1
@nbro Ketika saya menambahkan solusi versi spesifik, tidak ada solusi lain yang diperbarui untuk mempertimbangkan solusi ini
geoand
AuthenticationPrincipalArgumentResolver sekarang tidak digunakan lagi
Igor Donin
5
@Controller
public abstract class AbstractController {
    @ModelAttribute("loggedUser")
    public User getLoggedUser() {
        return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}
Kawan
sumber
0

Dan jika Anda membutuhkan pengguna yang berwenang dalam template (misalnya JSP) gunakan

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authentication property="principal.yourCustomField"/>

bersama dengan

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
        <version>${spring-security.version}</version>
    </dependency>
Grigory Kislin
sumber
0

Anda dapat mencoba ini: Dengan Menggunakan Objek Otentikasi dari Spring kita bisa mendapatkan rincian Pengguna dari itu dalam metode controller. Di bawah ini adalah contoh, dengan meneruskan objek Otentikasi dalam metode pengontrol bersama dengan argumen. Setelah pengguna diautentikasi detailnya terisi dalam Objek Otentikasi.

@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}
Mirza Shujathullah
sumber
Tolong jelaskan jawaban Anda.
Nikolai Shevchenko
Saya telah mengedit jawaban saya, kita dapat menggunakan Objek Otentikasi yang memiliki detail pengguna dari pengguna yang telah diautentikasi.
Mirza Shujathullah