mvc - Springboot/Angular2: ¿Cómo manejar las URL HTML5?
spring security angular 2 (8)
Creo que esta es una pregunta simple, pero no pude encontrar una respuesta o al menos usar los términos correctos en la búsqueda.
Estoy configurando
Angular2
y
Springboot
juntos.
De forma predeterminada,
Angular
utilizará rutas como
localhost:8080/dashboard
y
localhost:8080/dashboard/detail
.
Me gustaría evitar usar path como hashs, si es posible. Como dice la documentation angular:
La función provideRouter del enrutador establece LocationStrategy en PathLocationStrategy, por lo que es la estrategia predeterminada. Podemos cambiar a HashLocationStrategy con una anulación durante el proceso de arranque si lo preferimos.
Y entonces...
Casi todos los proyectos de Angular 2 deberían usar el estilo HTML 5 predeterminado. Produce URL que son más fáciles de entender para los usuarios. Y conserva la opción de hacer la representación del lado del servidor más adelante.
El problema es que cuando intento acceder a
localhost:8080/dashboard
, Spring buscará alguna asignación de controlador a esta ruta, que no tendrá.
Whitelabel Error Page
There was an unexpected error (type=Not Found, status=404).
No message available
Inicialmente pensé hacer que todos mis servicios estuvieran bajo
localhost:8080/api
y toda mi estática bajo
localhost:8080/app
.
Pero, ¿cómo le digo a Spring que ignore las solicitudes a esta ruta de
app
?
¿Existe una mejor solución con Angular2 o Boot?
En mis aplicaciones Spring Boot (versiones 1 y 2), mis recursos estáticos están en un solo lugar:
src/main/resources/static
static
es una carpeta reconocida por Spring Boot para cargar recursos estáticos.
Entonces la idea es personalizar la configuración de Spring MVC.
La forma más sencilla es usar la configuración Spring Java.
Implemento
WebMvcConfigurer
para anular
addResourceHandlers()
.
Agrego un
único
ResourceHandler
al
ResourceHandlerRegistry
actual.
El controlador se asigna en cada solicitud y especifico
classpath:/static/
como valor de ubicación del recurso (por supuesto, puede agregar otros si es necesario).
Agrego una clase anónima
PathResourceResolver
personalizada para anular
getResource(String resourcePath, Resource location)
.
Y la regla para devolver el recurso es la siguiente: si el recurso existe y es legible (por lo que es un archivo), lo devuelvo.
De lo contrario, por defecto devuelvo la página
index.html
.
¿Cuál es el comportamiento esperado para manejar las URL de HTML 5?
Aplicación Spring Boot 1.X:
Extender
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
es el camino.
La clase es un adaptador de la interfaz
WebMvcConfigurer
con métodos vacíos que permiten que las subclases anulen solo los métodos que les interesan.
Aquí está el código completo:
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.resource.PathResourceResolver;
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
Aplicación Spring Boot 2.X:
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
ha
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
desuso.
Implementar directamente
WebMvcConfigurer
es el camino ahora, ya que todavía es una interfaz pero ahora tiene métodos predeterminados (posibilitados por una línea de base de Java 8) y puede implementarse directamente sin la necesidad del adaptador.
Aquí está el código completo:
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
Estos son los tres pasos que debe seguir:
-
Implemente su propio bean TomcatEmbeddedServletContainerFactory y configure RewriteValve
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; ... import org.apache.catalina.valves.rewrite.RewriteValve; ... @Bean TomcatEmbeddedServletContainerFactory servletContainerFactory() { TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory(); factory.setPort(8080); factory.addContextValves(new RewriteValve()); return factory; }
-
Agregue un archivo rewrite.conf al directorio WEB-INF de su aplicación y especifique las reglas de reescritura. Aquí hay un ejemplo de contenido rewrite.conf, que estoy usando en la aplicación angular para aprovechar la PathLocationStrategy del angular (básicamente simplemente redirijo todo a index.html ya que solo usamos spring boot para servir el contenido web estático, de lo contrario necesita filtrar sus controladores en la regla RewriteCond):
RewriteCond %{REQUEST_URI} !^.*/.(bmp|css|gif|htc|html?|ico|jpe?g|js|pdf|png|swf|txt|xml|svg|eot|woff|woff2|ttf|map)$ RewriteRule ^(.*)$ /index.html [L]
-
Deshágase de useHash (o configúrelo como falso) de sus declaraciones de enrutamiento:
RouterModule.forRoot(routes)
o
RouterModule.forRoot(routes, {useHash: false})
Lo hice con un filter antiguo simple:
public class PathLocationStrategyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if(request instanceof HttpServletRequest) {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String uri = servletRequest.getRequestURI();
String contextPath = servletRequest.getContextPath();
if(!uri.startsWith(contextPath + "/api") &&
!uri.startsWith(contextPath + "/assets") &&
!uri.equals(contextPath) &&
// only forward if there''s no file extension (exclude *.js, *.css etc)
uri.matches("^([^.]+)$")) {
RequestDispatcher dispatcher = request.getRequestDispatcher("/");
dispatcher.forward(request, response);
return;
}
}
chain.doFilter(request, response);
}
}
Luego en
web.xml
:
<web-app>
<filter>
<filter-name>PathLocationStrategyFilter</filter-name>
<filter-class>mypackage.PathLocationStrategyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>PathLocationStrategyFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
</web-app>
Para hacerlo más simple, puede implementar ErrorPageRegistrar directamente.
@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
}
}
Esto reenviaría las solicitudes a index.html.
@Controller
@RequestMapping("/")
public class MainPageController {
@ResponseStatus(HttpStatus.OK)
@RequestMapping({ "/" })
public String forward() {
return "forward:/";
}
}
Puede reenviar todo lo que no esté asignado a Angular usando algo como esto:
@Controller
public class ForwardController {
@RequestMapping(value = "/**/{[path:[^//.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
Fuente: https://.com/a/44850886/3854385
Mi servidor Spring Boot para angular también es un servidor de puerta de enlace con las llamadas API a
/api
para no tener una página de inicio de sesión frente a las páginas angulares, puede usar algo como.
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
/**
* This sets up basic authentication for the microservice, it is here to prevent
* massive screwups, many applications will require more secuity, some will require less
*/
@EnableOAuth2Sso
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
@Override
public void configure(HttpSecurity http) throws Exception {
http
.logout().logoutSuccessUrl("/").and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll().and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
Puede reenviar todos los recursos no encontrados a su página principal proporcionando ErrorViewResolver personalizado. Todo lo que necesita hacer es agregar esto a su clase de @Configuration:
@Bean
ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() {
return new ErrorViewResolver() {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
return status == HttpStatus.NOT_FOUND
? new ModelAndView("index.html", Collections.<String, Object>emptyMap(), HttpStatus.OK)
: null;
}
};
}
Tengo una solución para usted, puede agregar un
ViewController
para reenviar solicitudes a Angular desde el arranque de Spring.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ViewController {
@RequestMapping({ "/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id://w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers" })
public String index() {
return "forward:/index.html";
}
}
aquí he redirigido todos mis angular2 ("/ bikes", "/ milages", "/ gallery", "/ tracks", "/ tracks / {id: / w +}", "/ location", "/ about", "/ tests", "/ tests / new", "/ tests / **", "/ preguntas", "/ answers") a mi SPA Puede hacer lo mismo para su proyecto y también puede redirigir su página de error 404 a la página de índice como un paso más. ¡Disfrutar!
reenvíe todo el enrutamiento angular con index.html. Incluyendo base href.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ViewController {
@RequestMapping({ "jsa/customer","jsa/customer/{id}",})
public String index() {
return "forward:/index.html";
}
}
En mi caso, jsa es base href.