Saat menggunakan Spring Security, apa cara yang tepat untuk mendapatkan informasi nama pengguna saat ini (yaitu SecurityContext) dalam sebuah kacang?

288

Saya memiliki aplikasi web Spring MVC yang menggunakan Spring Security. Saya ingin tahu nama pengguna pengguna yang saat ini masuk. Saya menggunakan cuplikan kode yang diberikan di bawah ini. Apakah ini cara yang diterima?

Saya tidak suka memiliki panggilan ke metode statis di dalam controller ini - yang mengalahkan seluruh tujuan Spring, IMHO. Apakah ada cara untuk mengkonfigurasi aplikasi agar SecurityContext saat ini, atau Otentikasi saat ini, disuntikkan sebagai gantinya?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }
Scott Bale
sumber
Mengapa Anda tidak memiliki pengontrol (pengontrol aman) sebagai kelas super mendapatkan pengguna dari SecurityContext dan mengaturnya sebagai variabel instan di dalam kelas itu? Dengan cara ini ketika Anda memperluas pengontrol aman, seluruh kelas Anda akan memiliki akses ke pokok Pengguna konteks saat ini.
Dehan de Croos

Jawaban:

259

Jika Anda menggunakan Spring 3 , cara termudah adalah:

 @RequestMapping(method = RequestMethod.GET)   
 public ModelAndView showResults(final HttpServletRequest request, Principal principal) {

     final String currentUser = principal.getName();

 }
tsunade21
sumber
69

Banyak yang telah berubah di dunia Musim Semi sejak pertanyaan ini dijawab. Spring telah menyederhanakan mendapatkan pengguna saat ini di controller. Untuk kacang lainnya, Spring telah mengadopsi saran dari penulis dan menyederhanakan injeksi 'SecurityContextHolder'. Lebih detail ada di komentar.


Ini adalah solusi yang akhirnya saya pakai. Alih-alih menggunakan SecurityContextHolderdi controller saya, saya ingin menyuntikkan sesuatu yang menggunakan di SecurityContextHolderbawah tenda tetapi abstrak jauh seperti kelas singleton dari kode saya. Saya tidak menemukan cara untuk melakukan ini selain memutar antarmuka saya sendiri, seperti:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Sekarang, controller saya (atau POJO apa pun) akan terlihat seperti ini:

public class FooController {

  private final SecurityContextFacade securityContextFacade;

  public FooController(SecurityContextFacade securityContextFacade) {
    this.securityContextFacade = securityContextFacade;
  }

  public void doSomething(){
    SecurityContext context = securityContextFacade.getContext();
    // do something w/ context
  }

}

Dan, karena antarmuka menjadi titik decoupling, pengujian unit mudah. Dalam contoh ini saya menggunakan Mockito:

public class FooControllerTest {

  private FooController controller;
  private SecurityContextFacade mockSecurityContextFacade;
  private SecurityContext mockSecurityContext;

  @Before
  public void setUp() throws Exception {
    mockSecurityContextFacade = mock(SecurityContextFacade.class);
    mockSecurityContext = mock(SecurityContext.class);
    stub(mockSecurityContextFacade.getContext()).toReturn(mockSecurityContext);
    controller = new FooController(mockSecurityContextFacade);
  }

  @Test
  public void testDoSomething() {
    controller.doSomething();
    verify(mockSecurityContextFacade).getContext();
  }

}

Implementasi default antarmuka terlihat seperti ini:

public class SecurityContextHolderFacade implements SecurityContextFacade {

  public SecurityContext getContext() {
    return SecurityContextHolder.getContext();
  }

  public void setContext(SecurityContext securityContext) {
    SecurityContextHolder.setContext(securityContext);
  }

}

Dan, akhirnya, konfigurasi Spring produksi terlihat seperti ini:

<bean id="myController" class="com.foo.FooController">
     ...
  <constructor-arg index="1">
    <bean class="com.foo.SecurityContextHolderFacade">
  </constructor-arg>
</bean>

