Khi sử dụng Spring Security, cách thích hợp để lấy thông tin tên người dùng hiện tại (tức là SecurityContext) trong một bean là gì?


288

Tôi có một ứng dụng web Spring MVC sử dụng Spring Security. Tôi muốn biết tên người dùng của người dùng hiện đang đăng nhập. Tôi đang sử dụng đoạn mã được đưa ra dưới đây. Đây có phải là cách được chấp nhận?

Tôi không thích có một cuộc gọi đến một phương thức tĩnh bên trong bộ điều khiển này - nó đánh bại toàn bộ mục đích của Spring, IMHO. Có cách nào để định cấu hình ứng dụng để có SecurityContext hiện tại hoặc Xác thực hiện tại, được tiêm không?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }

Tại sao bạn không có bộ điều khiển (bộ điều khiển bảo mật) với tư cách là một siêu lớp lấy người dùng từ SecurityContext và đặt nó làm biến đối tượng bên trong lớp đó? Bằng cách này khi bạn mở rộng bộ điều khiển an toàn, cả lớp của bạn sẽ có quyền truy cập vào hiệu trưởng Người dùng của bối cảnh hiện tại.
Dehan de Croos

Câu trả lời:


259

Nếu bạn đang sử dụng Spring 3 , cách dễ nhất là:

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

     final String currentUser = principal.getName();

 }

69

Rất nhiều điều đã thay đổi trong thế giới Mùa xuân kể từ khi câu hỏi này được trả lời. Spring đã đơn giản hóa việc có được người dùng hiện tại trong một bộ điều khiển. Đối với các loại đậu khác, Spring đã chấp nhận các đề xuất của tác giả và đơn giản hóa việc tiêm 'SecurityContextHolder'. Thêm chi tiết trong các ý kiến.


Đây là giải pháp tôi đã kết thúc với. Thay vì sử dụng SecurityContextHoldertrong bộ điều khiển của mình, tôi muốn tiêm thứ gì đó sử dụng SecurityContextHolderdưới mui xe nhưng trừu tượng hóa lớp giống như đơn lẻ khỏi mã của tôi. Tôi đã tìm thấy không có cách nào để làm điều này ngoài việc lăn giao diện của riêng tôi, như vậy:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Bây giờ, bộ điều khiển của tôi (hoặc bất cứ POJO nào) sẽ trông như thế này:

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
  }

}

Và, vì giao diện là một điểm tách rời, thử nghiệm đơn vị là đơn giản. Trong ví dụ này tôi sử dụng 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();
  }

}

Giao diện mặc định của giao diện trông như thế này:

public class SecurityContextHolderFacade implements SecurityContextFacade {

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

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

}

Và cuối cùng, cấu hình Spring sản xuất trông như thế này:

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

Có vẻ hơi ngớ ngẩn khi Spring, một thùng chứa phụ thuộc tất cả mọi thứ, đã không cung cấp một cách để tiêm một cái gì đó tương tự. Tôi hiểu SecurityContextHolderđược thừa hưởng từ acegi, nhưng vẫn còn. Vấn đề là, chúng rất gần - nếu chỉ SecurityContextHoldercó một getter để lấy thể hiện bên dưới SecurityContextHolderStrategy(đó là một giao diện), bạn có thể tiêm nó. Trên thực tế, tôi thậm chí đã mở một vấn đề Jira cho hiệu ứng đó.

Một điều cuối cùng - tôi đã thay đổi đáng kể câu trả lời tôi có ở đây trước đây. Kiểm tra lịch sử nếu bạn tò mò nhưng, như một đồng nghiệp đã chỉ ra cho tôi, câu trả lời trước của tôi sẽ không hoạt động trong môi trường đa luồng. Theo mặc định, bên dưới SecurityContextHolderStrategyđược sử dụng SecurityContextHolderlà một thể hiện của ThreadLocalSecurityContextHolderStrategy, lưu trữ SecurityContexts trong a ThreadLocal. Do đó, không nhất thiết phải tiêm SecurityContexttrực tiếp vào hạt đậu vào thời điểm khởi tạo - có thể cần phải lấy ra ThreadLocalmỗi lần, trong một môi trường đa luồng, do đó, một cái đúng được lấy ra.


