วิธีรับ UserDetails ของผู้ใช้ที่ใช้งานอยู่


170

ในตัวควบคุมของฉันเมื่อฉันต้องการผู้ใช้ที่ใช้งานอยู่ (ล็อกอิน) ฉันกำลังทำสิ่งต่อไปนี้เพื่อให้เกิดUserDetailsการใช้งานของฉัน:

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

มันใช้งานได้ดี แต่ฉันคิดว่า Spring จะทำให้ชีวิตง่ายขึ้นในกรณีเช่นนี้ มีวิธีที่จะUserDetailsautowired เป็นทั้งตัวควบคุมหรือวิธีการหรือไม่

ตัวอย่างเช่น:

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

แต่แทนที่จะได้รับUsernamePasswordAuthenticationTokenฉันจะได้รับUserDetailsแทนหรือไม่

ฉันกำลังมองหาทางออกที่สง่างาม ความคิดใด ๆ

คำตอบ:


226

คำนำ:ตั้งแต่ Spring-Security 3.2 มีคำอธิบายประกอบที่ดี@AuthenticationPrincipalอธิบายอธิบายไว้ในตอนท้ายของคำตอบนี้ นี่เป็นวิธีที่ดีที่สุดเมื่อคุณใช้ Spring-Security> = 3.2

เมื่อคุณ:

  • ใช้ Spring-Security เวอร์ชั่นที่เก่ากว่า
  • จำเป็นต้องโหลดวัตถุผู้ใช้ที่กำหนดเองของคุณจากฐานข้อมูลโดยข้อมูลบางอย่าง (เช่นการเข้าสู่ระบบหรือรหัส) ที่เก็บไว้ในเงินต้นหรือ
  • ต้องการเรียนรู้วิธีการHandlerMethodArgumentResolverหรือWebArgumentResolverสามารถแก้ปัญหานี้ในวิธีที่สง่างามหรือเพียงแค่ต้องการเรียนรู้พื้นหลัง@AuthenticationPrincipalและAuthenticationPrincipalArgumentResolver(เพราะมันขึ้นอยู่กับHandlerMethodArgumentResolver)

จากนั้นอ่านต่อไป - ใช้อย่างอื่น@AuthenticationPrincipalและขอขอบคุณ Rob Winch (ผู้เขียน@AuthenticationPrincipal) และLukas Schmelzeisen (สำหรับคำตอบของเขา)

(BTW: คำตอบของฉันเก่ากว่านิดหน่อย (มกราคม 2555) ดังนั้นจึงเป็นLukas Schmelzeisenที่ขึ้นมาเป็นคนแรกที่มี@AuthenticationPrincipalโซลูชันคำอธิบายประกอบบน Spring Security 3.2)


จากนั้นคุณสามารถใช้ในตัวควบคุมของคุณ

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

มันก็โอเคถ้าคุณต้องการมันอีกครั้ง แต่ถ้าคุณต้องการมันหลาย ๆ ครั้งมันน่าเกลียดเพราะมันทำให้ตัวควบคุมมลพิษของคุณมีรายละเอียดโครงสร้างพื้นฐานซึ่งโดยปกติแล้วกรอบดังกล่าวควรจะถูกซ่อนไว้

ดังนั้นสิ่งที่คุณอาจต้องการคือมีตัวควบคุมดังนี้:

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

WebArgumentResolverดังนั้นคุณจะต้องดำเนินการ มันมีวิธี

Object resolveArgument(MethodParameter methodParameter,
                   NativeWebRequest webRequest)
                   throws Exception

ที่ได้รับการร้องขอจากเว็บ (พารามิเตอร์ที่สอง) และจะต้องส่งกลับUserถ้ารู้สึกรับผิดชอบต่อการโต้แย้งวิธี (พารามิเตอร์แรก)

ตั้งแต่ฤดูใบไม้ผลิ 3.1 HandlerMethodArgumentResolverมีแนวคิดใหม่ที่เรียกว่า ถ้าคุณใช้ Spring 3.1+ คุณควรใช้มัน (อธิบายไว้ในหัวข้อถัดไปของคำตอบนี้))

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

คุณต้องกำหนดคำอธิบายประกอบที่กำหนดเอง - คุณสามารถข้ามมันได้หากทุกอินสแตนซ์ของผู้ใช้ควรนำมาจากบริบทความปลอดภัยเสมอ แต่ไม่เคยเป็นวัตถุคำสั่ง

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