Tampaknya lebih dari sedikit konyol bahwa Spring, wadah injeksi ketergantungan dari semua hal, belum menyediakan cara untuk menyuntikkan sesuatu yang serupa. Saya mengerti SecurityContextHolderdiwarisi dari acegi, tapi tetap saja. Masalahnya, mereka sangat dekat - jika hanya SecurityContextHoldermemiliki pengambil untuk mendapatkan SecurityContextHolderStrategycontoh yang mendasarinya (yang merupakan antarmuka), Anda bisa menyuntikkan itu. Bahkan, saya bahkan membuka masalah Jira untuk efek itu.

Satu hal terakhir - Saya baru saja mengubah jawaban yang saya miliki di sini sebelumnya. Periksa riwayatnya jika Anda penasaran, tetapi, seperti yang ditunjukkan oleh seorang rekan kerja kepada saya, jawaban saya sebelumnya tidak akan berfungsi di lingkungan multi-utas. Yang mendasari yang SecurityContextHolderStrategydigunakan oleh SecurityContextHolderadalah, secara default, instance dari ThreadLocalSecurityContextHolderStrategy, yang menyimpan SecurityContexts dalam ThreadLocal. Oleh karena itu, tidak selalu merupakan ide yang baik untuk menyuntikkan SecurityContextlangsung ke dalam kacang pada waktu inisialisasi - mungkin perlu diambil dari ThreadLocalsetiap waktu, dalam lingkungan multi-threaded, sehingga yang benar diambil.

Scott Bale
sumber
1
Saya suka solusi Anda - ini adalah penggunaan yang cerdas dari dukungan metode pabrik di Spring. Yang mengatakan, ini bekerja untuk Anda karena objek pengontrol mencakup untuk permintaan web. Jika Anda mengubah lingkup kacang pengontrol dengan cara yang salah, ini akan rusak.
Paul Morie
2
Dua komentar sebelumnya merujuk pada jawaban lama dan salah yang baru saja saya ganti.
Scott Bale
12
Apakah ini masih solusi yang disarankan dengan rilis Spring saat ini? Saya tidak percaya perlu begitu banyak kode hanya untuk mengambil hanya nama pengguna.
Ta Sas
6
Jika Anda menggunakan Spring Security 3.0.x, mereka menerapkan saran saya dalam masalah JIRA yang saya masuki jira.springsource.org/browse/SEC-1188 sehingga Anda sekarang dapat menyuntikkan instance SecurityContextHolderStrategy (dari SecurityContextHolder) langsung ke bean Anda melalui standar Konfigurasi pegas.
Scott Bale 4-10
4
Silakan lihat jawaban tsunade21. Spring 3 sekarang memungkinkan Anda untuk menggunakan java.security.Principal sebagai argumen metode di controller Anda
Patrick
22

Saya setuju bahwa harus meminta SecurityContext untuk pengguna saat ini bau, tampaknya cara yang sangat un-Spring untuk menangani masalah ini.

Saya menulis kelas "pembantu" statis untuk menangani masalah ini; itu kotor karena itu adalah metode global dan statis, tapi saya pikir cara ini jika kita mengubah apa pun yang terkait dengan Keamanan, setidaknya saya hanya perlu mengubah detail di satu tempat:

/**
* Returns the domain User object for the currently logged in user, or null
* if no User is logged in.
* 
* @return User object for the currently logged in user, or null if no User
*         is logged in.
*/
public static User getCurrentUser() {

    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()

    if (principal instanceof MyUserDetails) return ((MyUserDetails) principal).getUser();

    // principal object is either null or represents anonymous user -
    // neither of which our domain User object can represent - so return null
    return null;
}


/**
 * Utility method to determine if the current user is logged in /
 * authenticated.
 * <p>
 * Equivalent of calling:
 * <p>
 * <code>getCurrentUser() != null</code>
 * 
 * @return if user is logged in
 */
public static boolean isLoggedIn() {
    return getCurrentUser() != null;
}
matt b
sumber
22
itu sepanjang SecurityContextHolder.getContext () adalah, dan yang terakhir adalah threadsafe karena menjaga detail keamanan di threadLocal. Kode ini tidak memiliki status apa pun.
matt b
22

Untuk membuatnya hanya muncul di halaman JSP Anda, Anda dapat menggunakan Spring Security Tag Lib:

