【问题标题】:Handling Authentication Failure with Springboot & Spring security使用 Springboot 和 Spring security 处理身份验证失败
【发布时间】:2021-01-03 21:18:22
【问题描述】:

在使用 Spring 开发的 Rest 应用程序中,我使用 POJO 类、DTO 和实体进行用户管理。这是我的实体类的摘要。

@Entity
@Table(name="users")
@Getter @Setter
@AllArgsConstructor @NoArgsConstructor
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String userKeyId;

    @Column(nullable = false, length = 50)
    private String firstName;

    @Column(nullable = false, length = 50)
    private String lastName;
    
    @Column(nullable = false, length = 120, unique = true)
    private String email;

    @Column(nullable = false)
    private String encryptedPassword;

    @Column
    private String emailVerificationToken;

    @Column(name = "email_verification_status", columnDefinition = "BOOLEAN NOT NULL DEFAULT FALSE")
    private Boolean emailVerificationStatus = false;

    @Column(name="is_account_non_expired")
    private Boolean isAccountNonExpired;

    @Column(name="is_account_non_locked")
    private Boolean isAccountNonLocked;

    @Column(name="is_credentials_non_expired")
    private Boolean isCredentialsNonExpired;

    @Column(name="is_enabled")
    private Boolean isEnabled;

    @Column(name="is_logged_in")
    private Boolean isLoggedIn;
    
    @ManyToMany(cascade= { CascadeType.PERSIST }, fetch = FetchType.EAGER )
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns=@JoinColumn(name = "role_id", referencedColumnName = "id"))
    private Collection<RoleEntity> roles;

    @CreationTimestamp
    @Temporal(TemporalType.DATE)
    @Column(name="created_at")
    private Date createdAt;

    @UpdateTimestamp
    @Temporal(TemporalType.DATE)
    @Column(name="updated_at")
    private Date updatedAt;
}

我有一个实现 UserDetails 的 UserServiceImpl 类

然后我必须实现 loadUserByUsername

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    UserEntity userEntity = userRepository.findByEmail(email);
    if( userEntity == null) {
        throw new UsernameNotFoundException("User email is not in the database");
    } else {
        validateLoginAttempt(userEntity);
        log.info("Returning User : " + userEntity.getFirstName() + " " + userEntity.getLastName());
        userEntity.setLastLoginDateDisplay(userEntity.getLastLoginDate());
        userEntity.setLastLoginDate(new Date());
        userRepository.save(userEntity);
        return new UserPrincipal(userEntity);
    }
}

如果用户存在,我调用一个方法来验证身份验证。

private void validateLoginAttempt(UserEntity user) {
    if(user.getIsAccountNonLocked()) {
        if(loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
            user.setIsAccountNonLocked(Boolean.FALSE);
        } else {
            user.setIsAccountNonLocked(Boolean.TRUE);
        }
    } else {
        loginAttemptService.evictUserFromLoginAttemptCache(user.getEmail());
    }
}

此方法允许我检查用户帐户是否被锁定以及用户是否尝试连接太多次。

我的 LoginAttemptServiceImpl 如下:

@Service
public class LoginAttemptServiceImpl implements LoginAttemptService {
    public static final int MAXIMUM_AUTH_ATTEMPT = 5;
    public static final int AUTH_ATTEMPT_INCREMENT = 1;
    private LoadingCache<String, Integer> loginAttemptCache;
    private String username;

    public LoginAttemptServiceImpl() {
        super();
        loginAttemptCache = CacheBuilder.newBuilder()
                .expireAfterWrite(15, TimeUnit.MINUTES)
                .maximumSize(10000)
                .build(new CacheLoader<>() {
                    @Override
                    public Integer load(String key) {
                        return 0;
                    }
                });
    }

    @Override
    public void evictUserFromLoginAttemptCache(String username) {
        loginAttemptCache.invalidate(username);
    }

