Spring Security @AuthenticationPrincipal

27

我一直在尝试使用自定义用户类使@AuthenticationPrincipal正常工作。不幸的是,用户总是null。这里是代码:

控制器

@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(@AuthenticationPrincipal User user) {
    ModelAndView mav= new ModelAndView("/web/index");
    mav.addObject("user", user);
    return mav;
}

安全配置

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

}

CustomUserDetailsService

@Component
public class CustomUserDetailsService implements UserDetailsService {

@Autowired
UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // Spring Data findByXY function
    return userRepository.findByUsername(username);
}

用户实体

public class User implements UserDetails{
    private String username;
    private String password;
    private Collection<Authority> authorities;

    // Getters and Setters

}

权限实体

public class Authority implements GrantedAuthority{
    private User user;
    private String role;

    // Getters and Setters

    @Override
    public String getAuthority() {
        return this.getRole();
    }
}

我曾尝试过网上找到的各种解决方案,例如将我的自定义用户对象转换为以下内容:

return new org.springframework.security.core.userdetails.User(user.getLogin(), user.getPassword(), true, true, true, true,  authorities);

获取活跃用户的其他方法没有问题,但我发现@AuthenticationProvider CustomUserObject是最清洁的方式,这就是为什么我希望它能够工作。非常感谢任何帮助。


嗯...如果Spring安全认证被正确实现,你总是可以获取Principal对象,获取主体名称并通过用户名查找用户。 - Branislav Lazic
1
是的,这就是我目前正在做的事情... Authentication auth = SecurityContextHolder.getContext().getAuthentication(); User principal = uRep.findByUsername(auth.getName()); ..但正如我所说,当可用时,我希望有一个干净的解决方案。 - Lukehey
2
我和你面临完全相同的问题。@AuthenticationPrincipal总是给我一个默认用户(User类只是用默认构造函数初始化),而ContextSecurityHolder和HttpServletRequest#getUserPrincipal()则给我期望的(已认证)用户。顺便说一下,你不应该使用@EnableWebMvcSecurity(在Spring Security 4+中已弃用),而应该使用@EnableWebSecurity来注释你的SecurityConfig。 - Rémi Doolaeghe
谢谢指出,完全忽略了那个 : )。如果您找到解决方法,请不要犹豫在此发布。 - Lukehey
我已经在Spring上提交了一个JIRA: https://jira.spring.io/browse/SEC-3145 - Rémi Doolaeghe
13个回答

10
在我的情况下,我会收到一个字符串返回值(用户名),而不是UserDetails对象。也就是说,你应该将方法签名定义为:
public ModelAndView index(@AuthenticationPrincipal String username)

这并不奇怪,因为实际上 @AuthenticationPrincipal 返回的是Authentication.getPrincipal(),根据文档所述:
在带有用户名和密码的认证请求的情况下,这将是用户名。调用者应该为认证请求填充主体。
AuthenticationManager 实现通常会返回一个包含更丰富信息的 Authentication 作为应用程序使用的主体。许多身份验证提供程序将创建一个 UserDetails 对象作为主体。 参见:https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/api/ 因此,我假设你的 AuthenticationManager 实现仅返回用户名。

8

不必使用@AuthenticationPrincipal,你可以直接在方法参数中指定已验证用户的依赖关系。如下所示:

@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(Principal user) {
    ModelAndView mav= new ModelAndView("/web/index");
    mav.addObject("user", user);
    return mav;
} 

这个Principal对象将是通过Spring Security进行身份验证的实际对象。当方法被调用时,Spring会为您注入此对象。


7
正如我所说,这些方法都能正常工作。但是根据文档,只要我的用户对象实现了UserDetails并启用了WebMvcSecurity(我正在使用Spring Security 4.0.2),我就应该能够通过AuthenticationPrincipal注释获取自定义用户对象。你知道我可能漏掉了什么吗? - Lukehey
1
令人惊讶的是,使用 @AuthenticationPrincipal User 的方法对我来说不起作用,但直接使用 Principal 却可以。正在等待 Spring 3145 票 对此进行回答... - Rémi Doolaeghe
2
@RémiDoolaeghe - 这是一个晚回复,但你需要获取UserDetails的实例而不是仅仅是User - Ickster

4

来自文档:

AuthenticationPrincipalArgumentResolver 类将使用 SecurityContextHolder 中 Authentication.getPrincipal() 解析 CustomUser 参数。如果 Authentication 或 Authentication.getPrincipal() 为 null,则返回 null。如果类型不匹配,则除非 AuthenticationPrincipal.errorOnInvalidType() 为 true,否则将返回 null,否则将抛出 ClassCastException。

以下简单代码适用于我:

@RestController
public class ApiController {

