BFF實作Spring Security
---
spring boot層層filter, 安全控制設定為一個被稱為`springSecurityFilterChain`的Servlet filter,使用@EnableWebSecurity anonotation
- 對應用程式所有 URL 作驗證
- 允許使用者登出
- 阻絕 CSRF 攻擊
- 防止 Session Fixation
- ......
### Spring Secutiry key concept
1. Principle: user object, represents user or system that can execute operations in app
2. Authority: user role, like admin, user
3. Permission: describe the permission of the role
### 驗證Authentication與授權Authorization
1. Authentication: 建立principle的過程, 通常是要求user提供username and password, 系統透過驗證這些資訊來完成驗證
2. Authorization: 判斷該principle在app能執行那些操作, 此些規則在驗證階段就已建立好
### 核心物件 key object
1. Authentication: 包含Principal, Credentials ,Authorities
2. GrantedAuthority: Authorities 是List< GrantedAuthority>,設置賦予的最高權限, 預設的prefix為 "ROLE_"
3. SecurityContext: 驗證過的Authentication物件, 像是UsernamePasswordAuthenticationToken
4. SecurityContextHolder: 儲存驗證過的Authentication,aka SecurityContext管理員
5. AuthenticationManager: 定義filter驗證的api
6. AuthenticationProvider: 執行特定類型的身分驗證, 像是DaoAuthenticationProvider, JwtAuthenticationProvider
7. ProviderManager: AuthenticationManager 常用的實作類別,負責管理AuthenticationProvider
8. UserDetails: 封裝user details like name, password or authorities
9. UserDetailsService: 載入user details, 進行安全驗證時, 收到請求的username, 找到相應的使用者資訊, 封裝成userDetails
### 初始webSecurity設置
```java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthProvider authProvider;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
defaultWebSecurityExpressionHandler.setDefaultRolePrefix("");
return defaultWebSecurityExpressionHandler;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
}
@Override
public void configure(WebSecurity web) {
web.expressionHandler(new DefaultWebSecurityExpressionHandler() {
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication,
FilterInvocation fi) {
WebSecurityExpressionRoot root = (WebSecurityExpressionRoot) super.createSecurityExpressionRoot(
authentication, fi);
root.setDefaultRolePrefix("");
return root;
}
}).ignoring().antMatchers("/v2/api-docs",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html");
}
// 這邊進行設定
@Override
protected void configure(HttpSecurity http) throws Exception {
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceEncoding(true);
http
.authorizeRequests() // 請求需經過驗證
.antMatchers("/webjars/**", "/actuator/**") // 以下開頭
.permitAll() // 任何人皆可visit
.and() // 結尾斷句
.authorizeRequests()
.antMatchers("/login/**")
.permitAll()
.and()
.logout().logoutUrl("/logout").logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.and()
// X-Frame-Options預設為DENY, 表示可以加載frame頁面
// 這種frame標籤的頁面可以讓網頁內嵌其他網頁
// 需注意被惡意內嵌的攻擊 Clickjacking, 讓你誤按釣魚網站
.headers().frameOptions().disable()
.and()
// 新增跨網域的filter, CorsFilter未指定bean或配置corsConfigurationSource 則使用預設的HandlerMappingIntrospector
.cors()
.and()
// 關掉csrf攻擊, 使用別種token mechanism或簡化client和server之間的互動
// csrf通常是在表單資訊, Restful API通常為stateless也不仰賴server sessions或browser cookies, 無CSRF傾向, 所以通常都關掉
.csrf().disable()
.addFilterBefore(filter, CsrfFilter.class);
}
}
```
REF:
- [19. CORS](https://docs.spring.io/spring-security/site/docs/4.2.19.RELEASE/reference/html/cors.html)
- [Spring 里那么多种 CORS 的配置方式,到底有什么区别](https://segmentfault.com/a/1190000019485883)
```java
// 此annotation設定此方法的跨域限制, 像這個是全開
@CrossOrigin(origins = {"*"} ,allowCredentials = "true")
@PostMapping(path = "/secAuth", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse<LoginResponse>> loginFromSecAuth(@Valid @RequestBody LoginRequest oAuthRequest, HttpServletRequest request) { ... }
```
## Spring Security的應用流程
1. 使用者用 username 及 password 登入
2. 系統成功驗證對於該 username 的 password 正確
3. 系統取得有關該使用者的 context 資訊,例如角色列表等
4. 系統為該使用者建立 security context
使用者繼續執行某些受到控管的行動,資源控管機制會依照 security context 檢查執行該行動的權限
前三步基本組成了權限驗證的程序,而 Spring Security 大概是這樣做的:
1. **資訊裝箱**: 取得 username 及 password 並放入 **UsernamePasswordAuthenticationToken**(實作了Authentication介面) 的實例中
```java
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
oAuthRequest.getUserId(), oAuthRequest.getToken());
```
3. **箱子送驗證**: 該 token 被傳遞給 **AuthenticationManager** 的實例作驗證 validation,若驗證成功,AuthenticationManager 會回傳一個裝著該使用者 context 資訊的 Authentication 實例
:point_right: **需實作AuthenticationProvider**,定義自己的驗證邏輯, 驗證時透過AuthenticationManager來操作
```java
Authentication authentication = authenticationManager.authenticate(authRequest);
```
4. **建立驗證成功的context**: Spring Security 容器呼叫**SecurityContextHolder**.getContext().setAuthentication() 方法,並將剛得到的 Authentication 物件當參數傳入,以建立 security context
```java
SecurityContextHolder.getContext().setAuthentication(authentication);
// 抓取principal資訊回傳response
CheckTokenResponse checkTokenResponse = UserDetailUtil.getUserDetail();
```
```java
// 確認context非null, 若驗證成功則取得目標類型的principal
public class UserDetailUtil {
public static CheckTokenResponse getUserDetail() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
Object checkTokenResponse = auth.getPrincipal();
if (checkTokenResponse instanceof CheckTokenResponse) {
return (CheckTokenResponse) checkTokenResponse;
}
}
return null;
}
}
```
6. 從這之後,該使用者就會被視為通過驗證,只有 SecurityContextHolder 能取得裝著使用者完整 context 資訊的 Authentication
### 建立驗證邏輯
```java
@Component
@Slf4j
public class AuthProvider implements AuthenticationProvider {
@Autowired
private AuthService authService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1. 初始的principal為傳入的username
// 預期在authenticate後建構更完整的principal(user detail, 可以寫專門的userderail工具處理)
// 2. credentials為傳入的password/token
String userId = (String) authentication.getPrincipal();
String token = (String) authentication.getCredentials();
log.info("[Auth] authenticate userId: {}, token: {}", userId, token);
CheckTokenResponse ssoResponse = new CheckTokenResponse();
try {
// 3. 核准的權限角色 前綴+角色名稱,例如 ROLE_ADMIN
Set<String> authoritySet = new HashSet<>();
// 自定義的服務, 回傳檢查結果及人員資訊
ssoResponse = authService.checkToken(new CheckTokenRequest(token, userId));
log.info("[Auth] authenticate checkToken result --> userId: {}, token: {}, ssoResponse: {}", userId, token, ssoResponse);
// 設定權限角色, 但好像傳入空值?
ssoResponse.setAuthorities(authoritySet.toArray(new String[authoritySet.size()]));
} catch (AccessDeniedException e) {
throw new AccessDeniedException(e.getMessage(), e);
} catch (IOException e) {
throw new AccessDeniedException("Connect to oauth server failed", e);
} catch (JAXBException e) {
throw new AccessDeniedException("Jaxb Marshaller failed", e);
} catch (Exception e) {
throw new AccessDeniedException(e.getMessage(), e);
}
// 驗證後, 設定更仔細的principal: 參數分別是principal, password, authorities
// UsernamePasswordAuthenticationToken 繼承了 AbstractAuthenticationToken 則實作 Authentication
// 故回傳值屬於Authentication
return new UsernamePasswordAuthenticationToken(ssoResponse, token, ssoResponse.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
```
---
### 問答區
Q: 如何避免session遺失,後端的Pod有多個的時候,就會遇到session在request導到不同Pods時候,Session遺失的問題
A:
- 固定單個pod, 不作HPA
- K8S service設定sessionAffinity: ClientIP
- AP層的session工具,例如redis來做到session共用
Q: 什麼是X509Certificate
A: 用於標準格式的公鑰證書
Q: 配置configure時, WebSecurity和HttpSecurity的差別?
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{...}
A: WebSecurity是global的filter設定, HttpSecurity則是單一filter設定,詳細如下:
HttpSecurity用來建置`SecurityFilterChain`,一個程式可以有多個SecurityFilterChain,因此需要有一個中介者`FilterChainProxy`來管理這些SecurityFilterChain;`WebSecurity`則是用來建置`FilterChainProxy`,額外還有清理SecurityContext以避免memory leak, 定義防火牆, 啟用Spring security的debug mode等。最後再由FilterChainProxy結合Spring的bridge proxy `DelegatingFilterProxy`,將此些security的filter融入程式
- [Spring Security : 安全构建器HttpSecurity和WebSecurity的区别](https://blog.csdn.net/andy_zhang2007/article/details/90051654)
- [The difference between WebSecurity and HttpSecurity in Spring Security](https://www.springcloud.io/post/2022-02/websecurity-and-httpsecurity/#gsc.tab=0)
- [Spring Security Configuration - HttpSecurity vs WebSecurity](https://stackoverflow.com/questions/56388865/spring-security-configuration-httpsecurity-vs-websecurity)
Q: 在使用filter或AOP時,從HttpServletRequest輸入流讀取InputStream(確切來說是ServletInputStream, 繼承者),後續將無法再獲得請求內容
A: HttpServletRequest只能讀取一次,讀取InputStream時有pointer指示位置,讀取完畢則回傳-1,若要重讀則需呼叫InputStream.reset(),能否reset(),取決markSupport方法,而ServletInputStream沒有override此些方法,InputStream也不實現reset()
:point_right: 所以要處理的話,以http request包裝容器存儲`HttpServletRequestWrapper`,再往後傳即可解決
- [HttpServletRequest输入流只能读取一次的问题](https://blog.csdn.net/lrb0677/article/details/125194150)
Q: 自定義filter後,請求不會自己傳到後續的filter
A: 若filter要使請求繼續處理,一定要調用filterChain.doFilter()
Q: 什麼是Servlet(Server Applet)
A: 基於協議requesr, response service的java class, 在java EE是Servlet規範, aka java實現的介面, servlet廣義來講是指直線了這個servlet介面的java class; servlet的容器收到http請求後,將request封裝成一個servletRequest,並加封裝一個servletResponse,呼叫servlet應用的service方法,將servletRequest及servletResponse傳入,方法執行後將servlet將servletResponse回應給broswer
Q: Interceptor和Filter的差別
A:
| | filter | interceptor | description |
| --- | -------- | -------- | -------- |
| 定義位置 | java.servlet | handlerInterceptor定義在org.springframework.web.servlet |分別在servlet類別, 及spring框架中定義|
| 配置 | web.yml | - | - |
| 作用 | servlet前後作用, 不考慮servlet實現 | 方法, exception前後, 更靈活解耦合 | spring框架可以優先選用interceptor |
| 範圍 | servlet規定 | web app, application及swing皆可用| |
| 規範 | servlet規定 | 存在spring容器, 由spring框架支持 | |
| 與spring關係 | 不可使用容器內資源 | 屬於spring組件, 可以使用spring內的任何資源,透過IOC注入到interceptor即可 | |
| 調用方 | 被server(e.g. tomcat)所使用 | spring調用 | filter早於interceptor執行 |
| 實現 | 函數回調 | 基於java反射及動態代理機制 | |
| 觸發時機 | 請求進入容器後, 請求進入servlet之前, 返回亦然 | | |
request -> filter > servlet > interceptor > controller
controller > interceptor > servlet > filter ->response
- [SpringBoot中AOP實現落地——Filter(過濾器)、Intercepter(攔截器)、Aspect](https://www.gushiciku.cn/pl/grNA/zh-tw)
For SameSiteCookies.NONE be aware, that cookies are also Secure (SSL used), otherwise they couldn't be applied.
By default since Chrome 80 cookies considered as SameSite=Lax!
- [Spring Security 之不要太相信這個中文手冊](https://idontwannarock.github.io/spring-security-reference/)
- [Spring Security](https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html5/)
- [springboot2.x Spring Security Vue-resource跨域问题解决](https://blog.csdn.net/Keith003/article/details/104221174)
- [Springboot应用中设置Cookie的SameSite属性](https://springboot.io/t/topic/2602)