ในการกำหนดค่าคุณจะต้องเพิ่มสิ่งนี้:

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

@See: เรียนรู้วิธีกำหนดอาร์กิวเมนต์เมธอด Spring MVC @Controller

ควรสังเกตว่าถ้าคุณใช้ Spring 3.1 พวกเขาแนะนำ HandlerMethodArgumentResolver ผ่าน WebArgumentResolver - ดูความคิดเห็นโดยเจย์


เช่นเดียวกันกับHandlerMethodArgumentResolverสำหรับ 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;
          }
     }
}

ในการกำหนดค่าคุณจะต้องเพิ่มสิ่งนี้

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

@See การใช้ประโยชน์จากอินเตอร์เฟสSpring MVC 3.1 HandlerMethodArgumentResolver


โซลูชั่น Spring-Security 3.2

Spring Security 3.2 (อย่าสับสนกับ Spring 3.2) มีโซลูชันบิลด์อิน: @AuthenticationPrincipal( org.springframework.security.web.bind.annotation.AuthenticationPrincipal) นี่คือคำอธิบายอย่างดีในคำตอบของ Lukas Schmelzeisen

มันเป็นแค่การเขียน

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

ในการทำให้การทำงานนี้คุณต้องลงทะเบียนAuthenticationPrincipalArgumentResolver( org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver): โดย "เปิดใช้งาน" @EnableWebMvcSecurityหรือโดยการลงทะเบียน bean นี้ภายในmvc:argument-resolvers- วิธีเดียวกับที่ฉันอธิบายด้วยโซลูชันพฤษภาคม 3.1 ของ Spring ด้านบน

การอ้างอิง @See Spring Security 3.2 บทที่ 11.2 @AuthenticationPrincipal


โซลูชัน Spring-Security 4.0

ใช้งานได้เหมือนโซลูชัน Spring 3.2 แต่ใน Spring 4.0 @AuthenticationPrincipalและAuthenticationPrincipalArgumentResolverถูก "ย้าย" ไปยังแพ็คเกจอื่น:

(แต่คลาสเก่าในแพ็คเกจเก่ายังคงมีอยู่ดังนั้นอย่าผสมกัน!)

มันเป็นแค่การเขียน

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

เพื่อให้การทำงานนี้คุณต้องลงทะเบียน ( org.springframework.security.web.method.annotation.) AuthenticationPrincipalArgumentResolver: โดย "เปิดใช้งาน" @EnableWebMvcSecurityหรือโดยการลงทะเบียน bean นี้ภายในmvc:argument-resolvers- วิธีเดียวกับที่ฉันอธิบายด้วยโซลูชันพฤษภาคม 3.1 ของ Spring ด้านบน

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

@See Spring Security 5.0 อ้างอิงตอนที่ 39.3 @AuthenticationPrincipal


หรือเพียงแค่สร้าง bean ที่มีเมธอด getUserDetails () และ @Autowire ที่อยู่ในคอนโทรลเลอร์ของคุณ
sourcedelica

การใช้โซลูชันนี้ฉันตั้งค่าเบรกพอยต์ที่ด้านบนของ resolArgument () แต่แอพของฉันไม่เคยก้าวเข้าสู่โปรแกรมแก้ไขข้อโต้แย้งเว็บ การกำหนดค่าสปริงของคุณในบริบทเซิร์ฟเล็ตไม่ใช่บริบทรูทใช่ไหม
Jay

@Jay: การกำหนดค่านี้เป็นส่วนหนึ่งของบริบทรูทไม่ใช่บริบทของเซิร์ฟเล็ต - มันเป็นตะเข็บที่ฉันลืมระบุ id (id="applicationConversionService")ในตัวอย่าง
Ralph

11
ควรสังเกตว่าถ้าคุณใช้ Spring 3.1 พวกเขาแนะนำ HandlerMethodArgumentResolver ผ่าน WebArgumentResolver ฉันได้ HandlerMethodArgumentResolver ให้ทำงานโดยกำหนดค่าด้วย <annotation-driven> ในบริบทของ servlet นอกจากนั้นฉันได้ตอบคำถามตามที่โพสต์ไว้ที่นี่และทุกอย่างทำงานได้ยอดเยี่ยม
เจย์