  @RequestMapping(method = RequestMethod.GET, path = "/api/public/{id}")
  public ResponseEntity<ApiResponse> getPublicResource(
      @PathVariable Integer id,
      @AuthenticationPrincipal String principal
  ) {

    if (id % 2 == 0) {
      SecurityContext context = SecurityContextHolder.getContext();
      Authentication authentication = context.getAuthentication();
      return ResponseEntity.ok(new ApiResponse(
          authentication.getPrincipal().toString(),
          "this method scrapes principal directly from SpringSecurityContext"));
    }

    return ResponseEntity.ok(new ApiResponse(
        principal.toString(),
        "this method retrieves principal from method argument"));

  }

}

只需检查认证对象中的主体类型即可。

3
您需要检查您是否同时使用了正确版本的@AuthenticationPrincipal注释和AuthenticationPrincipalArgumentResolver类。在Spring Security 4.0版本之前,您需要使用以下类: 
  • org.springframework.security.web.bind.annotation.AuthenticationPrincipal

  • org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver

从4.0版本开始,您必须使用以下内容:
  • org.springframework.security.core.annotation.AuthenticationPrincipal

  • org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver

请参阅官方文档@AuthenticationPrincipal以获取配置示例。

2
我发现了另一个解决方案,尽管这不是官方的解决方法。
在你的控制器中:
@RequestMapping(value = "/login", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<UserDTO> login(@RequestBody UserDTO user){
    try {
        usersService.authenticate(user.getUsername(), user.getPassword());
        return new ResponseEntity<UserDTO>(user, HttpStatus.OK);
    }
    catch (BadCredentialsException e){
        return new ResponseEntity<UserDTO>(HttpStatus.UNAUTHORIZED);
    }
}

在这里,UserDTO只是一个包含用户名和密码的表单。

在您的CustomUserDetailsService中

public void authenticate(String username, String password){
    try{
        User user = new User(username, password);
        Authentication request = new UsernamePasswordAuthenticationToken(user, password, Arrays.asList(WebSecurityConfiguration.USER));
        Authentication result = authenticationManager.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);       
    } catch (InternalAuthenticationServiceException e){
        // treat as a bad credential
    }
}

实现自己的AuthenticationManager。
@Component
class DefaultAuthenticationManager implements AuthenticationManager {

@Autowired
private CustomUserDetailsService usersService;

public Authentication authenticate(Authentication auth) throws AuthenticationException {
    UserDetails user = usersService.loadUserByUsername(((User)auth.getPrincipal()).getUsername());
    if (user != null) {
        return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }
// Handle bad credentials here  
}
}

重要的是,CustomUserDetailsService#authenticate中的Principal是一个对象,而不是您验证用户的名称,以便框架可以处理它,然后通过@AuthenticationPrincipal机制进行注入。这对我有用。

1

我发现使用@AuthenticationPrincipal定义(expression = "userDetails")可以解决问题

public ResponseEntity<UserDetails> index(@AuthenticationPrincipal(expression = "userDetails") UserDetails userDetails) {
     return ResponseEntity.ok(userDetails);
}

1
@RequestMapping(method= RequestMethod.GET,value="/authenticate", produces = MediaType.APPLICATION_JSON_VALUE)
public Object authenticate(@AuthenticationPrincipal Object obj) {
    return obj;
}

我有同样的问题,我能够让这个工作起来。 你可以使用一个映射器。

@RequestMapping(method = RequestMethod.GET, value = "/authenticate2", produces = MediaType.APPLICATION_JSON_VALUE)
public User authenticate2(@AuthenticationPrincipal Object obj) throws IOException {
    return mapper.readValue(mapper.writeValueAsString(obj), User.class);
}

这些对我有用,希望它能对未来的任何人都有用。

0
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(@AuthenticationPrincipal(expression = "user") User user) {
    ModelAndView mav= new ModelAndView("/web/index");
    mav.addObject("user", user);
    return mav;
}

只需输入 expression = "user",您就可以开始了。 (如果您的用户实体类名称不同,请相应更新。)


0

我是通过扩展AbstractUserDetailsAuthenticationProvider身份验证提供程序来实现这一点的。

在我的提供程序中,我重写了以下方法:

protected UserDetails retrieveUser( String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {}

在这个方法中,我创建了我的UserDetails实现并返回它。然后我可以使用AuthenticationPrincipal注释在控制器中获取它。


我正在实现接口而不是覆盖抽象类。AuthenticatedPrincipal参数是使用我们实现Spring版本的userDetails对象的新实例创建的,但这并没有什么帮助,因为所有属性都为空。 - tad604

0

尽管文档提到了customUser,但我认为这是不可能的。在Spring控制器方法参数类型中使用@AuthenticationPrincipaljava.lang.Objectorg.springframework.security.core.userdetails.User

如果类型不匹配,则返回null,除非AuthenticationPrincipal.errorOnInvalidType()为true,在这种情况下将抛出ClassCastException异常。 Spring API Documentation


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接