diff --git a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java index c1bc4e1f..40ec1f94 100644 --- a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java +++ b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java @@ -24,8 +24,8 @@ import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import ru.micord.ervu.security.esia.token.EsiaTokensStore; import ru.micord.ervu.exception.JsonParsingException; -import ru.micord.ervu.security.esia.token.TokensStore; import ru.micord.ervu.security.esia.model.EmployeeModel; import ru.micord.ervu.security.esia.model.PersonModel; import ru.micord.ervu.security.esia.service.UlDataService; @@ -72,7 +72,7 @@ public class EmployeeInfoFileUploadService { String[] ids = userAccountId.split(":"); String userId = ids[0]; String ervuId = ids[1]; - String accessToken = TokensStore.getAccessToken(userId); + String accessToken = EsiaTokensStore.getAccessToken(userId); EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken); PersonModel personModel = employeeModel.getPerson(); diff --git a/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java b/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java index 9993b234..3867b41a 100644 --- a/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java +++ b/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java @@ -25,9 +25,7 @@ public class LogoutSuccessHandler public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String url = esiaAuthService.logout(request, response); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write(url); - response.getWriter().flush(); + response.sendRedirect(url); CsrfToken csrfToken = this.csrfTokenRepository.generateToken(request); this.csrfTokenRepository.saveToken(csrfToken, request, response); } diff --git a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java index 5f088eaa..bfc8d16a 100644 --- a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java @@ -105,10 +105,9 @@ public class SecurityConfig { @Bean public JwtAuthenticationFilter jwtAuthenticationFilter(SecurityHelper securityHelper, - AuthenticationManager manager, - JwtTokenService jwtTokenService) { + AuthenticationManager manager) { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter( - new JwtMatcher("/**", PERMIT_ALL), entryPoint(), securityHelper, jwtTokenService); + new JwtMatcher("/**", PERMIT_ALL), entryPoint(), securityHelper); jwtAuthenticationFilter.setAuthenticationManager(manager); return jwtAuthenticationFilter; } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java b/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java index 2dc2b2db..937368d7 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java @@ -29,6 +29,9 @@ public class EsiaConfig { @Value("${esia.redirect.url}") private String redirectUrl; + @Value("${esia.logout.redirect.url}") + private String logoutRedirectUrl; + @Value("${sign.url}") private String signUrl; @@ -107,6 +110,10 @@ public class EsiaConfig { return esiaTokenUrl; } + public String getLogoutRedirectUrl() { + return logoutRedirectUrl; + } + public String getSignVerifyUrl() { return signVerifyUrl; } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java index d08e460e..9e0e3c12 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java @@ -17,15 +17,14 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.UUID; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; import ervu.service.okopf.OkopfService; import org.springframework.security.authentication.AuthenticationManager; +import ru.micord.ervu.security.esia.token.EsiaTokensStore; import ru.micord.ervu.security.esia.exception.EsiaException; -import ru.micord.ervu.security.esia.token.TokensStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -127,7 +126,7 @@ public class EsiaAuthService { "obj_type", "B L F A", "client_certificate_hash", esiaConfig.getClientCertHash()); - return makeRequest(url, params); + return buildUrl(url, params); } catch (Exception e) { throw new EsiaException(e); @@ -154,12 +153,13 @@ public class EsiaAuthService { } } - private static String makeRequest(URL url, Map params) { + private static String buildUrl(URL url, Map params) { StringBuilder uriBuilder = new StringBuilder(url.toString()); uriBuilder.append('?'); for (Map.Entry node : params.entrySet()) { uriBuilder.append(node.getKey()).append('=').append(node.getValue()).append("&"); } + uriBuilder.deleteCharAt(uriBuilder.length() - 1); return uriBuilder.toString(); } @@ -238,8 +238,8 @@ public class EsiaAuthService { String prnOid = esiaAccessToken.getSbjId(); String ervuId = getErvuId(esiaAccessTokenStr, prnOid); Long expiresIn = tokenResponse.getExpiresIn(); - TokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); - TokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); + EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); + EsiaTokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); createTokenAndAddCookie(response, esiaAccessToken.getSbjId(), ervuId, hasRole, expiresIn); if (!hasRole) { LOGGER.error("The user with id = " + prnOid + " does not have the required role"); @@ -321,8 +321,8 @@ public class EsiaAuthService { EsiaAccessToken esiaAccessToken = ulDataService.readToken(accessToken); String prnOid = esiaAccessToken.getSbjId(); Long expiresIn = tokenResponse.getExpiresIn(); - TokensStore.addAccessToken(prnOid, accessToken, expiresIn); - TokensStore.addRefreshToken(prnOid, newRefreshToken, expiresIn); + EsiaTokensStore.addAccessToken(prnOid, accessToken, expiresIn); + EsiaTokensStore.addRefreshToken(prnOid, newRefreshToken, expiresIn); String ervuId = getErvuId(accessToken, prnOid); createTokenAndAddCookie(response, esiaAccessToken.getSbjId(), ervuId, true, expiresIn); } @@ -367,15 +367,15 @@ public class EsiaAuthService { try { securityHelper.clearAccessCookies(response); String userId = jwtTokenService.getUserAccountId(request); - TokensStore.removeAccessToken(userId); - TokensStore.removeRefreshToken(userId); + EsiaTokensStore.removeAccessToken(userId); + EsiaTokensStore.removeRefreshToken(userId); String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl(); - String redirectUrl = esiaConfig.getRedirectUrl(); + String redirectUrl = esiaConfig.getLogoutRedirectUrl(); URL url = new URL(logoutUrl); Map params = mapOf( "client_id", esiaConfig.getClientId(), "redirect_url", redirectUrl); - return makeRequest(url, params); + return buildUrl(url, params); } catch (Exception e) { throw new EsiaException(e); @@ -471,8 +471,7 @@ public class EsiaAuthService { private void createTokenAndAddCookie(HttpServletResponse response, String userId, String ervuId, Boolean hasRole, Long expiresIn) { Token token = jwtTokenService.createAccessToken(userId, expiresIn, ervuId, hasRole); - Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiresIn.intValue()); - response.addCookie(accessCookie); + securityHelper.addAccessCookies(response,token.getValue(), expiresIn.intValue()); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); SecurityContext context = SecurityContextHolder.createEmptyContext(); @@ -481,8 +480,6 @@ public class EsiaAuthService { context.setAuthentication(authentication); authenticationManager.authenticate(authentication); SecurityContextHolder.setContext(context); - Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiresIn.intValue()); - response.addCookie(authMarkerCookie); } private String verifyToken(String accessToken) { diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensStore.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java similarity index 71% rename from backend/src/main/java/ru/micord/ervu/security/esia/token/TokensStore.java rename to backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java index 33302e4d..e16d48ac 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensStore.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java @@ -1,12 +1,18 @@ package ru.micord.ervu.security.esia.token; +import java.lang.invoke.MethodHandles; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.CredentialsExpiredException; + /** * @author Eduard Tihomirov */ -public class TokensStore { +public class EsiaTokensStore { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final Map ACCESS_TOKENS_MAP = new ConcurrentHashMap<>(); private static final Map REFRESH_TOKENS_MAP = new ConcurrentHashMap<>(); @@ -21,6 +27,19 @@ public class TokensStore { return ACCESS_TOKENS_MAP.get(prnOid).getAccessToken(); } + public static boolean validateAccessToken(String prnOid) { + ExpiringToken token = ACCESS_TOKENS_MAP.get(prnOid); + if (token == null || token.getAccessToken() == null) { + LOGGER.error("No ESIA access token for prnOid: " + prnOid); + return false; + } + else if (token.isExpired()) { + LOGGER.error("ESIA access token expired for prnOid: " + prnOid); + return false; + } + return true; + } + public static void removeExpiredAccessToken() { for (String key : ACCESS_TOKENS_MAP.keySet()) { ExpiringToken token = ACCESS_TOKENS_MAP.get(key); diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java index 46652958..89c9db4c 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java @@ -14,7 +14,7 @@ public class TokensClearShedulerService { @SchedulerLock(name = "clearToken") @Transactional public void load() { - TokensStore.removeExpiredRefreshToken(); - TokensStore.removeExpiredAccessToken(); + EsiaTokensStore.removeExpiredRefreshToken(); + EsiaTokensStore.removeExpiredAccessToken(); } } diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java index f709679f..f497a4c9 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java @@ -2,6 +2,8 @@ package ru.micord.ervu.security.webbpm.jwt; import java.util.Collections; +import javax.servlet.http.HttpServletRequest; + import io.jsonwebtoken.ExpiredJwtException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; @@ -11,9 +13,13 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; import ru.micord.ervu.security.webbpm.jwt.model.Token; import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; +import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST; + @Component public class JwtAuthenticationProvider implements AuthenticationProvider { @@ -42,16 +48,26 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { throw new BadCredentialsException("Authentication Failed.", e); } - if (!jwtTokenService.isValid(token)) { - throw new BadCredentialsException("Auth token is not valid for user " + token.getUserAccountId()); + if (jwtTokenService.isValid(token)) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( + REFERENCE_REQUEST); + if (request == null) { + throw new IllegalStateException("No request found in request attributes"); + } + + if (request.getRequestURI().endsWith("esia/logout") || token.getHasRole()) { + UsernamePasswordAuthenticationToken pwdToken = + UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null, + Collections.emptyList() + ); + + return new JwtAuthentication(pwdToken, token.getUserAccountId(), token.getValue()); + } } - UsernamePasswordAuthenticationToken pwdToken = - UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null, - Collections.emptyList() - ); - - return new JwtAuthentication(pwdToken, token.getUserAccountId(), token.getValue()); + throw new BadCredentialsException( + "Auth token is not valid for user " + token.getUserAccountId()); } @Override diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java index 61580652..6ba1b098 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java @@ -7,10 +7,8 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import io.jsonwebtoken.ExpiredJwtException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; @@ -19,8 +17,6 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro import org.springframework.security.web.util.matcher.RequestMatcher; import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper; -import ru.micord.ervu.security.webbpm.jwt.model.Token; -import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.extractAuthToken; @@ -36,22 +32,24 @@ public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFil private final SecurityHelper securityHelper; - private final JwtTokenService jwtTokenService; - public JwtAuthenticationFilter(RequestMatcher requestMatcher, AuthenticationEntryPoint entryPoint, - SecurityHelper securityHelper, - JwtTokenService jwtTokenService) { + SecurityHelper securityHelper) { super(requestMatcher); this.entryPoint = entryPoint; this.securityHelper = securityHelper; - this.jwtTokenService = jwtTokenService; } @Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException { + String browser = httpServletRequest.getHeader("User-Agent"); + + if (browser == null || (!browser.contains("YaBrowser") && !browser.contains("Chromium GOST"))) { + LOGGER.error("Invalid browser. Use YaBrowser or Chromium GOST"); + return clearCookieAndSetStatus(httpServletResponse); + } String tokenStr = extractAuthToken(httpServletRequest); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { @@ -59,22 +57,10 @@ public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFil } try { authentication = getAuthenticationManager().authenticate(authentication); - String browser = httpServletRequest.getHeader("User-Agent"); - if (browser == null || (!browser.contains("YaBrowser") && !browser.contains("Chromium GOST"))) { - throw new CredentialsExpiredException("Invalid browser. Use YaBrowser or Chromium GOST"); - } - if (!httpServletRequest.getRequestURI().endsWith("esia/logout")) { - Token token = jwtTokenService.getToken(tokenStr); - if (!token.getHasRole()) { - throw new CredentialsExpiredException("Invalid token. User has no required role"); - } - } } - catch (CredentialsExpiredException e) { - securityHelper.clearAccessCookies(httpServletResponse); - httpServletResponse.setStatus(401); + catch (AuthenticationException e) { LOGGER.warn(e.getMessage()); - return null; + return clearCookieAndSetStatus(httpServletResponse); } return authentication; } @@ -96,4 +82,10 @@ public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFil SecurityContextHolder.clearContext(); entryPoint.commence(request, response, exception); } + + private Authentication clearCookieAndSetStatus(HttpServletResponse httpServletResponse) { + securityHelper.clearAccessCookies(httpServletResponse); + httpServletResponse.setStatus(401); + return null; + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java index 0a222c79..9c966f60 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java @@ -1,43 +1,89 @@ package ru.micord.ervu.security.webbpm.jwt.helper; -import javax.servlet.http.Cookie; +import java.net.IDN; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; -import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST; import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.AUTH_MARKER; import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.AUTH_TOKEN; -import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.createCookie; public final class SecurityHelper { @Value("${cookie.path:#{null}}") private String accessCookiePath; + @Value("${cookie.domain:#{null}}") + private String accessCookieDomain; + @Value("${cookie.secure:false}") + private boolean accessCookieSecure; + @Value("${cookie.same.site:Lax}") + private String accessCookieSameSite; + + @PostConstruct + private void init() { + + if (accessCookieDomain != null) { + accessCookieDomain = IDN.toASCII(accessCookieDomain); + } + } public void clearAccessCookies(HttpServletResponse response) { - Cookie tokenCookie = createCookie(AUTH_TOKEN, null, null); - tokenCookie.setMaxAge(0); - tokenCookie.setPath(accessCookiePath); - tokenCookie.setHttpOnly(true); - response.addCookie(tokenCookie); + ResponseCookie emptyAuthToken = createCookie(AUTH_TOKEN, null, accessCookiePath) + .maxAge(0).build(); + addResponseCookie(response, emptyAuthToken); - Cookie markerCookie = createCookie(AUTH_MARKER, null, null); - markerCookie.setMaxAge(0); - markerCookie.setPath("/"); - response.addCookie(markerCookie); + ResponseCookie emptyAuthMarker = createCookie(AUTH_MARKER, null, "/") + .maxAge(0) + .secure(false) + .httpOnly(false) + .build(); + addResponseCookie(response, emptyAuthMarker); } - public Cookie createAccessCookie(String cookieValue, int expiry) { - Cookie authToken = createCookie(SecurityUtil.AUTH_TOKEN, cookieValue, accessCookiePath); - authToken.setPath(accessCookiePath); - authToken.setMaxAge(expiry); - return authToken; + private void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } - public Cookie createAuthMarkerCookie(String cookieValue, int expiry) { - Cookie marker = createCookie(AUTH_MARKER, cookieValue, "/"); - marker.setMaxAge(expiry); - marker.setHttpOnly(false); - return marker; + public void addAccessCookies(HttpServletResponse response, String cookieValue, int expiry) { + ResponseCookie authTokenCookie = createCookie(AUTH_TOKEN, cookieValue, accessCookiePath) + .maxAge(expiry) + .build(); + addResponseCookie(response, authTokenCookie); + + ResponseCookie authMarker = createCookie(AUTH_MARKER, "true", "/") + .maxAge(expiry) + .secure(false) + .httpOnly(false) + .build(); + addResponseCookie(response, authMarker); + } + + public ResponseCookie.ResponseCookieBuilder createCookie(String name, String value, String path) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + + if (requestAttributes == null) { + throw new IllegalStateException("Must be called only in request context"); + } + HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( + REFERENCE_REQUEST); + + if (request == null) { + throw new IllegalStateException("Must be called only in request context"); + } + String cookieValue = value != null ? URLEncoder.encode(value, StandardCharsets.UTF_8) : ""; + return ResponseCookie.from(name, cookieValue) + .path(path != null ? path : request.getContextPath()) + .httpOnly(true) + .domain(accessCookieDomain) + .secure(accessCookieSecure) + .sameSite(accessCookieSameSite); } } diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java index 6676210d..db9a7e81 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java @@ -14,8 +14,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import ru.micord.ervu.security.esia.token.EsiaTokensStore; import ru.micord.ervu.security.exception.UnauthorizedException; -import ru.micord.ervu.security.esia.token.TokensStore; import ru.micord.ervu.security.webbpm.jwt.model.Token; import ru.cg.webbpm.modules.resources.api.ResourceMetadataUtils; @@ -67,7 +67,8 @@ public class JwtTokenService { LOGGER.info("Token {} is expired ", token.getValue()); return false; } - return true; + String[] ids = token.getUserAccountId().split(":"); + return EsiaTokensStore.validateAccessToken(ids[0]); } public Token getToken(String token) { @@ -80,11 +81,11 @@ public class JwtTokenService { } public String getAccessToken(HttpServletRequest request) { - return TokensStore.getAccessToken(getUserAccountId(request)); + return EsiaTokensStore.getAccessToken(getUserAccountId(request)); } public String getRefreshToken(HttpServletRequest request) { - return TokensStore.getRefreshToken(getUserAccountId(request)); + return EsiaTokensStore.getRefreshToken(getUserAccountId(request)); } public String getUserAccountId(HttpServletRequest request) { diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java index 34f12d18..2bd47616 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java @@ -1,20 +1,14 @@ package ru.micord.ervu.security.webbpm.jwt.util; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.Optional; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.util.WebUtils; import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; -import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST; - public final class SecurityUtil { public static final String AUTH_TOKEN = "auth_token"; @@ -24,24 +18,6 @@ public final class SecurityUtil { //empty } - public static Cookie createCookie(String name, String value, String path) { - String cookieValue = value != null ? URLEncoder.encode(value, StandardCharsets.UTF_8) : null; - Cookie cookie = new Cookie(name, cookieValue); - RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); - HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( - REFERENCE_REQUEST); - - if (path != null) { - cookie.setPath(path); - } - else { - cookie.setPath(request.getContextPath()); - } - cookie.setHttpOnly(true); - - return cookie; - } - public static String extractAuthToken(HttpServletRequest httpRequest) { Cookie cookie = WebUtils.getCookie(httpRequest, AUTH_TOKEN); return cookie != null ? cookie.getValue() : null; diff --git a/config.md b/config.md index 23312e90..721ab25a 100644 --- a/config.md +++ b/config.md @@ -768,9 +768,13 @@ JBPM использует 3 корневых категории логирова - `ESIA_CLIENT_ID` - – идентификатор системы-клиента (мнемоника системы в ЕСИА указанная прописными буквами) - `ESIA_REDIRECT_URL` - ссылка, по которой должен быть направлен пользователь после того, как ЕСИА даст разрешение на доступ к ресурсу. -Важно: ESIA_REDIRECT_URL должна содержать полный адрес вплоть до последнего слэша +Важно: `ESIA_REDIRECT_URL` должна содержать полный адрес вплоть до последнего слэша - https://lkul.ervu.loc/ - правильное значение параметра - https://lkul.ervu.loc - неправильное значение параметра +- `ESIA_LOGOUT_REDIRECT_URL` - ссылка, по которой должен быть направлен пользователь после logout-a + Важно: `ESIA_LOGOUT_REDIRECT_URL` должна содержать полный адрес вплоть до последнего слэша: +> - https://lkul.ervu.loc/home.html - правильное значение параметра +> - https://lkul.ervu.loc - неправильное значение параметра - `ESIA_UPLOAD_DATA_ROLE` - мнемоника группы, для роли "Сотрудник, ответственный за военно-учетную работу". - `SIGN_URL` - url для подписания с помощью КриптоПро секрета клиента, необходимого для аутентификации через ЕСИА. diff --git a/config/micord.env b/config/micord.env index 1c976304..eccfd664 100644 --- a/config/micord.env +++ b/config/micord.env @@ -22,6 +22,7 @@ ESIA_BASE_URI=https://esia-portal1.test.gosuslugi.ru/ ESIA_ISSUER_URL=http://esia-portal1.test.gosuslugi.ru/ ESIA_CLIENT_ID=MNSV89 ESIA_REDIRECT_URL=https://lkrp-dev.micord.ru/ul/ +ESIA_LOGOUT_REDIRECT_URL=https://lkrp-dev.micord.ru/ul/home.html ESIA_UPLOAD_DATA_ROLE=MNSV89_UPLOAD_DATA SIGN_URL=https://ervu-sign-dev.k8s.micord.ru/sign SIGN_VERIFY_URL=https://ervu-sign-dev.k8s.micord.ru/verify diff --git a/config/nginx.conf b/config/nginx.conf index b55fbded..93bb7ad5 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -78,12 +78,7 @@ http { root /frontend; index index.html; try_files $uri @index; - - #Application config - location = /src/resources/app-config.json { - add_header Cache-Control "no-cache"; - expires 0; - } + # Media: images, icons, video, audio, HTC location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|mp3|ogg|ogv|webm|htc|woff2|woff|ttf)$ { expires 1M; @@ -101,7 +96,7 @@ http { location @index { root /frontend; - add_header Cache-Control "no-cache"; + add_header Cache-Control no-cache; expires 0; try_files /index.html =404; } diff --git a/frontend/home.html b/frontend/home.html index f3d6eb92..b613bfba 100644 --- a/frontend/home.html +++ b/frontend/home.html @@ -31,7 +31,7 @@
-
Личный кабинет для ведения воинского учета в организациях
+
Личный кабинет для ведения воинского учета в организациях
Кому доступен личный кабинет?
diff --git a/frontend/index.webpack.html b/frontend/index.webpack.html index 54801a3c..7b846a88 100644 --- a/frontend/index.webpack.html +++ b/frontend/index.webpack.html @@ -4,6 +4,9 @@ Личный кабинет юр.лица + + diff --git a/frontend/src/resources/template/app/component/log_out.html b/frontend/src/resources/template/app/component/log_out.html index 996c245f..12503171 100644 --- a/frontend/src/resources/template/app/component/log_out.html +++ b/frontend/src/resources/template/app/component/log_out.html @@ -1,7 +1,10 @@ +
{{getOrgUnitName()}}
Данные организации - +
- + diff --git a/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts b/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts index c9ff00c5..0d4b7db6 100644 --- a/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts +++ b/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts @@ -1,4 +1,5 @@ import { + Event, GridColumnIdUtils, GridRow, GridRowModelType, @@ -7,7 +8,7 @@ import { } from "@webbpm/base-package"; import {ChangeDetectionStrategy, Component} from "@angular/core"; import { - ColDef, + ColDef, FilterChangedEvent, ICellRendererParams, ITooltipParams, ValueFormatterParams, @@ -24,6 +25,9 @@ import {StaticGridColumn} from "../../../generated/ru/micord/ervu/property/grid/ changeDetection: ChangeDetectionStrategy.OnPush }) export class InMemoryStaticGrid extends GridV2 { + // todo: remove on updating platform version up to 3.188 + @Visible("false") + public columnFiltersChanged: Event = new Event(); private rpcService: InMemoryStaticGridRpcService; @@ -109,4 +113,17 @@ export class InMemoryStaticGrid extends GridV2 { public getRowDataSize(): number { return this.rowData ? this.rowData.length : 0; } + + // todo: remove on updating platform version up to 3.188 + @Visible() + public hasColumnFilters(): boolean { + const filterModel: { [key: string]: any; } = this.gridApi.getFilterModel(); + return !!filterModel && Object.keys(filterModel).length > 0; + } + + // todo: remove on updating platform version up to 3.188 + public columnFilterChanged(event: FilterChangedEvent) { + this.columnFiltersChanged.trigger(event); + super.columnFilterChanged(event); + } } diff --git a/frontend/src/ts/modules/app/component/logout.component.ts b/frontend/src/ts/modules/app/component/logout.component.ts index 0ac178ae..deee5d42 100644 --- a/frontend/src/ts/modules/app/component/logout.component.ts +++ b/frontend/src/ts/modules/app/component/logout.component.ts @@ -1,23 +1,46 @@ -import {ChangeDetectorRef, Component, OnInit} from "@angular/core"; -import {Router} from "@angular/router"; -import {HttpClient} from "@angular/common/http"; +import {ChangeDetectorRef, Component, DoCheck, OnInit} from "@angular/core"; +import {HttpClient, HttpXsrfTokenExtractor} from "@angular/common/http"; import {AuthenticationService} from "../../security/authentication.service"; +import {AppConfigService} from "@webbpm/base-package"; @Component({ moduleId: module.id, selector: "[log-out]", templateUrl: "../../../../../src/resources/template/app/component/log_out.html" }) -export class LogOutComponent implements OnInit{ +export class LogOutComponent implements OnInit, DoCheck{ + private static readonly BACKEND_URL: string = "backend.url"; + private static readonly BACKEND_CONTEXT: string = "backend.context"; + private static readonly LOGOUT_URL_POSTFIX: string = "/esia/logout"; private userFullname: string; private orgUnitName: string; + csrfValue: any; + formAction: any; - constructor(private router: Router, private httpClient: HttpClient, - private authenticationService: AuthenticationService, private cd: ChangeDetectorRef) { + constructor(private httpClient: HttpClient, + private authenticationService: AuthenticationService, + private appConfigService: AppConfigService, + private tokenExtractor: HttpXsrfTokenExtractor, + private cd: ChangeDetectorRef) { + let backendUrl = this.appConfigService.getParamValue(LogOutComponent.BACKEND_URL); + let backendContext = this.appConfigService.getParamValue( + LogOutComponent.BACKEND_CONTEXT); + + if (backendUrl) { + this.formAction = `${backendUrl}${LogOutComponent.LOGOUT_URL_POSTFIX}`; + } + else if (backendContext) { + this.formAction = `/${backendContext}${LogOutComponent.LOGOUT_URL_POSTFIX}`; + } + } + + ngDoCheck(): void { + this.csrfValue = this.tokenExtractor.getToken(); } ngOnInit(): void { + this.csrfValue = this.tokenExtractor.getToken(); let isAuth = this.authenticationService.isAuthenticated(); if (isAuth) { Promise.all([ @@ -31,10 +54,6 @@ export class LogOutComponent implements OnInit{ } } - public logout(): void { - this.authenticationService.logout(); - } - public getUserFullname(): string { return this.userFullname; } diff --git a/resources/src/main/resources/business-model/Журнал взаимодействия.page b/resources/src/main/resources/business-model/Журнал взаимодействия.page index afe5cb02..33005c43 100644 --- a/resources/src/main/resources/business-model/Журнал взаимодействия.page +++ b/resources/src/main/resources/business-model/Журнал взаимодействия.page @@ -307,13 +307,224 @@ + + visible + +false + + + + + + + 98594cec-0a9b-4cef-af09-e1b71cb2ad9e + 2332e342-cb03-48cf-9745-e1bfbad545d2 + AC обновление текста + false + false + + + + eventRefs + + + + + + behavior + + {"objectId":"9b7c3369-e1fe-44f6-88f9-f8d9c83b30dc","packageName":"component.field","className":"NumberField","type":"TS"} + + + + propertyName + + "valueChangeEvent" + + + + + + + + + ifCondition + + + + logicalOperation + + null + + + + + + + thenActions + + + + + + behavior + + {"objectId":"7f891535-8dde-4e00-8064-584cad7ffcfd","packageName":"component","className":"Text","type":"TS"} + + + + method + + "setValue" + + + + value + + + + objectValue + + + + argument + + null + + + + behavior + + {"objectId":"9b7c3369-e1fe-44f6-88f9-f8d9c83b30dc","packageName":"component.field","className":"NumberField","type":"TS"} + + + + method + + "getTextValue" + + + + + + + + + + + + + + + + + + 98594cec-0a9b-4cef-af09-e1b71cb2ad9e + 7f600c0d-ad59-46ad-9aa8-037fdfaaac14 + AC установка количества + false + false + + + + eventRefs + + + + + + behavior + + {"objectId":"bbaf33d7-0679-440b-a394-cb805ce80300","packageName":"ervu.component.grid","className":"InMemoryStaticGrid","type":"TS"} + + + + propertyName + + "gridLoaded" + + + + + + + + + ifCondition + + + + logicalOperation + + null + + + + + + + thenActions + + + + + + behavior + + {"objectId":"9b7c3369-e1fe-44f6-88f9-f8d9c83b30dc","packageName":"component.field","className":"NumberField","type":"TS"} + + + + method + + "setValue" + + + + value + + + + objectValue + + + + argument + + null + + + + behavior + + {"objectId":"bbaf33d7-0679-440b-a394-cb805ce80300","packageName":"ervu.component.grid","className":"InMemoryStaticGrid","type":"TS"} + + + + method + + "getRowDataSize" + + + + + + + + + + + + + + 98594cec-0a9b-4cef-af09-e1b71cb2ad9e 6022b88c-7c96-4961-9a03-042d71e4ac16 - Обработка событий + AC нажатие кнопки false false @@ -431,6 +642,42 @@ + + + + + behavior + + {"objectId":"26242dda-11df-42f0-be93-9fe67ccc016e","packageName":"component.button","className":"Button","type":"TS"} + + + + method + + "setVisible" + + + + value + + + + staticValue + + + boolean + + +false + + + + + + + + + @@ -438,18 +685,59 @@ - + 98594cec-0a9b-4cef-af09-e1b71cb2ad9e - 7f600c0d-ad59-46ad-9aa8-037fdfaaac14 - Action Controller + dec5e904-4f52-444e-8478-d873461616d6 + AC доступность кнопки false false + + elseActions + + + + + + behavior + + {"objectId":"26242dda-11df-42f0-be93-9fe67ccc016e","packageName":"component.button","className":"Button","type":"TS"} + + + + method + + "setVisible" + + + + value + + + + staticValue + + + boolean + + +false + + + + + + + + + + eventRefs - + + @@ -461,18 +749,92 @@ propertyName - "gridLoaded" + "columnFiltersChanged" + ifCondition + + conditions + + + + + + _isGroupSelected + +false + + + + one + + + + conditionFirstPart + + + + objectValue + + + +behavior + + {"objectId":"bbaf33d7-0679-440b-a394-cb805ce80300","packageName":"ervu.component.grid","className":"InMemoryStaticGrid","type":"TS"} + + + +method + + "hasColumnFilters" + + + + + + + + + + conditionSecondPart + + + + staticValue + + + boolean + + + true + + + + + + + operation + + "EQUALS" + + + + + + + + + + logicalOperation @@ -485,19 +847,19 @@ thenActions - + behavior - {"objectId":"9b7c3369-e1fe-44f6-88f9-f8d9c83b30dc","packageName":"component.field","className":"NumberField","type":"TS"} + {"objectId":"26242dda-11df-42f0-be93-9fe67ccc016e","packageName":"component.button","className":"Button","type":"TS"} method - "setValue" + "setVisible" @@ -505,78 +867,13 @@ - objectValue + staticValue - - - argument - - null - - - - behavior - - {"objectId":"bbaf33d7-0679-440b-a394-cb805ce80300","packageName":"ervu.component.grid","className":"InMemoryStaticGrid","type":"TS"} - - - - method - - "getRowDataSize" - - - - - - - - - - - - - - - - behavior - - {"objectId":"7f891535-8dde-4e00-8064-584cad7ffcfd","packageName":"component","className":"Text","type":"TS"} - - - - method - - "setValue" - - - - value - - - - objectValue - - - - argument - - null - - - - behavior - - {"objectId":"9b7c3369-e1fe-44f6-88f9-f8d9c83b30dc","packageName":"component.field","className":"NumberField","type":"TS"} - - - - method - - "getTextValue" - - - + + boolean + + +true diff --git a/resources/src/main/resources/business-model/Личный кабинет юр лица.page b/resources/src/main/resources/business-model/Личный кабинет юр лица.page index 51401984..05549e31 100644 --- a/resources/src/main/resources/business-model/Личный кабинет юр лица.page +++ b/resources/src/main/resources/business-model/Личный кабинет юр лица.page @@ -10618,7 +10618,7 @@ initialValue - "Информацию о статусе приема сведений можно отслеживать в \u003ca href\u003d\"/#/filesentlog\"\u003eЖурнале взаимодействий с Реестром ВУ\u003c/a\u003e" + "Информацию о статусе приема сведений можно отслеживать в \u003ca href\u003d\"./#/filesentlog\"\u003eЖурнале взаимодействий с Реестром ВУ\u003c/a\u003e"