http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html

Untuk menggunakan salah satu tag, Anda harus mendeklarasikan taglib keamanan di JSP Anda:

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

Kemudian di halaman jsp lakukan sesuatu seperti ini:

<security:authorize access="isAuthenticated()">
    logged in as <security:authentication property="principal.username" /> 
</security:authorize>

<security:authorize access="! isAuthenticated()">
    not logged in
</security:authorize>

CATATAN: Seperti yang disebutkan dalam komentar oleh @ SBerg413, Anda harus menambahkan

use-expressions = "true"

ke tag "http" di konfigurasi security.xml agar ini berfungsi.

Taman Brad
sumber
Sepertinya ini mungkin cara yang disetujui Spring Security!
Nick Spacek
3
Agar metode ini berfungsi, Anda perlu menambahkan use-expressions = "true" ke tag http di config security.xml.
SBerg413
Terima kasih @ SBerg413, saya akan mengedit jawaban saya dan menambahkan klarifikasi penting Anda!
Brad Parks
14

Jika Anda menggunakan Spring Security ver> = 3.2, Anda dapat menggunakan @AuthenticationPrincipalanotasi:

@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) {
    String currentUsername = currentUser.getUsername();
    // ...
}

Di sini, CustomUseradalah objek kustom yang mengimplementasikan UserDetailsyang dikembalikan oleh kebiasaan UserDetailsService.

Informasi lebih lanjut dapat ditemukan di bab @AuthenticationPrincipal dari dokumen referensi Spring Security.

matsev
sumber
13

Saya mendapatkan pengguna terotentikasi oleh HttpServletRequest.getUserPrincipal ();

Contoh:

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.support.RequestContext;

import foo.Form;

@Controller
@RequestMapping(value="/welcome")
public class IndexController {

    @RequestMapping(method=RequestMethod.GET)
    public String getCreateForm(Model model, HttpServletRequest request) {

        if(request.getUserPrincipal() != null) {
            String loginName = request.getUserPrincipal().getName();
            System.out.println("loginName : " + loginName );
        }

        model.addAttribute("form", new Form());
        return "welcome";
    }
}
digz6666
sumber
Saya suka solusi Anda. Untuk spesialis Spring: Apakah itu solusi yang aman dan bagus?
marioosh
Bukan solusi yang bagus. Anda akan mendapatkan nulljika pengguna diautentikasi secara anonim ( http> anonymouselemen di Spring Security XML). SecurityContextHolderatau SecurityContextHolderStrategycara yang tepat.
Sekarang lemah
1
Sehingga saya sudah mengecek jika tidak null request.getUserPrincipal ()! = Null.
digz6666
Is null in Filter
Alex78191
9

Di Musim Semi 3+ Anda memiliki opsi berikut.

Pilihan 1 :

@RequestMapping(method = RequestMethod.GET)    
public String currentUserNameByPrincipal(Principal principal) {
    return principal.getName();
}

Pilihan 2 :

@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByAuthentication(Authentication authentication) {
    return authentication.getName();
}

Opsi 3:

@RequestMapping(method = RequestMethod.GET)    
public String currentUserByHTTPRequest(HttpServletRequest request) {
    return request.getUserPrincipal().getName();

}

Opsi 4: Fancy satu: Lihat ini untuk lebih jelasnya

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}
Tanah pertanian
sumber
1
Sejak 3.2, spring-security-web hadir dengan @CurrentUseryang berfungsi seperti kebiasaan @ActiveUserdari tautan Anda.
Mike Partridge
@ MikePridgeridge, sepertinya saya tidak menemukan apa yang Anda katakan, ada tautan ?? atau info lebih lanjut ??
azerafati
1
Kesalahan saya - saya salah mengerti javadoc Spring Security AuthenticationPrincipalArgumentResolver . Contoh ditampilkan di sana, dibungkus @AuthenticationPrincipaldengan @CurrentUseranotasi khusus . Karena 3.2 kita tidak perlu mengimplementasikan pemecah argumen khusus seperti pada jawaban yang ditautkan. Jawaban lain ini lebih detail.
Mike Partridge
5

