Рефакторинг кода

This commit is contained in:
Anton Dzyk 2025-12-19 09:44:34 +03:00
parent 5c89e14e7d
commit 20c67a04fc
11 changed files with 109 additions and 74 deletions

View File

@ -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();
}
}

View File

@ -0,0 +1,9 @@
package ru.oa2.lti.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "lti")
public record AppProperties(
String lmsUri
) {
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -48,10 +48,9 @@ public class ResultController {
if (session == null) return ResponseEntity.status(401).build(); if (session == null) return ResponseEntity.status(401).build();
var ltiLogin = (LtiLogin) session.getAttribute("lti_login"); 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"); String idToken = (String) session.getAttribute("id_token");
resultService.setResult( resultService.setResult(

View File

@ -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<String> listScopes = new ArrayList<>(Arrays.asList(scopes));
return String.join(" ", listScopes);
}
}

View File

@ -7,6 +7,7 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import ru.oa2.lti.config.AppProperties;
import java.util.Map; import java.util.Map;
@ -16,21 +17,24 @@ public class TokenService {
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
private final RestClient restClient; private final RestClient restClient;
private final AppProperties appProperties;
public TokenService(RestClient restClient) { public TokenService(RestClient restClient, AppProperties appProperties) {
this.restClient = restClient; this.restClient = restClient;
this.appProperties = appProperties;
} }
// Вызывается после LTI-запуска // Вызывается после LTI-запуска
public String exchangeForAccessToken( public String exchangeForAccessToken(
String clientId, String clientId) {
String tokenEndpoint) {
var endpointUrl = appProperties.lmsUri() + "/lti/token";
try { try {
// Генерируем client_assertion // Генерируем client_assertion
String clientAssertion = JwtAssertionGenerator.generateClientAssertion( String clientAssertion = JwtAssertionGenerator.generateClientAssertion(
clientId, clientId,
tokenEndpoint, endpointUrl,
"my-key-id-1" // должен совпадать с тем, что в JWKS "my-key-id-1" // должен совпадать с тем, что в JWKS
); );
@ -39,7 +43,7 @@ public class TokenService {
body.add("grant_type", "client_credentials"); body.add("grant_type", "client_credentials");
body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
body.add("client_assertion", clientAssertion); 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(); HttpHeaders headers = new HttpHeaders();
@ -48,7 +52,7 @@ public class TokenService {
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
// Отправляем запрос к OpenOLAT // Отправляем запрос к OpenOLAT
ResponseEntity<Map> response = restTemplate.postForEntity(tokenEndpoint, request, Map.class); ResponseEntity<Map> response = restTemplate.postForEntity(endpointUrl, request, Map.class);
if (response.getStatusCode() == HttpStatus.OK) { if (response.getStatusCode() == HttpStatus.OK) {
Map<String, Object> responseBody = response.getBody(); Map<String, Object> responseBody = response.getBody();
@ -70,7 +74,7 @@ public class TokenService {
"&code=code1" + "&code=code1" +
"&iss=" + iss + "&iss=" + iss +
"&login_hint=" + loginHint + "&login_hint=" + loginHint +
"&redirect_uri=http://openolat.local/tool/lti/redirect" + "&redirect_uri=" + appProperties.lmsUri() + "/tool/lti/redirect" +
"&lti_message_hint=" + ltiLoginHint) "&lti_message_hint=" + ltiLoginHint)
.retrieve(); .retrieve();

View File

@ -29,3 +29,6 @@ logging:
org.hibernate.SQL: DEBUG org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE org.hibernate.type.descriptor.sql.BasicBinder: TRACE
lti:
lms_url: http://openolat.local

View File

@ -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");
}
}

View File

@ -3,11 +3,11 @@ package ru.oa2.lti.jwt;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; 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.JwtService;
import ru.oa2.lti.service.jwt.JwtServiceImpl; import ru.oa2.lti.service.jwt.JwtServiceImpl;
@SpringBootTest(classes = {JwtConfig.class, JwtServiceImpl.class}) @SpringBootTest(classes = {AppConfig.class, JwtServiceImpl.class})
public class IdTokenTest { public class IdTokenTest {
@Autowired @Autowired