【发布时间】: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 个问题:
- 我想在失败的情况下返回一个对象。 response.sendError 应该做的工作,但它没有。我还尝试返回一个 Json 响应:https://www.baeldung.com/servlet-json-response
- 我使用 Guava 缓存,但同时通过将 isAccountNonLocked 设置为 false 来更新数据库。清除缓存后,我想将值设置为 True。
- 我没有更新 unsuccessfulAuthentication 方法中的尝试计数。我的回答总是:尝试编号 0:5 - 在帐户被阻止之前
感谢您的帮助和阅读全文!
【问题讨论】:
-
你的错误是什么?
-
我没有错误信息。我只是在响应正文中没有任何响应...我没有检索尝试次数,并且我不知道在清理缓存时如何更新数据库。
-
我希望您知道构建自己的自定义安全解决方案是不好的做法。
-
我使用 Spring Security 来实现它。那么如何在不覆盖 Spring Security 方法的情况下处理不成功的身份验证呢?如果我必须有特定的行为来避免暴力攻击?如果我希望用户在 3 次虚假身份验证等后更新密码。
标签: spring spring-boot spring-security