【问题标题】:How the Spring Security AuthenticationManager authenticate() method is able to check if the username and password sent are correct?Spring Security AuthenticationManager authenticate() 方法如何检查发送的用户名和密码是否正确?
【发布时间】:2022-01-03 03:56:10
【问题描述】:

我正在开发一个 Spring Boot 应用程序,该应用程序获取系统上现有用户的用户名和密码,然后生成 JWT 令牌。我从教程中复制了它,并对其进行了更改以适应我的特定用例。逻辑对我来说很清楚,但我对用户如何在系统上进行身份验证有很大的疑问。下面我将尝试向您解释,因为这是结构化的,我有什么疑问。

JWT 生成令牌系统由两个不同的微服务组成,分别是:

GET-USER-WS:这个微服务简单地使用Hibernate\JPA来检索系统中特定用户的信息。基本上它包含一个调用服务类的控制器类,该服务类本身调用 JPA 存储库,以检索特定的用户信息:

@RestController
@RequestMapping("api/users")
@Log
public class UserController {
    
    @Autowired
    UserService userService;
    
    @GetMapping(value = "/{email}", produces = "application/json")
    public ResponseEntity<User> getUserByEmail(@PathVariable("email") String eMail) throws NotFoundException  {
        
        log.info(String.format("****** Get the user with eMail %s *******", eMail) );
        
        User user = userService.getUserByEmail(eMail);
        
        if (user == null)
        {
            String ErrMsg = String.format("The user with eMail %s was not found", eMail);
            
            log.warning(ErrMsg);
            
            throw new NotFoundException(ErrMsg);
        }
            
            
        return new ResponseEntity<User>(user, HttpStatus.OK);
        
    }

}

如您所见,此控制器包含一个 API,该 API 使用电子邮件参数(即系统上的用户名)并返回包含此用户信息的 JSON。

然后我有第二个微服务(名为 AUTH-SERVER-JWT),它调用前一个 API 以获取将用于生成 JWT 令牌的用户信息。为了使描述尽可能简单,它包含这个控制器类:

@RestController
//@CrossOrigin(origins = "http://localhost:4200")
public class JwtAuthenticationRestController  {

    @Value("${sicurezza.header}")
    private String tokenHeader;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    @Qualifier("customUserDetailsService")
    //private UserDetailsService userDetailsService;
    private CustomUserDetailsService userDetailsService;
    
    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationRestController.class);

    @PostMapping(value = "${sicurezza.uri}")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtTokenRequest authenticationRequest)
            throws AuthenticationException {
        logger.info("Autenticazione e Generazione Token");

        authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());

        //final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final UserDetailsWrapper userDetailsWrapper = userDetailsService.loadCompleteUserByUsername(authenticationRequest.getUsername());

        final String token = jwtTokenUtil.generateToken(userDetailsWrapper);
        
        logger.warn(String.format("Token %s", token));

        return ResponseEntity.ok(new JwtTokenResponse(token));
    }

    @RequestMapping(value = "${sicurezza.uri}", method = RequestMethod.GET)
    public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request) 
            throws Exception 
    {
        String authToken = request.getHeader(tokenHeader);
        
        if (authToken == null || authToken.length() < 7)
        {
            throw new Exception("Token assente o non valido!");
        }
        
        final String token = authToken.substring(7);
        
        if (jwtTokenUtil.canTokenBeRefreshed(token)) 
        {
            String refreshedToken = jwtTokenUtil.refreshToken(token);
            
            return ResponseEntity.ok(new JwtTokenResponse(refreshedToken));
        } 
        else 
        {
            return ResponseEntity.badRequest().body(null);
        }
    }

    @ExceptionHandler({ AuthenticationException.class })
    public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) 
    {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
    }

    private void authenticate(String username, String password) 
    {
        Objects.requireNonNull(username);
        Objects.requireNonNull(password);

        try {   
            /// ???
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } 
        catch (DisabledException e) 
        {
            logger.warn("UTENTE DISABILITATO");
            throw new AuthenticationException("UTENTE DISABILITATO", e);
        } 
        catch (BadCredentialsException e) 
        {
            logger.warn("CREDENZIALI NON VALIDE");
            throw new AuthenticationException("CREDENZIALI NON VALIDE", e);
        }
    }
}