Ya, statika umumnya buruk - umumnya, tetapi dalam kasus ini, statis adalah kode paling aman yang dapat Anda tulis. Karena konteks keamanan mengaitkan Principal dengan utas yang saat ini berjalan, kode yang paling aman akan mengakses statis dari utas secara langsung. Menyembunyikan akses di belakang kelas pembungkus yang disuntikkan memberikan penyerang poin lebih banyak untuk diserang. Mereka tidak memerlukan akses ke kode (yang akan sulit diubah jika toples ditandatangani), mereka hanya perlu cara untuk mengesampingkan konfigurasi, yang dapat dilakukan saat runtime atau memasukkan beberapa XML ke classpath. Bahkan menggunakan injeksi anotasi dalam kode yang ditandatangani akan ditimpa dengan XML eksternal. XML semacam itu dapat menyuntikkan sistem yang sedang berjalan dengan prinsip jahat.

Michael Bushe
sumber
5

Saya hanya akan melakukan ini:

request.getRemoteUser();
Dan
sumber
1
Itu mungkin berhasil, tetapi tidak andal. Dari javadoc: "Apakah nama pengguna dikirim dengan setiap permintaan berikutnya tergantung pada browser dan jenis otentikasi." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
Scott Bale
3
Ini sebenarnya cara yang valid dan sangat sederhana untuk mendapatkan nama pengguna jarak jauh di aplikasi web Spring Security. Rantai filter standar mencakup SecurityContextHolderAwareRequestFilteryang membungkus permintaan dan mengimplementasikan panggilan ini dengan mengakses SecurityContextHolder.
Shaun the Sheep
4

Untuk aplikasi Spring MVC terakhir yang saya tulis, saya tidak menyuntikkan pemegang SecurityContext, tetapi saya memang memiliki basis controller yang saya punya dua metode utilitas yang terkait dengan ini ... isAuthenticated () & getUsername (). Secara internal mereka melakukan panggilan metode statis yang Anda jelaskan.

Setidaknya itu hanya di satu tempat jika Anda perlu refactor nanti.

RichH
sumber
3

Anda bisa menggunakan pendekatan Spring AOP. Misalnya jika Anda memiliki beberapa layanan, yang perlu diketahui kepala sekolah saat ini. Anda dapat memperkenalkan anotasi khusus yaitu @Principal, yang menunjukkan bahwa Layanan ini harus bergantung pada prinsipal.

public class SomeService {
    private String principal;
    @Principal
    public setPrincipal(String principal){
        this.principal=principal;
    }
}

Kemudian dalam saran Anda, yang saya pikir perlu memperpanjang MethodBeforeAdvice, periksa bahwa layanan tertentu memiliki penjelasan @Principal dan menyuntikkan nama Prinsipal, atau setel ke 'ANONIM' sebagai gantinya.

Pavel Rodionov
sumber
Saya perlu mengakses Kepala Sekolah di dalam kelas layanan bisakah Anda memposting contoh lengkap di github? Saya tidak tahu musim semi AOP, karena itu permintaan.
Rakesh Waghela
2

Satu-satunya masalah adalah bahwa bahkan setelah diautentikasi dengan Spring Security, pengguna / pengguna kacang tidak ada dalam wadah, jadi menyuntikkan ketergantungan itu akan sulit. Sebelum kami menggunakan Spring Security, kami akan membuat kacang scoped sesi yang memiliki Kepala Sekolah saat ini, menyuntikkan itu ke "AuthService" dan kemudian menyuntikkan Layanan itu ke sebagian besar layanan lain dalam Aplikasi. Jadi Layanan tersebut hanya akan memanggil authService.getCurrentUser () untuk mendapatkan objek. Jika Anda memiliki tempat dalam kode Anda di mana Anda mendapatkan referensi ke Kepala Sekolah yang sama di sesi, Anda bisa mengaturnya sebagai properti di kacang scoped sesi Anda.

cliff.meyers
sumber
1

Coba ini

Otentikasi otentikasi = SecurityContextHolder.getContext (). GetAuthentication ();
String userName = authentication.getName ();