@sourcedelica ทำไมไม่เพียงสร้างวิธีการคงที่?
Alex78191

66

ในขณะที่Ralphs Answerมอบทางออกที่หรูหราด้วย Spring Security 3.2 คุณไม่จำเป็นต้องใช้งานของคุณเองArgumentResolverอีกต่อไป

หากคุณมีUserDetailsการนำไปใช้CustomUserคุณสามารถทำสิ่งนี้ได้:

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

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

ดูเอกสารความปลอดภัยของ Spring: @AuthenticationPrincipal


2
สำหรับผู้ที่ไม่ชอบการอ่านการเชื่อมโยงให้นี้จะต้องมีการเปิดใช้งานทั้งโดย@EnableWebMvcSecurityหรือในรูปแบบ XML:<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven>
sodik

วิธีการตรวจสอบว่าcustomUserเป็นโมฆะหรือไม่?
Sajad

if (customUser != null) { ... }
T3rm1

27

Spring Security มีวัตถุประสงค์เพื่อทำงานกับเฟรมเวิร์กที่ไม่ใช่สปริงอื่น ๆ ดังนั้นจึงไม่ได้รวมเข้ากับ Spring MVC อย่างแน่นหนา Spring Security ส่งคืนAuthenticationออบเจ็กต์จากHttpServletRequest.getUserPrincipal()วิธีการตามค่าเริ่มต้นดังนั้นนั่นคือสิ่งที่คุณได้รับในฐานะตัวการ คุณสามารถรับUserDetailsวัตถุของคุณได้โดยตรงจากสิ่งนี้โดยใช้

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

โปรดทราบว่าประเภทวัตถุอาจแตกต่างกันไปขึ้นอยู่กับกลไกการตรวจสอบความถูกต้องที่ใช้ ( UsernamePasswordAuthenticationTokenตัวอย่างเช่นคุณอาจไม่ได้รับ a ) และAuthenticationไม่จำเป็นต้องมีUserDetailsไม่เคร่งครัดต้องมีมันสามารถเป็นสตริงหรือชนิดอื่น ๆ

หากคุณไม่ต้องการโทรSecurityContextHolderโดยตรงแนวทางที่หรูหราที่สุด (ซึ่งฉันจะติดตาม) คือการฉีดอินเทอร์เฟซการเข้าถึงบริบทความปลอดภัยที่กำหนดเองของคุณเองซึ่งปรับแต่งให้ตรงกับความต้องการและประเภทวัตถุผู้ใช้ของคุณ สร้างส่วนต่อประสานด้วยวิธีการที่เกี่ยวข้องเช่น:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

จากนั้นคุณสามารถใช้สิ่งนี้ได้โดยการเข้าถึงSecurityContextHolderในการใช้งานมาตรฐานของคุณดังนั้นแยกรหัสของคุณจาก Spring Security โดยสิ้นเชิง จากนั้นแทรกสิ่งนี้ลงในคอนโทรลเลอร์ที่ต้องการเข้าถึงข้อมูลความปลอดภัยหรือข้อมูลผู้ใช้ปัจจุบัน

ประโยชน์หลักอื่น ๆ คือมันง่ายที่จะทำให้การใช้งานที่ง่ายด้วยข้อมูลคงที่สำหรับการทดสอบโดยไม่ต้องกังวลเกี่ยวกับการเติมข้อมูลในเธรดและอื่น ๆ


ฉันกำลังพิจารณาวิธีการนี้ แต่ฉันไม่แน่ใจก) วิธีที่จะทำอย่างถูกต้อง (tm) และ b) หากมีปัญหาเธรดใด ๆ คุณแน่ใจหรือไม่ว่าไม่มีปัญหา ฉันจะใช้วิธีการเพิ่มความคิดเห็นที่โพสต์ไว้ด้านบน แต่ฉันคิดว่านี่ยังเป็นวิธีที่เหมาะสมในการทำเช่นนี้ ขอบคุณสำหรับการโพสต์ :)
Awnry Bear

4
ในทางเทคนิคเหมือนกับการเข้าถึง SecurityContextHolder โดยตรงจากคอนโทรลเลอร์ของคุณดังนั้นจึงไม่ควรมีปัญหาเธรดใด ๆ มันทำให้การโทรไปยังที่เดียวและช่วยให้คุณสามารถฉีดทางเลือกสำหรับการทดสอบได้อย่างง่ายดาย นอกจากนี้คุณยังสามารถใช้วิธีการเดียวกันในคลาสที่ไม่ใช่เว็บอื่นที่ต้องการเข้าถึงข้อมูลความปลอดภัย
Shaun the Sheep

