Merge branch 'master' into develop
This commit is contained in:
commit
198f01641f
148 changed files with 557 additions and 376 deletions
|
|
@ -0,0 +1,19 @@
|
|||
package ru.micord.ervu.audit.config;
|
||||
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* @author Adel Kalimullin
|
||||
*/
|
||||
public class AuditDisabledCondition implements Condition {
|
||||
private static final String AUDIT_ENABLED_PROPERTY_NAME = "audit.kafka.enabled";
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
Environment env = context.getEnvironment();
|
||||
return !Boolean.parseBoolean(env.getProperty(AUDIT_ENABLED_PROPERTY_NAME));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package ru.micord.ervu.audit.config;
|
||||
|
||||
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* @author Adel Kalimullin
|
||||
*/
|
||||
public class AuditEnabledCondition implements Condition {
|
||||
private static final String AUDIT_ENABLED_PROPERTY_NAME = "audit.kafka.enabled";
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
Environment env = context.getEnvironment();
|
||||
return Boolean.parseBoolean(env.getProperty(AUDIT_ENABLED_PROPERTY_NAME));
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import org.apache.kafka.common.config.SaslConfigs;
|
|||
import org.apache.kafka.common.serialization.StringSerializer;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.config.TopicBuilder;
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
|
||||
|
|
@ -22,6 +23,7 @@ import org.springframework.kafka.core.ProducerFactory;
|
|||
* @author Adel Kalimullin
|
||||
*/
|
||||
@Configuration
|
||||
@Conditional(AuditEnabledCondition.class)
|
||||
public class AuditKafkaConfig {
|
||||
@Value("${audit.kafka.bootstrap.servers}")
|
||||
private String bootstrapServers;
|
||||
|
|
@ -35,11 +37,11 @@ public class AuditKafkaConfig {
|
|||
private String password;
|
||||
@Value("${audit.kafka.sasl.mechanism}")
|
||||
private String saslMechanism;
|
||||
@Value("${audit.kafka.authorization.topic}")
|
||||
@Value("${audit.kafka.authorization.topic:ervu.lkrp.auth.events}")
|
||||
private String authorizationTopic;
|
||||
@Value("${audit.kafka.action.topic}")
|
||||
@Value("${audit.kafka.action.topic:ervu.lkrp.action.events}")
|
||||
private String actionTopic;
|
||||
@Value("${audit.kafka.file.download.topic}")
|
||||
@Value("${audit.kafka.file.download.topic:ervu.lkrp.import.file}")
|
||||
private String fileDownloadTopic;
|
||||
|
||||
@Bean("auditProducerFactory")
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@ package ru.micord.ervu.audit.service.impl;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.micord.ervu.audit.config.AuditEnabledCondition;
|
||||
import ru.micord.ervu.audit.service.AuditKafkaPublisher;
|
||||
|
||||
/**
|
||||
* @author Adel Kalimullin
|
||||
*/
|
||||
@Service
|
||||
@Conditional(AuditEnabledCondition.class)
|
||||
public class BaseAuditKafkaPublisher implements AuditKafkaPublisher {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BaseAuditKafkaPublisher.class);
|
||||
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||
@Value("${audit.kafka.enabled}")
|
||||
private boolean auditEnabled;
|
||||
|
||||
public BaseAuditKafkaPublisher(
|
||||
@Qualifier("auditTemplate") KafkaTemplate<String, String> kafkaTemplate) {
|
||||
|
|
@ -25,7 +25,6 @@ public class BaseAuditKafkaPublisher implements AuditKafkaPublisher {
|
|||
|
||||
@Override
|
||||
public void publishEvent(String topic, String message) {
|
||||
if (auditEnabled) {
|
||||
kafkaTemplate.send(topic, message)
|
||||
.addCallback(
|
||||
result -> {
|
||||
|
|
@ -35,8 +34,4 @@ public class BaseAuditKafkaPublisher implements AuditKafkaPublisher {
|
|||
)
|
||||
);
|
||||
}
|
||||
else {
|
||||
LOGGER.info("Audit is disabled. Event not published.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.micord.ervu.audit.config.AuditEnabledCondition;
|
||||
import ru.micord.ervu.audit.constants.AuditConstants;
|
||||
import ru.micord.ervu.audit.model.AuditActionEvent;
|
||||
import ru.micord.ervu.audit.model.AuditActionRequest;
|
||||
|
|
@ -22,15 +24,16 @@ import ru.micord.ervu.util.NetworkUtils;
|
|||
* @author Adel Kalimullin
|
||||
*/
|
||||
@Service
|
||||
@Conditional(AuditEnabledCondition.class)
|
||||
public class BaseAuditService implements AuditService {
|
||||
private final AuditKafkaPublisher auditPublisher;
|
||||
private final JwtTokenService jwtTokenService;
|
||||
private final ObjectMapper objectMapper;
|
||||
@Value("${audit.kafka.authorization.topic}")
|
||||
@Value("${audit.kafka.authorization.topic:ervu.lkrp.auth.events}")
|
||||
private String authorizationTopic;
|
||||
@Value("${audit.kafka.action.topic}")
|
||||
@Value("${audit.kafka.action.topic:ervu.lkrp.action.events}")
|
||||
private String actionTopic;
|
||||
@Value("${audit.kafka.file.download.topic}")
|
||||
@Value("${audit.kafka.file.download.topic:ervu.lkrp.import.file}")
|
||||
private String fileDownloadTopic;
|
||||
|
||||
public BaseAuditService(AuditKafkaPublisher auditPublisher, JwtTokenService jwtTokenService,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package ru.micord.ervu.audit.service.impl;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.micord.ervu.audit.config.AuditDisabledCondition;
|
||||
import ru.micord.ervu.audit.model.AuditActionRequest;
|
||||
import ru.micord.ervu.audit.service.AuditService;
|
||||
import ru.micord.ervu.security.esia.model.PersonModel;
|
||||
|
||||
/**
|
||||
* @author Adel Kalimullin
|
||||
*/
|
||||
@Service
|
||||
@Conditional(AuditDisabledCondition.class)
|
||||
public class StubAuditService implements AuditService {
|
||||
|
||||
@Override
|
||||
public void processActionEvent(HttpServletRequest request,
|
||||
AuditActionRequest auditActionRequest) {}
|
||||
|
||||
@Override
|
||||
public void processAuthEvent(HttpServletRequest request, PersonModel personModel, String status,
|
||||
String eventType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processDownloadEvent(HttpServletRequest request, int fileSize, String fileName,
|
||||
String formatRegistry, String status) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package ru.micord.ervu.security.esia;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ru.micord.ervu.security.esia.exception.EsiaException;
|
||||
import ru.micord.ervu.security.esia.model.ExpiringState;
|
||||
import ru.micord.ervu.security.esia.model.ExpiringToken;
|
||||
|
||||
|
|
@ -16,7 +19,7 @@ public class EsiaAuthInfoStore {
|
|||
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||
private static final Map<String, ExpiringToken> ACCESS_TOKENS_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<String, ExpiringToken> REFRESH_TOKENS_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<String, ExpiringState> PRNS_UUID_STATE_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<String, List<ExpiringState>> PRNS_UUID_STATE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
public static void addAccessToken(String prnOid, String token, long expiresIn) {
|
||||
if (token != null) {
|
||||
|
|
@ -79,13 +82,34 @@ public class EsiaAuthInfoStore {
|
|||
REFRESH_TOKENS_MAP.remove(prnOid);
|
||||
}
|
||||
|
||||
public static void addState(String prnsUUID, String state, long expiresIn) {
|
||||
public static void addState(String prnsUUID, String state, long expiresIn, long attemptsCount) {
|
||||
long expiryTime = System.currentTimeMillis() + expiresIn * 1000L;
|
||||
PRNS_UUID_STATE_MAP.put(prnsUUID, new ExpiringState(state, expiryTime));
|
||||
ExpiringState newState = new ExpiringState(state, expiryTime);
|
||||
PRNS_UUID_STATE_MAP.compute(prnsUUID, (key, states) -> {
|
||||
if (states == null) {
|
||||
states = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
if (states.size() >= attemptsCount) {
|
||||
throw new EsiaException("The number of login attempts has been exceeded.");
|
||||
}
|
||||
states.add(newState);
|
||||
return states;
|
||||
});
|
||||
}
|
||||
|
||||
public static String getState(String prnsUUID) {
|
||||
return PRNS_UUID_STATE_MAP.get(prnsUUID).getState();
|
||||
public static boolean containsState(String prnsUUID, String state) {
|
||||
List<ExpiringState> states = PRNS_UUID_STATE_MAP.get(prnsUUID);
|
||||
if (states == null) {
|
||||
return false;
|
||||
}
|
||||
long currentTime = System.currentTimeMillis();
|
||||
states.removeIf(expiringState -> expiringState.getExpiryTime() < currentTime);
|
||||
for (ExpiringState expiringState : states) {
|
||||
if (expiringState.getState().equals(state)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void removeState(String prnsUUID) {
|
||||
|
|
@ -94,10 +118,10 @@ public class EsiaAuthInfoStore {
|
|||
|
||||
public static void removeExpiredState() {
|
||||
for (String key : PRNS_UUID_STATE_MAP.keySet()) {
|
||||
ExpiringState state = PRNS_UUID_STATE_MAP.get(key);
|
||||
if (state != null && state.isExpired()) {
|
||||
PRNS_UUID_STATE_MAP.remove(key);
|
||||
}
|
||||
PRNS_UUID_STATE_MAP.computeIfPresent(key, (k, states) -> {
|
||||
states.removeIf(ExpiringState::isExpired);
|
||||
return states.isEmpty() ? null : states;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,9 @@ public class EsiaConfig {
|
|||
@Value("${esia.state.cookie.life.time:300}")
|
||||
private long esiaStateCookieLifeTime;
|
||||
|
||||
@Value("${esia.login.attempts.count:5}")
|
||||
private long esiaLoginAttemptsCount;
|
||||
|
||||
|
||||
public String getEsiaScopes() {
|
||||
String[] scopeItems = esiaScopes.split(",");
|
||||
|
|
@ -123,4 +126,8 @@ public class EsiaConfig {
|
|||
public long getEsiaStateCookieLifeTime() {
|
||||
return esiaStateCookieLifeTime;
|
||||
}
|
||||
|
||||
public long getEsiaLoginAttemptsCount() {
|
||||
return esiaLoginAttemptsCount;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ public class EsiaController {
|
|||
private JwtTokenService jwtTokenService;
|
||||
|
||||
@RequestMapping(value = "/esia/url")
|
||||
public String getEsiaUrl(HttpServletResponse response) {
|
||||
return esiaAuthService.generateAuthCodeUrl(response);
|
||||
public String getEsiaUrl(HttpServletResponse response, HttpServletRequest request) {
|
||||
return esiaAuthService.generateAuthCodeUrl(response, request);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/esia/auth")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import java.time.LocalDateTime;
|
|||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
|
@ -30,6 +31,8 @@ import org.springframework.beans.factory.annotation.Value;
|
|||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import ru.micord.ervu.audit.constants.AuditConstants;
|
||||
import ru.micord.ervu.audit.service.AuditService;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
import ru.micord.ervu.audit.constants.AuditConstants;
|
||||
import ru.micord.ervu.audit.service.AuditService;
|
||||
|
|
@ -97,7 +100,7 @@ public class EsiaAuthService {
|
|||
@Value("${ervu.kafka.request.topic}")
|
||||
private String requestTopic;
|
||||
|
||||
public String generateAuthCodeUrl(HttpServletResponse response) {
|
||||
public String generateAuthCodeUrl(HttpServletResponse response, HttpServletRequest request) {
|
||||
try {
|
||||
String clientId = esiaConfig.getClientId();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
|
||||
|
|
@ -105,6 +108,10 @@ public class EsiaAuthService {
|
|||
String timestamp = dt.format(formatter);
|
||||
String state = UUID.randomUUID().toString();
|
||||
String prnsUUID = UUID.randomUUID().toString();
|
||||
Cookie oldPrnsCookie = WebUtils.getCookie(request, PRNS_UUID);
|
||||
if (oldPrnsCookie != null) {
|
||||
prnsUUID = oldPrnsCookie.getValue();
|
||||
}
|
||||
String redirectUrl = esiaConfig.getRedirectUrl();
|
||||
String redirectUrlEncoded = redirectUrl.replaceAll(":", "%3A")
|
||||
.replaceAll("/", "%2F");
|
||||
|
|
@ -118,7 +125,7 @@ public class EsiaAuthService {
|
|||
parameters.put("redirect_uri", esiaConfig.getRedirectUrl());
|
||||
|
||||
String clientSecret = signMap(parameters);
|
||||
EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime());
|
||||
EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime(), esiaConfig.getEsiaLoginAttemptsCount());
|
||||
ResponseCookie prnsCookie = securityHelper.createCookie(PRNS_UUID, prnsUUID, "/")
|
||||
.maxAge(esiaConfig.getEsiaStateCookieLifeTime())
|
||||
.build();
|
||||
|
|
@ -330,6 +337,9 @@ public class EsiaAuthService {
|
|||
tokenResponse != null ? tokenResponse.getErrorDescription() : "response is empty";
|
||||
throw new IllegalStateException("Esia response error. " + errMsg);
|
||||
}
|
||||
if (!tokenResponse.getState().equals(state)) {
|
||||
throw new EsiaException("Token invalid. State from request not equals with state from response.");
|
||||
}
|
||||
String esiaAccessTokenStr = tokenResponse.getAccessToken();
|
||||
String verifyResult = verifyToken(esiaAccessTokenStr);
|
||||
if (verifyResult != null) {
|
||||
|
|
@ -385,6 +395,7 @@ public class EsiaAuthService {
|
|||
|
||||
public String logout(HttpServletRequest request, HttpServletResponse response) {
|
||||
PersonModel personModel = null;
|
||||
String status = null;
|
||||
try {
|
||||
String userId = jwtTokenService.getUserAccountId(request);
|
||||
String accessToken = EsiaAuthInfoStore.getAccessToken(userId);
|
||||
|
|
@ -399,18 +410,19 @@ public class EsiaAuthService {
|
|||
"client_id", esiaConfig.getClientId(),
|
||||
"redirect_url", redirectUrl
|
||||
);
|
||||
auditService.processAuthEvent(
|
||||
request, personModel, AuditConstants.SUCCESS_STATUS, AuditConstants.LOGOUT_EVENT_TYPE
|
||||
);
|
||||
status = AuditConstants.SUCCESS_STATUS;
|
||||
return buildUrl(url, params);
|
||||
}
|
||||
catch (Exception e) {
|
||||
status = AuditConstants.FAILURE_STATUS;
|
||||
throw new EsiaException(e);
|
||||
}
|
||||
finally {
|
||||
if (personModel != null){
|
||||
auditService.processAuthEvent(
|
||||
request, personModel, AuditConstants.FAILURE_STATUS, AuditConstants.LOGOUT_EVENT_TYPE
|
||||
request, personModel, status, AuditConstants.LOGOUT_EVENT_TYPE
|
||||
);
|
||||
}
|
||||
throw new EsiaException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -475,8 +487,11 @@ public class EsiaAuthService {
|
|||
ZoneId.systemDefault()
|
||||
);
|
||||
LocalDateTime currentTime = LocalDateTime.now();
|
||||
if (!currentTime.isAfter(iatTime) || !expTime.isAfter(iatTime)) {
|
||||
return "Token invalid. Token expired";
|
||||
if (currentTime.getNano() > 0) {
|
||||
currentTime = currentTime.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS);
|
||||
}
|
||||
if (currentTime.isBefore(iatTime) || expTime.isBefore(iatTime) || currentTime.isAfter(expTime)) {
|
||||
return "Token invalid. Token expired, current: " + currentTime + " iat: " + iatTime + " exp: " + expTime;
|
||||
}
|
||||
HttpResponse<String> response = signVerify(accessToken);
|
||||
if (response.statusCode() != 200) {
|
||||
|
|
@ -512,8 +527,7 @@ public class EsiaAuthService {
|
|||
return "State invalid. Cookie not found";
|
||||
}
|
||||
String prnsUUID = cookie.getValue();
|
||||
String oldState = EsiaAuthInfoStore.getState(prnsUUID);
|
||||
if (oldState == null || !oldState.equals(state)) {
|
||||
if (!EsiaAuthInfoStore.containsState(prnsUUID, state)) {
|
||||
return "State invalid. State from ESIA not equals with state before";
|
||||
}
|
||||
EsiaAuthInfoStore.removeState(prnsUUID);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
error.unknown=The system is temporarily unavailable. Please try again later.
|
||||
error.unknown=The system is temporarily unavailable. Please try again later.
|
||||
Loading…
Add table
Add a link
Reference in a new issue