CORS 跨來源資源共享
SAMEORIGIN 同源政策,是 Netscape 提出,所有支援 Javascript 的瀏覽器都使用這個策略,同源是 domain name, port, protocol 相同的意思,網頁執行一個 script 時,會檢查是否同源,非同源就會拒絕。
只要 domain name, port, protocol 某一個不同,就代表 cross origin。
CORS 允許伺服器指示瀏覽器允許從除其自身以外的任何來源載入資源。
spring MVC 本身透過在 method 上設定 @CrossOrigin
@Slf4j
@RestController
@Validated
public class ResponseBodyController {
@CrossOrigin
@GetMapping(value = "/user/json/{userId}", produces = MediaType.APPLICATION_JSON_VALUE)
public User getJsonUserInfo(@PathVariable("userId") @Size(min = 5, max = 8) String userId) {
User user = new User("test1", 18);
user.setId(Long.valueOf(userId));
log.info("user info: {}", user);
return user;
}
}
@CrossOrigin
預設支援所有 request 來源、所有 http header, methods
@CrossOrigin
也可以用在 class
@Slf4j
@CrossOrigin(origins = "https://www.test.com", maxAge=3600)
@RestController
public class ResponseBodyController {
}
也可以放在 WebMvcConfigurer
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/user/**")
.allowedMethods("GET", "POST")
.allowedOrigins("https://www.test.com")
.allowCredentials(true)
.maxAge(3600);
}
}
安全性
加上 security library
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
啟動時會出現以下的 log
2024-11-20T16:36:50.740+08:00 WARN 5307 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 06ee988c-9983-4187-b040-5bfadde83893
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2024-11-20T16:36:50.784+08:00 INFO 5307 --- [ main] r$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
瀏覽 http://localhost:8080/demo1/test 時,會出現 ## Please sign in
的登入畫面
這時候填寫
username: user
password: 06ee988c-9983-4187-b040-5bfadde83893
就可以打開 index 頁面
可在 application.yml 修改預設的帳號密碼
spring:
security:
user:
name: root
password: password
自訂安全機制
可註冊 SecurityFilterChain 與 UserDetailsService 類別,實作自訂安全機制
SecurityConfig.java
package tw.com.test.demo1.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers("/test/**").hasRole("TEST").requestMatchers("/**").permitAll();
}).logout((logout) -> logout.logoutUrl("/")).formLogin(withDefaults()).build();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("test").password("{noop}test").roles("ADMIN", "TEST").build());
manager.createUser(User.withUsername("root").password("{noop}root").roles("ADMIN").build());
return manager;
}
}
REST service
實作 REST service 有兩種方式
RestTemplate (servlet)
WebClient (reactive)
RestTemplate (servlet)
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
自動設定類別 RestTemplateAutoConfiguration
註冊在 org.springframework.boot.autoconfigure.AutoConfiguration.imports
ResTemplate 並沒有任何預設的註冊
必須要先註冊一個 RestTemplate instance,可透過 restTemplateBuilder 產生
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Bean
public RestTemplate defaultRestTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.build();
}
}
新增一個 controller 測試
package tw.com.test.demo1.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
import tw.com.test.demo1.bean.User;
@Slf4j
@RestController
@RequiredArgsConstructor
public class CallRestController {
public static final String GET_USERINFO_URL = "http://localhost:8080/demo1/user/json/{uid}";
private final RestTemplate restTemplate;
@GetMapping("/restTemplate/{uid}")
public User restTemplate(@PathVariable("uid") String uid) {
return this.restTemplate.getForObject(GET_USERINFO_URL, User.class, uid);
}
}
用 postman 測試 GET http://localhost:8080/demo1/restTemplate/10000
回傳結果
{
"id": 10000,
"age": 18,
"username": "test1"
}
自訂 RestTemplate,加上 timeout, basic authentication
@Bean
public RestTemplate defaultRestTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.basicAuthentication("test", "test")
.build();
}
也可以直接實作 RestTemplateCustomizer
package tw.com.test.demo1.handler;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Component
public class CustomRestTemplateCustomizer implements RestTemplateCustomizer {
@Override
public void customize(RestTemplate restTemplate) {
HttpRoutePlanner routePlanner = new CustomRoutePlanner(new HttpHost("proxy.test.com"));
RequestConfig requestConfig = RequestConfig.custom().build();
HttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setRoutePlanner(routePlanner).build();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
}
static class CustomRoutePlanner extends DefaultProxyRoutePlanner {
CustomRoutePlanner(HttpHost proxy) {
super(proxy);
}
@Override
protected HttpHost determineProxy(HttpHost target, HttpContext context) throws HttpException {
log.info("hostName is {}", target.getHostName());
if ("localhost".equals(target.getHostName())) {
return null;
}
return super.determineProxy(target, context);
}
}
}
有用到 http client 5
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
也可以實作最底層的 HttpAccessor
WebClient (reactive)
WebClient 是使用 speign WebFlux 的 http client
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
User.java
package tw.com.test.demo3.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class User {
private Long id;
private String username;
private Integer age;
}
WebConfig.java
package tw.com.test.demo3.config;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
@Configuration
public class WebConfig {
@Bean
public WebClient webClient(WebClient.Builder webClientBuilder) {
return webClientBuilder.build();
}
}
CallRestController.java
package tw.com.test.demo3.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import tw.com.test.demo3.bean.User;
@Slf4j
@RestController
@RequiredArgsConstructor
public class CallRestController {
public static final String GET_USERINFO_URL = "http://localhost:8080/user/json/{uid}";
private final WebClient webClient;
@GetMapping("/webClient/{uid}")
public Mono<User> webClient(@PathVariable("uid") String uid) {
return this.webClient.get().uri(GET_USERINFO_URL, uid).retrieve().bodyToMono(User.class);
}
}
這是要呼叫的 rest url
package tw.com.test.demo3.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import tw.com.test.demo3.bean.User;
@Slf4j
@RestController
@Validated
public class ResponseBodyController {
@CrossOrigin
@GetMapping(value = "/user/json/{userId}", produces = MediaType.APPLICATION_JSON_VALUE)
public User getJsonUserInfo(@PathVariable("userId") String userId) {
User user = new User((long)1000, "test", 18);
user.setId(Long.valueOf(userId));
log.info("user info: {}", user);
return user;
}
}
以 GET method 呼叫 http://localhost:8080/webClient/10000
就能取得結果
{
"id": 10000,
"username": "test",
"age": 18
}
自訂 WebClient
修改 WebConfig.java,改用 reactor netty 的 ReactorClientHttpConnector
package tw.com.test.demo3.config;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
@Configuration
public class WebConfig {
// @Bean
// public WebClient webClient(WebClient.Builder webClientBuilder) {
// return webClientBuilder.build();
// }
@Bean
public WebClient webClient(WebClient.Builder webClientBuilder) {
HttpClient httpClient = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3).doOnConnected(conn -> {
conn.addHandlerLast(new ReadTimeoutHandler(3000));
conn.addHandlerLast(new WriteTimeoutHandler(3000));
});
ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
return webClientBuilder.clientConnector(connector).build();
}
}
可改用 ClientHttpConnector 的其他實作的客戶端,目前 spring boot 支援
Reactor netty (default)
Jetty RS Client
Apache HttpClient
JDK HttpClient