该类包含两个方法,第一个用于生成全新的 JWT 令牌,第二个用于刷新现有的 JWT 令牌。现在考虑与 createAuthenticationToken() 方法相关的第一个用例(生成全新的 JWT 令牌)。此方法将与 JWT 令牌请求相关的信息作为输入参数:@RequestBody JwtTokenRequest authenticationRequest。基本上 JwtTokenRequest 是一个简单的 DTO 对象,如下所示:

@Data
public class JwtTokenRequest implements Serializable 
{
    private static final long serialVersionUID = -3558537416135446309L;
    private String username;
    private String password;
}

所以正文请求中的有效负载将是这样的:

{
    "username": "xxx@gmail.com",
    "password": "password" 
}

注意:在我的数据库中,我有一个用户拥有此用户名和密码,因此将在系统上检索和验证该用户。

如您所见,createAuthenticationToken() 方法执行的第一个有效操作是:

authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());

基本上,它调用在同一个类中定义的 authenticate() 方法,将之前的凭据 ("username": "xxx@gmail.com"“密码”:“密码”)。

如您所见,这是我的 authenticate() 方法

private void authenticate(String username, String password) 
{
    Objects.requireNonNull(username);
    Objects.requireNonNull(password);

    try {   
        /// ???
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    } 
    catch (DisabledException e) 
    {
        logger.warn("UTENTE DISABILITATO");
        throw new AuthenticationException("UTENTE DISABILITATO", e);
    } 
    catch (BadCredentialsException e) 
    {
        logger.warn("CREDENTIAL ERROR");
        throw new AuthenticationException(""CREDENTIAL ERROR", e);
    }
}

基本上,它将这些凭据传递给定义到注入的 Spring Security AuthenticationManager 实例中的 authenticate() 方法,通过这一行:

authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

此方法似乎能够验证或不验证这些凭据。它似乎工作正常,因为如果我输入了错误的用户名或密码,它会进入 CREDENTIAL ERROR 情况并抛出 AuthenticationException 异常。

这是我最大的疑问:为什么它有效?!?!这怎么可能?如果您回到 createAuthenticationToken() 控制器方法,您可以看到它按以下顺序执行这两个操作:

authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());

final UserDetailsWrapper userDetailsWrapper = userDetailsService.loadCompleteUserByUsername(authenticationRequest.getUsername());

它首先执行 authenticate() 方法(应该检查发送的用户名和密码是否正确),然后调用检索用户信息的服务方法。

看看 authenticate() 方法如何能够检查原始有效负载中发送的凭据是否正确?

【问题讨论】:

  • 它验证那些与数据库中的那些,它还能做什么?
  • @M.Deinum 是的,它完美地工作:它使用我在我的数据库中拥有的预期用户名和密码进行验证。我的疑问不是它是否有效(它有效),而是它是如何运作的?因为我找不到类似这样的配置:“用户名和密码在此表的此字段中”
  • 好吧,你配置了一些东西你的数据库恰好满足了 Spring Security 使用的默认 JDBC 模式。如果没有看到您如何配置安全性,这基本上是不可能的。

标签: java spring spring-boot spring-security


【解决方案1】:

通常,AuthenticationManager 的实现是一个ProviderManager,它将遍历所有已配置的AuthenticationProviders,并尝试使用提供的凭据进行身份验证。

AuthenticationProviders 之一是DaoAuthenticationProvider,它支持UsernamePasswordAuthenticationToken 并使用UserDetailsService(您有一个customUserDetailsService)来检索用户并使用配置的@ 比较password 987654333@.

关于Authentication Architecture的参考文档中有更详细的解释。

【讨论】:

    猜你喜欢
    • 2015-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多