    @Override
    public void addUserToLoginAttemptCache(String username) {
        int attempts = 0;
        try {
            attempts = AUTH_ATTEMPT_INCREMENT + loginAttemptCache.get(username);
            loginAttemptCache.put(username, attempts);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean hasExceededMaxAttempts(String username) {
        try {
            return loginAttemptCache.get(username) >= MAXIMUM_AUTH_ATTEMPT;
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public int getLoginAttempts(String username) throws ExecutionException {
        return loginAttemptCache.get(username);
    }
}

我还实现了身份验证失败的事件监听器:

@Component
public class AuthenticationFailureListener {
    private final LoginAttemptService loginAttemptService;

    @Autowired
    public AuthenticationFailureListener(LoginAttemptService loginAttemptService) {
        this.loginAttemptService = loginAttemptService;
    }

    @EventListener
    public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
        Object principal = event.getAuthentication().getPrincipal();
        if (principal instanceof String) {
            String username = (String) event.getAuthentication().getPrincipal();
            loginAttemptService.addUserToLoginAttemptCache(username);
        }
    }
}

最后我的 AuthenticationFilter 允许我管理成功和不成功的响应:

@Override
protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain chain,
                                        Authentication authResult) throws IOException, ServletException {
    String userName = ((UserPrincipal)authResult.getPrincipal()).getUsername();
    // built the token
    String token = Jwts.builder()
            .setSubject(userName)
            .setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
            .signWith(SignatureAlgorithm.HS512, SecurityConstants.getTokenSecret())
            .compact();

    UserService userService = (UserService) SpringApplicationContext.getBean("userServiceImpl");
    UserDto userDto = userService.getUser(userName);

    response.addHeader(SecurityConstants.HEADER_STRING_USERID, userDto.getUserKeyId());
    response.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
}

@SneakyThrows
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
                                          HttpServletResponse response,
                                          AuthenticationException failed) throws IOException, ServletException {
    // super.unsuccessfulAuthentication(request, response, failed);
    int attempts;

    if(loginAttemptService.hasExceededMaxAttempts(this.username)) {
        attempts = loginAttemptService.getLoginAttempts(this.username);
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Attempt number " + attempts + ": Account is locked for 15 minutes");
    } else {
        attempts = loginAttemptService.getLoginAttempts(this.username);
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Attempt number " + attempts + ": " + (SecurityConstants.MAX_AUTH_ATTEMPTS - attempts) + " - before account is blocked");
    }
}

身份验证在成功时有效...我的问题涉及失败,我有 3 个问题:

  1. 我想在失败的情况下返回一个对象。 response.sendError 应该做的工作,但它没有。我还尝试返回一个 Json 响应:https://www.baeldung.com/servlet-json-response
  2. 我使用 Guava 缓存,但同时通过将 isAccountNonLocked 设置为 false 来更新数据库。清除缓存后,我想将值设置为 True。
  3. 我没有更新 unsuccessfulAuthentication 方法中的尝试计数。我的回答总是:尝试编号 0:5 - 在帐户被阻止之前

感谢您的帮助和阅读全文!

【问题讨论】:

  • 你的错误是什么?
  • 我没有错误信息。我只是在响应正文中没有任何响应...我没有检索尝试次数,并且我不知道在清理缓存时如何更新数据库。
  • 我希望您知道构建自己的自定义安全解决方案是不好的做法。
  • 我使用 Spring Security 来实现它。那么如何在不覆盖 Spring Security 方法的情况下处理不成功的身份验证呢?如果我必须有特定的行为来避免暴力攻击?如果我希望用户在 3 次虚假身份验证等后更新密码。

标签: spring spring-boot spring-security


【解决方案1】:

关于问题 1,您可以使用与您发布的链接中提到的方法类似的方法,但使用 response.getWriter().write(String) 和 Jackson 的 ObjectMapper,如下所示:

        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        ObjectMapper mapper = new ObjectMapper();
        
        response.getWriter().write(mapper.writeValueAsString( /*Your custom POJO here */ ));

【讨论】:

  • 不幸的是我也试过这个没有成功...
  • 认证失败时是否触发了unsuccessfulAuthentication?如果是这样,你在使用上面的代码时会报错吗?
【解决方案2】:

对于问题 2:我找到了解决它的技巧。我没有在清除缓存的同时更新数据库,而是在登录验证时进行更新...

private void validateLoginAttempt(UserEntity user) {
    if(user.getIsAccountNonLocked()) {
        if(loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
            user.setIsAccountNonLocked(Boolean.FALSE);
        } else {
            user.setIsAccountNonLocked(Boolean.TRUE);
        }
    } else {
        if(!loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
            user.setIsAccountNonLocked(Boolean.TRUE);
        }
        loginAttemptService.evictUserFromLoginAttemptCache(user.getEmail());
    }
}

【讨论】:

    【解决方案3】:

    对于问题 3: 在扩展 WebSecurityConfigurerAdapter 的 WebSecurity 类中,我实现了一个 bean,以便将它注入到我的 AuthenticationFilter 中。

    这是我的豆子:

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    

    这是我的 AuthenticationFilter 类。我最初将这个类添加为组件(产生错误消息的坏主意)。

    // @Component
    public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
        private final AuthenticationManager authenticationManager;
        private final LoginAttemptService loginAttemptService;
        private String username;
    
        public AuthenticationFilter(AuthenticationManager authenticationManager, LoginAttemptService loginAttemptService) {
            this.authenticationManager = authenticationManager;
            this.loginAttemptService = loginAttemptService;
        }
    ....
    

    【讨论】:

      猜你喜欢
      • 2015-07-28
      • 1970-01-01
      • 2014-09-15
      • 2022-08-16
      • 2013-11-13
      • 2016-02-07
      • 2012-08-15
      • 2021-04-08
      • 2015-05-13
      相关资源
      最近更新 更多