Gotcha ... นั่นคือสิ่งที่ฉันคิด นี่จะเป็นสิ่งที่ดีที่จะกลับมาอีกครั้งถ้าฉันมีปัญหาในการทดสอบหน่วยด้วยวิธีการใส่คำอธิบายประกอบหรือถ้าฉันต้องการลดการแต่งงานด้วยสปริง
Awnry Bear

@LukeTaylor คุณช่วยอธิบายเพิ่มเติมเกี่ยวกับวิธีการนี้ได้ไหมฉันเป็นคนใหม่สำหรับการรักษาความปลอดภัยในฤดูใบไม้ผลิและฤดูใบไม้ผลิดังนั้นฉันจึงไม่ค่อยเข้าใจวิธีที่จะใช้สิ่งนี้ ฉันจะใช้สิ่งนี้ที่ไหนเพื่อให้เข้าถึงได้? กับ UserServiceImpl ของฉัน?
Cu7l4ss

9

ใช้HandlerInterceptorอินเทอร์เฟซแล้วฉีดUserDetailsลงในแต่ละคำขอที่มีรูปแบบดังนี้:

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

1
ขอบคุณ @atrain มันมีประโยชน์และสง่างาม นอกจากนี้ฉันต้องเพิ่ม<mvc:interceptors>ในไฟล์การกำหนดค่าแอปพลิเคชันของฉัน
Eric

เป็นการดีกว่าที่จะได้รับผู้ใช้ที่ได้รับอนุญาตในเทมเพลตผ่านทางspring-security-taglibs: stackoverflow.com/a/44373331/548473
Grigory Kislin

9

เริ่มต้นด้วย Spring Security เวอร์ชั่น 3.2 ฟังก์ชันการใช้งานที่กำหนดเองที่ได้รับการใช้งานโดยคำตอบที่เก่ากว่านั้นมีอยู่ในรูปแบบของ@AuthenticationPrincipalคำอธิบายประกอบที่ได้รับการสนับสนุนAuthenticationPrincipalArgumentResolverคำอธิบายประกอบที่มีการสนับสนุนจาก

ตัวอย่างง่ายๆของการใช้งานคือ:

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

CustomUser ต้องสามารถกำหนดได้ authentication.getPrincipal()

นี่คือ Javadocs ของAuthenticationPrincipalและAuthenticationPrincipalArgumentResolver ที่สอดคล้องกัน


1
@nbro เมื่อฉันเพิ่มโซลูชันเฉพาะเวอร์ชันไม่มีโซลูชันอื่นใดที่ได้รับการอัปเดตเพื่อพิจารณาโซลูชันนี้
geoand

AuthenticationPrincipalArgumentResolver ขณะนี้เลิกใช้แล้ว
Igor Donin

5
@Controller
public abstract class AbstractController {
    @ModelAttribute("loggedUser")
    public User getLoggedUser() {
        return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}

0

และหากคุณต้องการผู้ใช้ที่ได้รับอนุญาตในการใช้เทมเพลต (เช่น JSP)

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

พร้อมกับ

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

0

คุณสามารถลองสิ่งนี้: โดยใช้การรับรองความถูกต้องวัตถุจากฤดูใบไม้ผลิเราจะได้รับรายละเอียดผู้ใช้จากมันในวิธีการควบคุม ด้านล่างเป็นตัวอย่างโดยการส่งวัตถุการตรวจสอบความถูกต้องในวิธีการควบคุมพร้อมกับการโต้แย้งเมื่อผู้ใช้ตรวจสอบความถูกต้องรายละเอียดจะถูกบรรจุในวัตถุการตรวจสอบความถูกต้อง

@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}

โปรดอธิบายคำตอบของคุณให้ละเอียด
Nikolai Shevchenko

ฉันได้แก้ไขคำตอบของฉันเราสามารถใช้ Authentication Object ซึ่งมีรายละเอียดผู้ใช้ของผู้ใช้ที่ได้รับการรับรองความถูกต้องแล้ว
Mirza Shujathullah
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.