1
Tôi thích giải pháp của bạn - đó là một cách sử dụng thông minh của hỗ trợ phương pháp nhà máy trong Spring. Điều đó nói rằng, điều này làm việc cho bạn vì đối tượng điều khiển nằm trong phạm vi yêu cầu web. Nếu bạn thay đổi phạm vi của bean điều khiển sai cách, điều này sẽ bị hỏng.
Paul Morie

2
Hai bình luận trước đề cập đến một câu trả lời cũ, không chính xác mà tôi vừa thay thế.
Scott Bale

12
Đây có còn là giải pháp được đề xuất với bản phát hành Spring hiện tại không? Tôi không thể tin rằng nó cần rất nhiều mã chỉ để lấy tên người dùng.
Ta Sas

6
Nếu bạn đang sử dụng Spring Security 3.0.x, họ đã triển khai đề xuất của tôi trong vấn đề JIRA, tôi đã đăng nhập jira.springsource.org/browse/SEC-1188 để bây giờ bạn có thể tiêm phiên bản SecurityContextHolderStrargety (từ SecurityContextHolder) vào bean của bạn thông qua tiêu chuẩn của bạn Cấu hình lò xo.
Scott Bale

4
Xin vui lòng xem câu trả lời tsunade21. Giờ đây, Spring 3 cho phép bạn sử dụng java.security.Principal làm đối số phương thức trong bộ điều khiển của bạn
Patrick

22

Tôi đồng ý rằng phải truy vấn SecurityContext cho người dùng hiện tại bốc mùi, có vẻ như một cách rất không hợp lý để xử lý vấn đề này.

Tôi đã viết một lớp "người trợ giúp" tĩnh để giải quyết vấn đề này; Nó bẩn ở chỗ nó là phương pháp toàn cầu và tĩnh, nhưng tôi đã hiểu theo cách này nếu chúng ta thay đổi bất cứ điều gì liên quan đến Bảo mật, ít nhất tôi chỉ phải thay đổi chi tiết ở một nơi:

/**
* 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;
}

22
nó dài như SecurityContextHolder.getContext () và cái sau là chủ đề an toàn vì nó giữ các chi tiết bảo mật trong một threadLocal. Mã này duy trì không có trạng thái.
matt b

22

Để làm cho nó chỉ hiển thị trong các trang JSP của bạn, bạn có thể sử dụng Thẻ bảo mật Spring Lib:

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

Để sử dụng bất kỳ thẻ nào, bạn phải khai báo taglib bảo mật trong tệp JSP của bạn:

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

Sau đó, trong một trang jsp làm một cái gì đó như thế này:

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

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

LƯU Ý: Như đã đề cập trong các nhận xét của @ SBerg413, bạn sẽ cần thêm

sử dụng biểu thức = "đúng"

vào thẻ "http" trong cấu hình security.xml để làm việc này.


Có vẻ như đây có thể là cách được Spring Security chấp thuận!
Nick Spacek

3
Để phương thức này hoạt động, bạn cần thêm use-biểu thức = "true" vào thẻ http trong cấu hình security.xml.
SBerg413

Cảm ơn @ SBerg413, tôi sẽ chỉnh sửa câu trả lời của tôi và thêm phần làm rõ quan trọng của bạn!
Brad park

14

Nếu bạn đang sử dụng Spring Security ver> = 3.2, bạn có thể sử dụng @AuthenticationPrincipalchú thích:

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

Ở đây, CustomUserlà một đối tượng tùy chỉnh thực hiện UserDetailsđược trả về bởi một tùy chỉnh UserDetailsService.

Thông tin chi tiết có thể được tìm thấy trong chương @AuthenticationPrincipal của tài liệu tham khảo Bảo mật mùa xuân.


13

Tôi nhận được người dùng xác thực bởi HttpServletRequest.getUserPrincipal ();

Thí dụ:

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";
    }
}

Tôi thích giải pháp của bạn. Để các chuyên gia mùa xuân: Đó có phải là giải pháp an toàn, tốt?
marioosh

Không phải là một giải pháp tốt. Bạn sẽ nhận được nullnếu người dùng được xác thực ẩn danh ( http> anonymouscác phần tử trong Spring Security XML). SecurityContextHolderhoặc SecurityContextHolderStrategylà cách thích hợp.
Bây giờ là

1
Vì vậy, tôi đã kiểm tra nếu không null request.getUserPrincipal ()! = Null.
digz6666

Là null trong Bộ lọc
Alex78191

9

Trong Spring 3+ bạn có các tùy chọn sau.

Lựa chọn 1 :

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

Lựa chọn 2 :

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

Tùy chọn 3:

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

}

Tùy chọn 4: Fancy one: Kiểm tra điều này để biết thêm chi tiết

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

1
Kể từ 3.2, spring-security-web đi kèm với @CurrentUserhoạt động như tùy chỉnh @ActiveUsertừ liên kết của bạn.
Mike Partridge

@MikePartridge, tôi dường như không tìm thấy những gì bạn nói, bất kỳ liên kết nào ?? hoặc biết thêm thông tin ??
azerafati

1
Lỗi của tôi - Tôi đã hiểu nhầm Spring Security xác thựcPrincipalArgumentResolver javadoc. Một ví dụ được hiển thị ở đó gói @AuthenticationPrincipalvới một @CurrentUserchú thích tùy chỉnh . Kể từ 3.2, chúng ta không cần triển khai trình giải quyết đối số tùy chỉnh như trong câu trả lời được liên kết. Câu trả lời khác này có nhiều chi tiết hơn.
Mike Partridge

5

Đúng, thống kê nói chung là xấu - nói chung, nhưng trong trường hợp này, tĩnh là mã an toàn nhất bạn có thể viết. Vì bối cảnh bảo mật liên kết một Hiệu trưởng với luồng hiện đang chạy, mã bảo mật nhất sẽ truy cập tĩnh từ luồng càng trực tiếp càng tốt. Ẩn truy cập đằng sau một lớp bao bọc được tiêm cung cấp cho kẻ tấn công có nhiều điểm hơn để tấn công. Họ sẽ không cần quyền truy cập vào mã (mà họ sẽ gặp khó khăn khi thay đổi nếu jar được ký), họ chỉ cần một cách để ghi đè cấu hình, có thể được thực hiện trong thời gian chạy hoặc trượt một số XML vào đường dẫn lớp. Ngay cả việc sử dụng phép chú thích trong mã đã ký cũng sẽ bị quá tải với XML bên ngoài. XML như vậy có thể tiêm hệ thống đang chạy với một hiệu trưởng giả mạo.


5

Tôi sẽ chỉ làm điều này:

request.getRemoteUser();

1
Điều đó có thể làm việc, nhưng không đáng tin cậy. Từ javadoc: "Việc tên người dùng được gửi với mỗi yêu cầu tiếp theo hay không tùy thuộc vào trình duyệt và loại xác thực." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/
Kẻ

3
Đây thực sự là một cách hợp lệ và rất đơn giản để có được tên người dùng từ xa trong ứng dụng web Spring Security. Chuỗi bộ lọc tiêu chuẩn bao gồm một chuỗi bao SecurityContextHolderAwareRequestFilterbọc yêu cầu và thực hiện cuộc gọi này bằng cách truy cập vào SecurityContextHolder.
Shaun the Sheep

4

Đối với ứng dụng Spring MVC cuối cùng mà tôi đã viết, tôi đã không sử dụng trình giữ SecurityContext, nhưng tôi đã có một bộ điều khiển cơ bản mà tôi có hai phương thức tiện ích liên quan đến điều này ... isAuthenticated () & getUsername (). Trong nội bộ họ thực hiện các phương thức tĩnh gọi bạn mô tả.

Ít nhất thì nó chỉ ở một nơi nếu bạn cần tái cấu trúc sau này.


3

Bạn có thể sử dụng aproach Spring AOP. Ví dụ: nếu bạn có một số dịch vụ, cần phải biết hiệu trưởng hiện tại. Bạn có thể giới thiệu chú thích tùy chỉnh, ví dụ @Principal, cho biết Dịch vụ này phải phụ thuộc chính.

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

Sau đó, theo lời khuyên của bạn, mà tôi nghĩ cần phải mở rộng Phương thứcB BeforeAdvice, hãy kiểm tra xem dịch vụ cụ thể đó có chú thích @Principal và nhập tên Hiệu trưởng hay đặt thành 'ANONYMOUS'.


Tôi cần truy cập Hiệu trưởng bên trong lớp dịch vụ, bạn có thể đăng một ví dụ hoàn chỉnh trên github không? Tôi không biết mùa xuân AOP, do đó yêu cầu.
Rakesh Waghela

2

Vấn đề duy nhất là ngay cả sau khi xác thực với Spring Security, người dùng / bean chính không tồn tại trong container, do đó việc tiêm phụ thuộc sẽ rất khó khăn. Trước khi chúng tôi sử dụng Spring Security, chúng tôi sẽ tạo một bean có phạm vi phiên có Hiệu trưởng hiện tại, đưa nó vào "AuthService" và sau đó đưa Dịch vụ đó vào hầu hết các dịch vụ khác trong Ứng dụng. Vì vậy, các Dịch vụ đó chỉ đơn giản gọi authService.getCienUser () để lấy đối tượng. Nếu bạn có một vị trí trong mã của mình, nơi bạn có được một tham chiếu đến cùng một Hiệu trưởng trong phiên, bạn có thể chỉ cần đặt nó làm tài sản trên hạt đậu trong phiên của bạn.


1

Thử cái này

Xác thực xác thực = SecurityContextHolder.getContext (). GetAuthentication ();
Chuỗi userName = xác thực.getName ();


3
Gọi phương thức tĩnh SecurityContextHolder.getContext () chính xác là những gì tôi đã phàn nàn trong câu hỏi ban đầu. Bạn chưa trả lời gì cả.
Scott Bale

2
Tuy nhiên, đó chính xác là những gì tài liệu đề xuất: static.springsource.org/spring-security/site/docs/3.0.x/. Vậy bạn đang làm gì bằng cách tránh nó? Bạn đang tìm kiếm một giải pháp phức tạp cho một vấn đề đơn giản. Tốt nhất - bạn có được hành vi tương tự. Tồi tệ nhất, bạn nhận được một lỗi hoặc lỗ hổng bảo mật.
Bob Kerns

2
@BobKerns Để thử nghiệm, nó có khả năng thực hiện xác thực trái ngược với việc đặt nó trên một luồng cục bộ.

1

Giải pháp tốt nhất nếu bạn đang sử dụng Spring 3 và cần hiệu trưởng được xác thực trong bộ điều khiển của bạn là làm một cái gì đó như thế này:

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();
            }
            ...

    }

1
Tại sao bạn lại thực hiện ví dụ đó UsernamePasswordAuthenticationToken kiểm tra khi tham số đã thuộc loại UsernamePasswordAuthenticationToken?
Scott Bale

(authToken instanceof UsernamePasswordAuthenticationToken) là chức năng tương đương với if (authToken! = null). Cái sau có thể sạch hơn một chút nhưng nếu không thì không có gì khác biệt.
Đánh dấu

1

Tôi đang sử dụng @AuthenticationPrincipalchú thích trong @Controllercác lớp cũng như trong các @ControllerAdvicerchú thích. Ví dụ.:

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


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

Trong trường hợp UserActivelà lớp tôi sử dụng cho các dịch vụ người dùng đăng nhập, và kéo dài từ org.springframework.security.core.userdetails.User. Cái gì đó như:

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
}

Thật sự dễ dàng.


0

Xác định Principallà một phụ thuộc trong phương thức điều khiển của bạn và mùa xuân sẽ đưa người dùng được xác thực hiện tại vào phương thức của bạn theo yêu cầu.


-2

Tôi muốn chia sẻ cách hỗ trợ chi tiết người dùng của tôi trên trang freemarker. Mọi thứ đều rất đơn giản và hoạt động hoàn hảo!

Bạn chỉ cần đặt lại yêu cầu xác thực default-target-url(trang sau khi đăng nhập mẫu) Đây là phương pháp Điều khiển của tôi cho trang đó:

@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");
}

Và đây là mã ftl của tôi:

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

Và đó là, tên người dùng sẽ xuất hiện trên mỗi trang sau khi ủy quyền.


Cảm ơn bạn đã cố gắng trả lời, nhưng việc sử dụng phương thức tĩnh SecurityContextHolder.getContext () chính xác là những gì tôi muốn tránh và lý do tôi đã hỏi câu hỏi này ngay từ đầu.
Scott Bale
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.