Merge remote-tracking branch 'origin/hotfix/1.8.2'

This commit is contained in:
Zaripov Emil 2024-10-25 14:21:52 +03:00
commit fa2aa366a1
32 changed files with 707 additions and 269 deletions

View file

@ -0,0 +1,34 @@
package ru.micord.ervu.security;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import ru.micord.ervu.security.esia.service.EsiaAuthService;
public class LogoutSuccessHandler
implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {
private final CsrfTokenRepository csrfTokenRepository;
private final EsiaAuthService esiaAuthService;
public LogoutSuccessHandler(CsrfTokenRepository csrfTokenRepository,
EsiaAuthService esiaAuthService) {
this.csrfTokenRepository = csrfTokenRepository;
this.esiaAuthService = esiaAuthService;
}
@Override
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();
CsrfToken csrfToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(csrfToken, request, response);
}
}

View file

@ -4,59 +4,105 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import ru.micord.ervu.security.webbpm.jwt.filter.JwtAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.web.filter.RequestContextFilter;
import ru.micord.ervu.security.esia.service.EsiaAuthService;
import ru.micord.ervu.security.filter.FilterChainExceptionHandler;
import ru.micord.ervu.security.webbpm.jwt.JwtAuthenticationProvider;
import ru.micord.ervu.security.webbpm.jwt.JwtMatcher;
import ru.micord.ervu.security.webbpm.jwt.UnauthorizedEntryPoint;
import ru.micord.ervu.security.webbpm.jwt.filter.JwtAuthenticationFilter;
import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
import static ru.micord.ervu.security.SecurityConstants.ESIA_LOGOUT;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public class SecurityConfig {
private static final String[] PERMIT_ALL = new String[] {
"/version", "/esia/url", "/esia/auth", "esia/refresh"
};
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private EsiaAuthService esiaAuthService;
@Autowired
private FilterChainExceptionHandler filterChainExceptionHandler;
@Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(jwtAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
httpConfigure(http);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new RequestContextFilter(), LogoutFilter.class);
http.addFilterAfter(filterChainExceptionHandler, RequestContextFilter.class);
return http.build();
}
httpConfigure(http);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
protected void httpConfigure(HttpSecurity httpSecurity) throws Exception {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
tokenRepository.setCookiePath("/");
XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
delegate.setCsrfRequestAttributeName(null);
// Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
// default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
CsrfTokenRequestHandler requestHandler = delegate::handle;
httpSecurity.authorizeHttpRequests(
(authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers(PERMIT_ALL)
.permitAll()
.anyRequest()
.authenticated())
.csrf((csrf) -> csrf.csrfTokenRepository(tokenRepository)
.csrfTokenRequestHandler(requestHandler))
.logout((logout) -> logout.logoutUrl(ESIA_LOGOUT)
.logoutSuccessHandler(new LogoutSuccessHandler(tokenRepository, esiaAuthService)))
.exceptionHandling()
.authenticationEntryPoint(entryPoint())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
protected void httpConfigure(HttpSecurity httpSecurity) throws Exception {
String[] permitAll = {"/version", "/esia/url", "/esia/auth", "esia/refresh"};
public AuthenticationEntryPoint entryPoint() {
return new UnauthorizedEntryPoint();
}
httpSecurity.authorizeRequests()
.antMatchers(permitAll).permitAll()
.antMatchers("/**").authenticated()
.and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(entryPoint())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
public AuthenticationEntryPoint entryPoint() {
return new UnauthorizedEntryPoint();
}
@Bean
public SecurityHelper securityHelper() {
return new SecurityHelper();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter("/**",
entryPoint()
);
jwtAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
return jwtAuthenticationFilter;
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(SecurityHelper securityHelper,
AuthenticationManager manager,
JwtTokenService jwtTokenService) {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(
new JwtMatcher("/**", PERMIT_ALL), entryPoint(), securityHelper, jwtTokenService);
jwtAuthenticationFilter.setAuthenticationManager(manager);
return jwtAuthenticationFilter;
}
}

View file

@ -0,0 +1,5 @@
package ru.micord.ervu.security;
public class SecurityConstants {
public static final String ESIA_LOGOUT = "/esia/logout";
}

View file

@ -1,15 +1,22 @@
package ru.micord.ervu.security.esia.controller;
import javax.servlet.http.Cookie;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ru.micord.ervu.security.esia.model.PersonDataModel;
import ru.micord.ervu.security.esia.model.PersonModel;
import ru.micord.ervu.security.esia.service.EsiaAuthService;
import ru.micord.ervu.security.esia.service.PersonalDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
/**
* @author Eduard Tihomirov
@ -23,13 +30,16 @@ public class EsiaController {
@Autowired
private PersonalDataService personalDataService;
@Autowired
private JwtTokenService jwtTokenService;
@RequestMapping(value = "/esia/url")
public String getEsiaUrl() {
return esiaAuthService.generateAuthCodeUrl();
}
@RequestMapping(value = "/esia/auth", params = "code", method = RequestMethod.GET)
public boolean esiaAuth(@RequestParam("code") String code, HttpServletRequest request, HttpServletResponse response) {
@GetMapping(value = "/esia/auth", params = "code")
public ResponseEntity<?> esiaAuth(@RequestParam("code") String code, HttpServletRequest request, HttpServletResponse response) {
return esiaAuthService.getEsiaTokensByCode(code, request, response);
}
@ -40,18 +50,8 @@ public class EsiaController {
@RequestMapping(value = "/esia/person")
public PersonDataModel getPersonModel(HttpServletRequest request) {
String accessToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("access_token")) {
accessToken = cookie.getValue();
}
}
}
if (accessToken == null) {
return null;
}
String accessToken = jwtTokenService.getAccessToken(request);
DateFormat df = new SimpleDateFormat("dd.MM.yyyy");
PersonModel personModel = personalDataService.getPersonModel(accessToken);
PersonDataModel personDataModel = new PersonDataModel();
personDataModel.birthDate = personModel.getBirthDate();
@ -69,24 +69,8 @@ public class EsiaController {
@RequestMapping(value = "/esia/userfullname")
public String getUserFullname(HttpServletRequest request) {
String accessToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("access_token")) {
accessToken = cookie.getValue();
}
}
}
if (accessToken == null) {
return null;
}
String accessToken = jwtTokenService.getAccessToken(request);
PersonModel personModel = personalDataService.getPersonModel(accessToken);
return personModel.getLastName() + " " + personModel.getFirstName().charAt(0) + ". " + personModel.getMiddleName().charAt(0) + ".";
}
@RequestMapping(value = "/esia/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
return esiaAuthService.logout(request, response);
}
}

View file

@ -22,10 +22,14 @@ import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContext;
import ru.micord.ervu.kafka.model.Document;
import ru.micord.ervu.kafka.model.Person;
import ru.micord.ervu.kafka.model.Response;
import ru.micord.ervu.kafka.service.ReplyingKafkaService;
import ru.micord.ervu.security.esia.token.TokensStore;
import ru.micord.ervu.security.esia.config.EsiaConfig;
import ru.micord.ervu.security.esia.model.FormUrlencoded;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
@ -36,6 +40,8 @@ import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication;
import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
import ru.micord.ervu.security.webbpm.jwt.model.Token;
@ -44,10 +50,6 @@ import ru.micord.ervu.security.webbpm.jwt.model.Token;
*/
@Service
public class EsiaAuthService {
@Value("${cookie.path:#{null}}")
private String path;
@Autowired
private ObjectMapper objectMapper;
@ -64,6 +66,9 @@ public class EsiaAuthService {
@Autowired
private PersonalDataService personalDataService;
@Autowired
private SecurityHelper securityHelper;
@Value("${ervu.kafka.reply.topic}")
private String requestReplyTopic;
@ -141,7 +146,7 @@ public class EsiaAuthService {
return uriBuilder.toString();
}
public boolean getEsiaTokensByCode(String esiaAuthCode, HttpServletRequest request, HttpServletResponse response) {
public ResponseEntity<?> getEsiaTokensByCode(String esiaAuthCode, HttpServletRequest request, HttpServletResponse response) {
try {
String clientId = esiaConfig.getClientId();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
@ -183,50 +188,45 @@ public class EsiaAuthService {
.build()
.send(postReq, HttpResponse.BodyHandlers.ofString());
String responseString = postResp.body();
EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString, EsiaTokenResponse.class);
if (tokenResponse != null && tokenResponse.getError() != null) {
EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString,
EsiaTokenResponse.class
);
if (tokenResponse == null) {
throw new IllegalStateException("Got empty esia response");
}
if (tokenResponse.getError() != null) {
throw new RuntimeException(tokenResponse.getError_description());
}
String cookiePath = null;
if (path != null) {
cookiePath = path;
}
else {
cookiePath = request.getContextPath();
}
String accessToken = tokenResponse.getAccess_token();
Cookie cookie = new Cookie("access_token", accessToken);
cookie.setHttpOnly(true);
cookie.setPath(cookiePath);
response.addCookie(cookie);
String refreshToken = tokenResponse.getRefresh_token();
Cookie cookieRefresh = new Cookie("refresh_token", refreshToken);
cookieRefresh.setHttpOnly(true);
cookieRefresh.setPath(cookiePath);
response.addCookie(cookieRefresh);
byte[] decodedBytes = Base64.getDecoder()
.decode(
accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.')));
String decodedString = new String(decodedBytes);
EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString, EsiaAccessToken.class);
String ervuId = getErvuId(accessToken);
Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), tokenResponse.getExpires_in(), ervuId);
Cookie authToken = new Cookie("auth_token", token.getValue());
authToken.setPath(cookiePath);
authToken.setHttpOnly(true);
response.addCookie(authToken);
SecurityContextHolder.getContext()
.setAuthentication(
new UsernamePasswordAuthenticationToken(esiaAccessToken.getSbj_id(), null));
Cookie isAuth = new Cookie("webbpm.ervu-lkrp-fl", "true");
isAuth.setMaxAge(tokenResponse.getExpires_in().intValue());
isAuth.setPath("/");
response.addCookie(isAuth);
return true;
EsiaAccessToken esiaAccessToken = personalDataService.readToken(accessToken);
String prnOid = esiaAccessToken.getSbj_id();
Long expiresIn = tokenResponse.getExpires_in();
TokensStore.addAccessToken(prnOid, accessToken, expiresIn);
TokensStore.addRefreshToken(prnOid, refreshToken, expiresIn);
Response ervuIdResponse = getErvuIdResponse(accessToken);
Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId());
int expiry = tokenResponse.getExpires_in().intValue();
Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry);
response.addCookie(accessCookie);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null);
SecurityContext context = SecurityContextHolder.createEmptyContext();
JwtAuthentication authentication = new JwtAuthentication(usernamePasswordAuthenticationToken,
esiaAccessToken.getSbj_id(), token.getValue());
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry);
response.addCookie(authMarkerCookie);
if (ervuIdResponse.getErrorData() != null) {
return new ResponseEntity<>(
"Доступ запрещен. " + ervuIdResponse.getErrorData().getName(),
HttpStatus.FORBIDDEN
);
}
return ResponseEntity.ok("Authentication successful");
}
catch (Exception e) {
throw new RuntimeException(e);
@ -235,15 +235,7 @@ public class EsiaAuthService {
public void getEsiaTokensByRefreshToken(HttpServletRequest request, HttpServletResponse response) {
try {
String refreshToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh_token")) {
refreshToken = cookie.getValue();
}
}
}
String refreshToken = jwtTokenService.getRefreshToken(request);
String clientId = esiaConfig.getClientId();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
ZonedDateTime dt = ZonedDateTime.now();
@ -288,43 +280,26 @@ public class EsiaAuthService {
throw new RuntimeException(tokenResponse.getError_description());
}
String accessToken = tokenResponse.getAccess_token();
Cookie cookie = new Cookie("access_token", accessToken);
cookie.setHttpOnly(true);
String cookiePath = null;
if (path != null) {
cookiePath = path;
}
else {
cookiePath = request.getContextPath();
}
cookie.setPath(cookiePath);
response.addCookie(cookie);
String newRefreshToken = tokenResponse.getRefresh_token();
Cookie cookieRefresh = new Cookie("refresh_token", newRefreshToken);
cookieRefresh.setHttpOnly(true);
cookieRefresh.setPath(cookiePath);
response.addCookie(cookieRefresh);
byte[] decodedBytes = Base64.getDecoder()
.decode(
accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.')));
String decodedString = new String(decodedBytes);
EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString, EsiaAccessToken.class);
String ervuId = getErvuId(accessToken);
Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), tokenResponse.getExpires_in(), ervuId);
Cookie authToken = new Cookie("auth_token", token.getValue());
authToken.setPath(cookiePath);
authToken.setHttpOnly(true);
response.addCookie(authToken);
SecurityContextHolder.getContext()
.setAuthentication(
new UsernamePasswordAuthenticationToken(esiaAccessToken.getSbj_id(), null));
Cookie isAuth = new Cookie("webbpm.ervu-lkrp-fl", "true");
isAuth.setMaxAge(tokenResponse.getExpires_in().intValue());
isAuth.setPath("/");
response.addCookie(isAuth);
EsiaAccessToken esiaAccessToken = personalDataService.readToken(accessToken);
String prnOid = esiaAccessToken.getSbj_id();
Long expiresIn = tokenResponse.getExpires_in();
TokensStore.addAccessToken(prnOid, accessToken, expiresIn);
TokensStore.addRefreshToken(prnOid, newRefreshToken, expiresIn);
Response ervuIdResponse = getErvuIdResponse(accessToken);
Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId());
int expiry = tokenResponse.getExpires_in().intValue();
Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry);
response.addCookie(accessCookie);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null);
SecurityContext context = SecurityContextHolder.createEmptyContext();
JwtAuthentication authentication = new JwtAuthentication(usernamePasswordAuthenticationToken,
esiaAccessToken.getSbj_id(), token.getValue());
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry);
response.addCookie(authMarkerCookie);
}
catch (Exception e) {
throw new RuntimeException(e);
@ -365,23 +340,10 @@ public class EsiaAuthService {
public String logout(HttpServletRequest request, HttpServletResponse response) {
try {
Cookie[] cookies = request.getCookies();
if (cookies != null)
for (Cookie cookie : cookies) {
if (cookie.getName().equals("webbpm.ervu-lkrp-fl")) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
else if (cookie.getName().equals("auth_token") || cookie.getName().equals("refresh_token")
|| cookie.getName().equals("access_token")) {
cookie.setValue("");
cookie.setPath(cookie.getPath());
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
securityHelper.clearAccessCookies(response);
String userId = jwtTokenService.getUserAccountId(request);
TokensStore.removeAccessToken(userId);
TokensStore.removeRefreshToken(userId);
String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl();
String redirectUrl = esiaConfig.getRedirectUrl();
URL url = new URL(logoutUrl);
@ -395,22 +357,14 @@ public class EsiaAuthService {
}
}
public String getErvuId(String accessToken) {
public Response getErvuIdResponse(String accessToken) {
try {
PersonModel personModel = personalDataService.getPersonModel(accessToken);
Person person = copyToPerson(personModel);
String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic,
requestReplyTopic, objectMapper.writeValueAsString(person)
);
Response response = objectMapper.readValue(kafkaResponse, Response.class);
if (response.getErrorData() != null) {
throw new RuntimeException(
"Error code = " + response.getErrorData().getCode() + ", error name = "
+ response.getErrorData().getName());
}
else {
return response.getErvuId();
}
return objectMapper.readValue(kafkaResponse, Response.class);
}
catch (Exception e) {
throw new RuntimeException(e);

View file

@ -31,11 +31,7 @@ public class EsiaPersonalDataService implements PersonalDataService {
@Override
public PersonModel getPersonModel(String accessToken) {
try {
byte[] decodedBytes = Base64.getDecoder()
.decode(
accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.')));
String decodedString = new String(decodedBytes);
EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString, EsiaAccessToken.class);
EsiaAccessToken esiaAccessToken = readToken(accessToken);
String prnsId = esiaAccessToken.getSbj_id();
PersonModel personModel = getPersonData(prnsId, accessToken);
personModel.setPassportModel(
@ -88,4 +84,23 @@ public class EsiaPersonalDataService implements PersonalDataService {
throw new RuntimeException(e);
}
}
@Override
public EsiaAccessToken readToken(String accessToken) {
try {
byte[] decodedBytes = Base64.getDecoder()
.decode(
accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.'))
.replace('-', '+')
.replace('_', '/'));
String decodedString = new String(decodedBytes);
EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString,
EsiaAccessToken.class
);
return esiaAccessToken;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,5 +1,6 @@
package ru.micord.ervu.security.esia.service;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.PersonModel;
/**
@ -8,4 +9,5 @@ import ru.micord.ervu.security.esia.model.PersonModel;
public interface PersonalDataService {
PersonModel getPersonModel(String accessToken);
EsiaAccessToken readToken(String accessToken);
}

View file

@ -0,0 +1,34 @@
package ru.micord.ervu.security.esia.token;
/**
* @author Eduard Tihomirov
*/
public class ExpiringToken {
private String accessToken;
private long expiryTime;
public ExpiringToken(String accessToken, long expiryTime) {
this.accessToken = accessToken;
this.expiryTime = expiryTime;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public long getExpiryTime() {
return expiryTime;
}
public void setExpiryTime(long expiryTime) {
this.expiryTime = expiryTime;
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
}

View file

@ -0,0 +1,20 @@
package ru.micord.ervu.security.esia.token;
import net.javacrumbs.shedlock.core.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Eduard Tihomirov
*/
@Service
public class TokensClearShedulerService {
@Scheduled(cron = "${esia.token.clear.cron:0 0 */1 * * *}")
@SchedulerLock(name = "clearToken")
@Transactional
public void load() {
TokensStore.removeExpiredRefreshToken();
TokensStore.removeExpiredAccessToken();
}
}

View file

@ -0,0 +1,60 @@
package ru.micord.ervu.security.esia.token;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Eduard Tihomirov
*/
public class TokensStore {
private static final Map<String, ExpiringToken> accessTokensMap = new ConcurrentHashMap<>();
private static final Map<String, ExpiringToken> refreshTokensMap = new ConcurrentHashMap<>();
public static void addAccessToken(String prnOid, String token, long expiresIn) {
if (token != null) {
long expiryTime = System.currentTimeMillis() + 1000L * expiresIn;
accessTokensMap.put(prnOid, new ExpiringToken(token, expiryTime));
}
}
public static String getAccessToken(String prnOid) {
return accessTokensMap.get(prnOid).getAccessToken();
}
public static void removeExpiredAccessToken() {
for (String key : accessTokensMap.keySet()) {
ExpiringToken token = accessTokensMap.get(key);
if (token != null && token.isExpired()) {
accessTokensMap.remove(key);
}
}
}
public static void removeExpiredRefreshToken() {
for (String key : refreshTokensMap.keySet()) {
ExpiringToken token = refreshTokensMap.get(key);
if (token != null && token.isExpired()) {
refreshTokensMap.remove(key);
}
}
}
public static void removeAccessToken(String prnOid) {
accessTokensMap.remove(prnOid);
}
public static void addRefreshToken(String prnOid, String token, long expiresIn) {
if (token != null) {
long expiryTime = System.currentTimeMillis() + 1000L * expiresIn;
refreshTokensMap.put(prnOid, new ExpiringToken(token, expiryTime));
}
}
public static String getRefreshToken(String prnOid) {
return refreshTokensMap.get(prnOid).getAccessToken();
}
public static void removeRefreshToken(String prnOid) {
refreshTokensMap.remove(prnOid);
}
}

View file

@ -0,0 +1,29 @@
package ru.micord.ervu.security.filter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) {
try {
filterChain.doFilter(request, response);
}
catch (Exception e) {
resolver.resolveException(request, response, null, e);
}
}
}

View file

@ -1,5 +1,7 @@
package ru.micord.ervu.security.webbpm.jwt;
import java.util.Collections;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
@ -44,10 +46,12 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
throw new BadCredentialsException("Auth token is not valid for user " + token.getUserAccountId());
}
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null);
UsernamePasswordAuthenticationToken pwdToken =
UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null,
Collections.emptyList()
);
return new JwtAuthentication(usernamePasswordAuthenticationToken, token.getUserAccountId());
return new JwtAuthentication(pwdToken, token.getUserAccountId(), token.getValue());
}
@Override

View file

@ -0,0 +1,33 @@
package ru.micord.ervu.security.webbpm.jwt;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.extractAuthToken;
public final class JwtMatcher implements RequestMatcher {
private final Set<AntPathRequestMatcher> excludedPathMatchers;
private final AntPathRequestMatcher securedMatcher;
public JwtMatcher(String securedPath, String... excludedPaths) {
this.securedMatcher = new AntPathRequestMatcher(securedPath);
this.excludedPathMatchers = Arrays.stream(excludedPaths)
.map(AntPathRequestMatcher::new)
.collect(Collectors.toSet());
}
@Override
public boolean matches(HttpServletRequest request) {
if (this.excludedPathMatchers.stream().anyMatch(matcher -> matcher.matches(request))) {
return false;
}
else {
return extractAuthToken(request) != null && this.securedMatcher.matches(request);
}
}
}

View file

@ -4,7 +4,6 @@ import java.io.IOException;
import java.lang.invoke.MethodHandles;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -16,69 +15,81 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
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;
/**
* @author Flyur Karimov
*/
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final Logger LOGGER = LoggerFactory.getLogger(
MethodHandles.lookup().lookupClass());
private final AuthenticationEntryPoint entryPoint;
public JwtAuthenticationFilter(String securityPath, AuthenticationEntryPoint entryPoint) {
super(securityPath);
private final SecurityHelper securityHelper;
private final JwtTokenService jwtTokenService;
public JwtAuthenticationFilter(RequestMatcher requestMatcher,
AuthenticationEntryPoint entryPoint,
SecurityHelper securityHelper,
JwtTokenService jwtTokenService) {
super(requestMatcher);
this.entryPoint = entryPoint;
this.securityHelper = securityHelper;
this.jwtTokenService = jwtTokenService;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws AuthenticationException {
String token = extractAuthTokenFromRequest(httpServletRequest);
HttpServletResponse httpServletResponse)
throws AuthenticationException {
String tokenStr = extractAuthToken(httpServletRequest);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
authentication = new JwtAuthentication(null, null, token);
authentication = new JwtAuthentication(null, null, tokenStr);
}
try {
authentication = getAuthenticationManager().authenticate(authentication);
if (!httpServletRequest.getRequestURI().endsWith("esia/logout")) {
Token token = jwtTokenService.getToken(tokenStr);
String[] ids = token.getUserAccountId().split(":");
if (ids.length != 2) {
throw new CredentialsExpiredException("Invalid token. User has no ervuId");
}
}
}
catch (CredentialsExpiredException e) {
securityHelper.clearAccessCookies(httpServletResponse);
httpServletResponse.setStatus(401);
LOGGER.warn(e.getMessage());
return null;
}
return authentication;
}
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return extractAuthTokenFromRequest(request) != null;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authentication) throws IOException, ServletException {
FilterChain chain, Authentication authentication)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
LOGGER.error("Jwt unsuccessful authentication exception", exception);
SecurityContextHolder.clearContext();
entryPoint.commence(request, response, exception);
}
public String extractAuthTokenFromRequest(HttpServletRequest httpRequest) {
String token = null;
Cookie[] cookies = httpRequest.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("auth_token")) {
token = cookie.getValue();
}
}
}
return token;
}
}

View file

@ -0,0 +1,43 @@
package ru.micord.ervu.security.webbpm.jwt.helper;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil;
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;
public void clearAccessCookies(HttpServletResponse response) {
Cookie tokenCookie = createCookie(AUTH_TOKEN, null, null);
tokenCookie.setMaxAge(0);
tokenCookie.setPath(accessCookiePath);
tokenCookie.setHttpOnly(true);
response.addCookie(tokenCookie);
Cookie markerCookie = createCookie(AUTH_MARKER, null, null);
markerCookie.setMaxAge(0);
markerCookie.setPath("/");
response.addCookie(markerCookie);
}
public Cookie createAccessCookie(String cookieValue, int expiry) {
Cookie authToken = createCookie(SecurityUtil.AUTH_TOKEN, cookieValue, accessCookiePath);
authToken.setPath(accessCookiePath);
authToken.setMaxAge(expiry);
return authToken;
}
public Cookie createAuthMarkerCookie(String cookieValue, int expiry) {
Cookie marker = createCookie(AUTH_MARKER, cookieValue, "/");
marker.setMaxAge(expiry);
marker.setHttpOnly(false);
return marker;
}
}

View file

@ -1,12 +1,9 @@
package ru.micord.ervu.security.webbpm.jwt.service;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.Optional;
import javax.crypto.SecretKey;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import io.jsonwebtoken.Claims;
@ -17,10 +14,13 @@ 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.TokensStore;
import ru.micord.ervu.security.webbpm.jwt.model.Token;
import ru.cg.webbpm.modules.resources.api.ResourceMetadataUtils;
import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.extractAuthToken;
/**
* @author Flyur Karimov
*/
@ -29,7 +29,7 @@ public class JwtTokenService {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Value("${webbpm.security.token.issuer:}")
@Value("${webbpm.security.token.issuer:#{null}}")
private final String tokenIssuerName =
ResourceMetadataUtils.PROJECT_GROUP_ID + "." + ResourceMetadataUtils.PROJECT_ARTIFACT_ID;
private final SecretKey SIGNING_KEY;
@ -80,13 +80,27 @@ public class JwtTokenService {
}
public String getErvuId() {
String authToken = Optional.ofNullable(request.getCookies())
.map(cookies -> Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals("auth_token"))
.findFirst()
.map(Cookie::getValue)
.orElseThrow(() -> new RuntimeException("Failed to get auth data. User unauthorized.")))
.orElseThrow(() -> new RuntimeException("Failed to get auth data. User unauthorized."));
return getToken(authToken).getUserAccountId().split(":")[1];
String extractAuthToken = extractAuthToken(request);
return getToken(extractAuthToken).getUserAccountId().split(":")[1];
}
public String getAccessToken(HttpServletRequest request) {
return TokensStore.getAccessToken(getUserAccountId(request));
}
public String getRefreshToken(HttpServletRequest request) {
return TokensStore.getRefreshToken(getUserAccountId(request));
}
public String getUserAccountId(HttpServletRequest request) {
String authToken = extractAuthToken(request);
if (authToken != null) {
String[] ids = getToken(authToken).getUserAccountId().split(":");
return ids[0];
}
else {
throw new RuntimeException("Failed to get auth data. User unauthorized.");
}
}
}

View file

@ -0,0 +1,45 @@
package ru.micord.ervu.security.webbpm.jwt.util;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.util.WebUtils;
import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST;
public final class SecurityUtil {
public static final String AUTH_TOKEN = "auth_token";
public static final String AUTH_MARKER = "webbpm.ervu-lkrp-fl";
private 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;
}
}