From 20c67a04fcb4d5320e6818e73bfaff4e7a97af2e Mon Sep 17 00:00:00 2001 From: Anton Dzyk Date: Fri, 19 Dec 2025 09:44:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/oa2/lti/config/AppConfig.java | 51 +++++++++++++++++++ .../java/ru/oa2/lti/config/AppProperties.java | 9 ++++ .../java/ru/oa2/lti/config/JwtConfig.java | 18 ------- .../java/ru/oa2/lti/config/MapperConfig.java | 19 ------- .../ru/oa2/lti/config/RestClientConfig.java | 25 --------- .../oa2/lti/controller/ResultController.java | 5 +- .../java/ru/oa2/lti/service/jwt/Scope.java | 16 ++++++ .../ru/oa2/lti/service/jwt/TokenService.java | 18 ++++--- src/main/resources/application.yaml | 3 ++ src/test/java/ru/oa2/lti/ScopeTest.java | 15 ++++++ src/test/java/ru/oa2/lti/jwt/IdTokenTest.java | 4 +- 11 files changed, 109 insertions(+), 74 deletions(-) create mode 100644 src/main/java/ru/oa2/lti/config/AppConfig.java create mode 100644 src/main/java/ru/oa2/lti/config/AppProperties.java delete mode 100644 src/main/java/ru/oa2/lti/config/JwtConfig.java delete mode 100644 src/main/java/ru/oa2/lti/config/MapperConfig.java delete mode 100644 src/main/java/ru/oa2/lti/config/RestClientConfig.java create mode 100644 src/main/java/ru/oa2/lti/service/jwt/Scope.java create mode 100644 src/test/java/ru/oa2/lti/ScopeTest.java diff --git a/src/main/java/ru/oa2/lti/config/AppConfig.java b/src/main/java/ru/oa2/lti/config/AppConfig.java new file mode 100644 index 0000000..48a08e3 --- /dev/null +++ b/src/main/java/ru/oa2/lti/config/AppConfig.java @@ -0,0 +1,51 @@ +package ru.oa2.lti.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.web.client.RestClient; + +import java.nio.charset.StandardCharsets; + +@Configuration +@EnableConfigurationProperties(AppProperties.class) +public class AppConfig { + + @Autowired + AppProperties appProperties; + + @Bean + public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder + .withJwkSetUri(appProperties.lmsUri() + "/lti/keys") + .build(); + } + + @Bean + public ObjectMapper getMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + return mapper; + } + + @Bean + public RestClient getRestClient(RestClient.Builder restClientBuilder) { + return restClientBuilder + .baseUrl(appProperties.lmsUri()) + .defaultHeader(HttpHeaders.USER_AGENT, "LtiProvider") + .defaultStatusHandler(HttpStatusCode::isError, (req, res) -> { + throw new RuntimeException("API error: " + res.getStatusCode() + " body: " + + new String(res.getBody().readAllBytes(), StandardCharsets.UTF_8)); //TODO business exception + }) + .build(); + } +} diff --git a/src/main/java/ru/oa2/lti/config/AppProperties.java b/src/main/java/ru/oa2/lti/config/AppProperties.java new file mode 100644 index 0000000..795221f --- /dev/null +++ b/src/main/java/ru/oa2/lti/config/AppProperties.java @@ -0,0 +1,9 @@ +package ru.oa2.lti.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "lti") +public record AppProperties( + String lmsUri +) { +} diff --git a/src/main/java/ru/oa2/lti/config/JwtConfig.java b/src/main/java/ru/oa2/lti/config/JwtConfig.java deleted file mode 100644 index 15ca8d9..0000000 --- a/src/main/java/ru/oa2/lti/config/JwtConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.oa2.lti.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; - - -@Configuration -public class JwtConfig { - - @Bean - public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder - .withJwkSetUri("http://openolat.local/lti/keys") //TODO адрес из application - .build(); - } -} diff --git a/src/main/java/ru/oa2/lti/config/MapperConfig.java b/src/main/java/ru/oa2/lti/config/MapperConfig.java deleted file mode 100644 index 9ce76bb..0000000 --- a/src/main/java/ru/oa2/lti/config/MapperConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.oa2.lti.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class MapperConfig { - - @Bean - public ObjectMapper getMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - - return mapper; - } -} diff --git a/src/main/java/ru/oa2/lti/config/RestClientConfig.java b/src/main/java/ru/oa2/lti/config/RestClientConfig.java deleted file mode 100644 index ae7062c..0000000 --- a/src/main/java/ru/oa2/lti/config/RestClientConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.oa2.lti.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatusCode; -import org.springframework.web.client.RestClient; - -import java.nio.charset.StandardCharsets; - -@Configuration -public class RestClientConfig { - - @Bean - public RestClient getRestClient(RestClient.Builder restClientBuilder) { - return restClientBuilder - .baseUrl("http://openolat.local") - .defaultHeader(HttpHeaders.USER_AGENT, "SpringBootApp") - .defaultStatusHandler(HttpStatusCode::isError, (req, res) -> { - throw new RuntimeException("API error: " + res.getStatusCode() + " body: " + - new String(res.getBody().readAllBytes(), StandardCharsets.UTF_8)); //TODO business exception - }) - .build(); - } -} diff --git a/src/main/java/ru/oa2/lti/controller/ResultController.java b/src/main/java/ru/oa2/lti/controller/ResultController.java index bc689bf..019953f 100644 --- a/src/main/java/ru/oa2/lti/controller/ResultController.java +++ b/src/main/java/ru/oa2/lti/controller/ResultController.java @@ -48,10 +48,9 @@ public class ResultController { if (session == null) return ResponseEntity.status(401).build(); var ltiLogin = (LtiLogin) session.getAttribute("lti_login"); - var assessToken = tokenService.exchangeForAccessToken(ltiLogin.getClientId(), "http://openolat.local/lti/token"); + var assessToken = tokenService.exchangeForAccessToken( + ltiLogin.getClientId()); - String userId = (String) session.getAttribute("lti_user_id"); - UUID ltiContextId = (UUID) session.getAttribute("lti_context_id"); String idToken = (String) session.getAttribute("id_token"); resultService.setResult( diff --git a/src/main/java/ru/oa2/lti/service/jwt/Scope.java b/src/main/java/ru/oa2/lti/service/jwt/Scope.java new file mode 100644 index 0000000..3d9c728 --- /dev/null +++ b/src/main/java/ru/oa2/lti/service/jwt/Scope.java @@ -0,0 +1,16 @@ +package ru.oa2.lti.service.jwt; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Scope { + public static String LINE_ITEM = "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem"; + public static String SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/scope"; + public static String ENDPOINT = "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"; + + public static String scope(String... scopes) { + List listScopes = new ArrayList<>(Arrays.asList(scopes)); + return String.join(" ", listScopes); + } +} diff --git a/src/main/java/ru/oa2/lti/service/jwt/TokenService.java b/src/main/java/ru/oa2/lti/service/jwt/TokenService.java index 21ca6d8..8d6f45a 100644 --- a/src/main/java/ru/oa2/lti/service/jwt/TokenService.java +++ b/src/main/java/ru/oa2/lti/service/jwt/TokenService.java @@ -7,6 +7,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; +import ru.oa2.lti.config.AppProperties; import java.util.Map; @@ -16,21 +17,24 @@ public class TokenService { private final RestTemplate restTemplate = new RestTemplate(); private final RestClient restClient; + private final AppProperties appProperties; - public TokenService(RestClient restClient) { + public TokenService(RestClient restClient, AppProperties appProperties) { this.restClient = restClient; + this.appProperties = appProperties; } // Вызывается после LTI-запуска public String exchangeForAccessToken( - String clientId, - String tokenEndpoint) { + String clientId) { + + var endpointUrl = appProperties.lmsUri() + "/lti/token"; try { // Генерируем client_assertion String clientAssertion = JwtAssertionGenerator.generateClientAssertion( clientId, - tokenEndpoint, + endpointUrl, "my-key-id-1" // должен совпадать с тем, что в JWKS ); @@ -39,7 +43,7 @@ public class TokenService { body.add("grant_type", "client_credentials"); body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); body.add("client_assertion", clientAssertion); - body.add("scope", "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/scope https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"); + body.add("scope", Scope.scope(Scope.LINE_ITEM, Scope.SCOPE, Scope.ENDPOINT)); // Заголовки HttpHeaders headers = new HttpHeaders(); @@ -48,7 +52,7 @@ public class TokenService { HttpEntity> request = new HttpEntity<>(body, headers); // Отправляем запрос к OpenOLAT - ResponseEntity response = restTemplate.postForEntity(tokenEndpoint, request, Map.class); + ResponseEntity response = restTemplate.postForEntity(endpointUrl, request, Map.class); if (response.getStatusCode() == HttpStatus.OK) { Map responseBody = response.getBody(); @@ -70,7 +74,7 @@ public class TokenService { "&code=code1" + "&iss=" + iss + "&login_hint=" + loginHint + - "&redirect_uri=http://openolat.local/tool/lti/redirect" + + "&redirect_uri=" + appProperties.lmsUri() + "/tool/lti/redirect" + "<i_message_hint=" + ltiLoginHint) .retrieve(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9e73a48..99027c2 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -29,3 +29,6 @@ logging: org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE + +lti: + lms_url: http://openolat.local diff --git a/src/test/java/ru/oa2/lti/ScopeTest.java b/src/test/java/ru/oa2/lti/ScopeTest.java new file mode 100644 index 0000000..9754488 --- /dev/null +++ b/src/test/java/ru/oa2/lti/ScopeTest.java @@ -0,0 +1,15 @@ +package ru.oa2.lti; + +import org.junit.jupiter.api.Test; +import ru.oa2.lti.service.jwt.Scope; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class ScopeTest { + + @Test + public void scopeTest() { + assertThat(Scope.scope(Scope.ENDPOINT, Scope.LINE_ITEM)) + .isEqualTo("https://purl.imsglobal.org/spec/lti-ags/claim/endpoint https://purl.imsglobal.org/spec/lti-ags/scope/lineitem"); + } +} diff --git a/src/test/java/ru/oa2/lti/jwt/IdTokenTest.java b/src/test/java/ru/oa2/lti/jwt/IdTokenTest.java index 01994db..cd2c160 100644 --- a/src/test/java/ru/oa2/lti/jwt/IdTokenTest.java +++ b/src/test/java/ru/oa2/lti/jwt/IdTokenTest.java @@ -3,11 +3,11 @@ package ru.oa2.lti.jwt; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import ru.oa2.lti.config.JwtConfig; +import ru.oa2.lti.config.AppConfig; import ru.oa2.lti.service.jwt.JwtService; import ru.oa2.lti.service.jwt.JwtServiceImpl; -@SpringBootTest(classes = {JwtConfig.class, JwtServiceImpl.class}) +@SpringBootTest(classes = {AppConfig.class, JwtServiceImpl.class}) public class IdTokenTest { @Autowired