menghargai
sumber
3
Memanggil SecurityContextHolder.getContext () metode statis adalah persis apa yang saya keluhkan dalam pertanyaan aslinya. Anda belum menjawab apa pun.
Scott Bale
2
Namun, persis apa yang direkomendasikan oleh dokumentasi: static.springsource.org/spring-security/site/docs/3.0.x/... Jadi, apa yang Anda capai dengan menghindarinya? Anda sedang mencari solusi rumit untuk masalah sederhana. Paling-paling - Anda mendapatkan perilaku yang sama. Paling buruk, Anda mendapatkan lubang bug atau keamanan.
Bob Kerns
2
@ BobKerns Untuk pengujian, lebih bersih untuk dapat menyuntikkan otentikasi daripada meletakkannya di utas lokal.
1

Solusi terbaik jika Anda menggunakan Spring 3 dan memerlukan prinsip autentikasi di controller Anda adalah melakukan sesuatu seperti ini:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;

    @Controller
    public class KnoteController {
        @RequestMapping(method = RequestMethod.GET)
        public java.lang.String list(Model uiModel, UsernamePasswordAuthenticationToken authToken) {

            if (authToken instanceof UsernamePasswordAuthenticationToken) {
                user = (User) authToken.getPrincipal();
            }
            ...

    }
Menandai
sumber
1
Mengapa Anda melakukan hal itu dari UsernamePasswordAuthenticationToken memeriksa ketika parameter sudah ketik UsernamePasswordAuthenticationToken?
Scott Bale
(instance authToken UsernamePasswordAuthenticationToken) adalah fungsional yang setara dengan if (authToken! = null). Yang terakhir mungkin sedikit lebih bersih tetapi sebaliknya tidak ada perbedaan.
Markus
1

Saya menggunakan @AuthenticationPrincipalanotasi di @Controllerkelas maupun di @ControllerAdviceranotasi. Ex.:

@ControllerAdvice
public class ControllerAdvicer
{
    private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAdvicer.class);


    @ModelAttribute("userActive")
    public UserActive currentUser(@AuthenticationPrincipal UserActive currentUser)
    {
        return currentUser;
    }
}

Di mana UserActivekelas yang saya gunakan untuk layanan pengguna yang dicatat, dan diperluas dari org.springframework.security.core.userdetails.User. Sesuatu seperti:

public class UserActive extends org.springframework.security.core.userdetails.User
{

    private final User user;

    public UserActive(User user)
    {
        super(user.getUsername(), user.getPasswordHash(), user.getGrantedAuthorities());
        this.user = user;
    }

     //More functions
}

Sangat mudah.

EliuX
sumber
0

Tetapkan Principalsebagai ketergantungan dalam metode pengontrol Anda dan pegas akan menyuntikkan pengguna terotentikasi saat ini dalam metode Anda saat pemanggilan.

Saya lanjut
sumber
-2

Saya suka membagikan cara saya mendukung detail pengguna di halaman freemarker. Semuanya sangat sederhana dan bekerja dengan sempurna!

Anda hanya perlu meletakkan rerequest Authentication di default-target-url(halaman setelah form-login) Ini adalah metode Kontroler saya untuk halaman itu:

@RequestMapping(value = "/monitoring", method = RequestMethod.GET)
public ModelAndView getMonitoringPage(Model model, final HttpServletRequest request) {
    showRequestLog("monitoring");


    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String userName = authentication.getName();
    //create a new session
    HttpSession session = request.getSession(true);
    session.setAttribute("username", userName);

    return new ModelAndView(catalogPath + "monitoring");
}

Dan ini adalah kode ftl saya:

<@security.authorize ifAnyGranted="ROLE_ADMIN, ROLE_USER">
<p style="padding-right: 20px;">Logged in as ${username!"Anonymous" }</p>
</@security.authorize> 

Dan itu saja, nama pengguna akan muncul di setiap halaman setelah otorisasi.

Serge
sumber
Terima kasih telah mencoba menjawab, tetapi penggunaan metode statis SecurityContextHolder.getContext () adalah persis apa yang ingin saya hindari, dan alasan saya mengajukan pertanyaan ini sejak awal.
Scott Bale