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)