您好,登錄后才能下訂單哦!
本篇內容介紹了“JWT的單點登陸SSO開發及原理是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
> ??在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth3 等權限、認證相關的內容、原理及設計學習并整理一遍。本系列文章就是在學習的過程中加強印象和理解所撰寫的,如有侵權請告知。
> 項目環境: > - JDK1.8 > - Spring boot 2.x > - Spring Security 5.x
??單點登錄(Single Sign On),簡稱為SSO,是目前比較流行的企業業務整合的解決方案之一。 SSO的定義是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。 單點登陸本質上也是OAuth3的使用,所以其開發依賴于授權認證服務,如果不清楚的可以看我的上一篇文章。
??從單點登陸的定義上來看就知道我們需要新建個應用程序,我把它命名為 security-sso-client。接下的開發就在這個應用程序上了。
??主要依賴 spring-boot-starter-security、spring-security-oauth3-autoconfigure、spring-security-oauth3 這3個。其中 spring-security-oauth3-autoconfigure 是Spring Boot 2.X 才有的。
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-security</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-redis</artifactid> </dependency> <!--@EnableOAuth3Sso 引入,Spring Boot 2.x 將這個注解移到該依賴包--> <dependency> <groupid>org.springframework.security.oauth.boot</groupid> <artifactid>spring-security-oauth3-autoconfigure</artifactid> <exclusions> <exclusion> <groupid>org.springframework.security.oauth</groupid> <artifactid>spring-security-oauth3</artifactid> </exclusion> </exclusions> <version>2.1.7.RELEASE</version> </dependency> <!-- 不是starter,手動配置 --> <dependency> <groupid>org.springframework.security.oauth</groupid> <artifactid>spring-security-oauth3</artifactid> <!--請注意下 spring-authorization-oauth3 的版本 務必高于 2.3.2.RELEASE,這是官方的一個bug: java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V 要求必須大于2.3.5 版本,官方解釋:https://github.com/BUG9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth3/open --> <version>2.3.5.RELEASE</version> </dependency>
??單點的基礎配置引入是依賴 @EnableOAuth3Sso 實現的,在Spring Boot 2.x 及以上版本 的 @EnableOAuth3Sso 是在 spring-security-oauth3-autoconfigure 依賴里的。我這里簡單配置了一下:
@Configuration @EnableOAuth3Sso public class ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/","/error","/login").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); } }
?? 因為單點期間可能存在某些問題,會重定向到 /error ,所以我們把 /error 設置成無權限訪問。
@RestController @Slf4j public class TestController { @GetMapping("/client/{clientId}") public String getClient(@PathVariable String clientId) { return clientId; } }
<meta charset="UTF-8"> <title>OSS-client</title> <h2>OSS-client</h2> <a href="http://localhost:8091/client/1">跳轉到OSS-client-1</a> <a href="http://localhost:8092/client/2">跳轉到OSS-client-2</a>
?? 由于我們要測試多應用間的單點,所以我們至少需要2個單點客戶端,我這邊通過Spring Boot 的多環境配置實現。
?? 我們都知道單點實現本質就是Oauth3的授權碼模式,所以我們需要配置訪問授權服務器的地址信息,包括 :
security.oauth3.client.user-authorization-uri = /oauth/authorize 請求認證的地址,即獲取code 碼
security.oauth3.client.access-token-uri = /oauth/token 請求令牌的地址
security.oauth3.resource.jwt.key-uri = /oauth/token_key 解析jwt令牌所需要密鑰的地址,服務啟動時會調用 授權服務該接口獲取jwt key,所以務必保證授權服務正常
security.oauth3.client.client-id = client1 clientId 信息
security.oauth3.client.client-secret = 123456 clientSecret 信息
其中有幾個配置需要簡單解釋下:
security.oauth3.sso.login-path=/login OAuth3授權服務器觸發重定向到客戶端的路徑 ,默認為 /login,這個路徑要與授權服務器的回調地址(域名)后的路徑一致
server.servlet.session.cookie.name = OAUTH2CLIENTSESSION 解決單機開發存在的問題,如果是非單機開發可忽略其配置
auth-server: http://localhost:9090 # authorization服務地址
security: oauth3: client: user-authorization-uri: ${auth-server}/oauth/authorize #請求認證的地址 access-token-uri: ${auth-server}/oauth/token #請求令牌的地址 resource: jwt: key-uri: ${auth-server}/oauth/token_key #解析jwt令牌所需要密鑰的地址,服務啟動時會調用 授權服務該接口獲取jwt key,所以務必保證授權服務正常 sso: login-path: /login #指向登錄頁面的路徑,即OAuth3授權服務器觸發重定向到客戶端的路徑 ,默認為 /login
server: servlet: session: cookie: name: OAUTH2CLIENTSESSION # 解決 Possible CSRF detected - state parameter was required but no state could be found 問題 spring: profiles: active: client1
#### application-client1.yml 配置 ?? application-client2 和 application-client1是一樣的,只是端口號和client信息不一樣而已,這里就不再重復貼出了。
server: port: 8091
security: oauth3: client: client-id: client1 client-secret: 123456
#### 五、單點測試 ?? 效果如下: ![https://cache.yisu.com/upload/information/20210524/347/787917.jpg](https://cache.yisu.com/upload/information/20210524/347/787917.jpg) ??從效果圖中我們可以發現,當我們第一次訪問client2 的接口時,跳轉到了授權服務的登陸界面,完成登陸后成功跳轉回到了client2 的測試接口,并且展示了接口返回值。此時我們訪問client1 的 測試接口時直接返回(表面現象)了接口返回值。這就是單點登陸的效果,好奇心強的同學一定會在心里問道:它是如何實現的? 那么接下來我們就來揭開其面紗。 ### 二、 單點登陸原理解析 #### 一、@EnableOAuth3Sso ?? 我們都知道 @EnableOAuth3Sso 是實現單點登陸的最核心配置注解,那么我們來看下 @EnableOAuth3Sso 的源碼:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @EnableOAuth3Client @EnableConfigurationProperties(OAuth3SsoProperties.class) @Import({ OAuth3SsoDefaultConfiguration.class, OAuth3SsoCustomConfiguration.class, ResourceServerTokenServicesConfiguration.class }) public @interface EnableOAuth3Sso {
}
?? 其中我們關注4個配置文件的引用: ResourceServerTokenServicesConfiguration 、OAuth3SsoDefaultConfiguration 、 OAuth3SsoProperties 和 @EnableOAuth3Client: - OAuth3SsoDefaultConfiguration 單點登陸的核心配置,內部創建了 SsoSecurityConfigurer 對象, SsoSecurityConfigurer 內部 主要是配置 **OAuth3ClientAuthenticationProcessingFilter** 這個單點登陸核心過濾器之一。 - ResourceServerTokenServicesConfiguration 內部讀取了我們在 yml 中配置的信息 - OAuth3SsoProperties 配置了回調地址url ,這個就是 security.oauth3.sso.login-path=/login 匹配的 - @EnableOAuth3Client 標明單點客戶端,其內部 主要 配置了 **OAuth3ClientContextFilter** 這個單點登陸核心過濾器之一 #### 二、 OAuth3ClientContextFilter ?? OAuth3ClientContextFilter 過濾器類似于 ExceptionTranslationFilter , 它本身沒有做任何過濾處理,只要當 chain.doFilter() 出現異常后 做出一個重定向處理。 但別小看這個重定向處理,它可是實現單點登陸的第一步,還記得第一次單點時會跳轉到授權服務器的登陸頁面么?而這個功能就是 OAuth3ClientContextFilter 實現的。我們來看下其源碼:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; request.setAttribute(CURRENT_URI, calculateCurrentUri(request)); // 1、記錄當前地址(currentUri)到HttpServletRequest
try { chain.doFilter(servletRequest, servletResponse); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer .getFirstThrowableOfType( UserRedirectRequiredException.class, causeChain); if (redirect != null) { // 2、判斷當前異常 UserRedirectRequiredException 對象 是否為空 redirectUser(redirect, request, response); // 3、重定向訪問 授權服務 /oauth/authorize } else { if (ex instanceof ServletException) { throw (ServletException) ex; } if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } throw new NestedServletException("Unhandled exception", ex); } } }
?? Debug看下: ![微信圖片_20190916173425.png](https://cache.yisu.com/upload/information/20210524/347/787918.jpg) ??整個 filter 分三步: - 1、記錄當前地址(currentUri)到HttpServletRequest - 2、判斷當前異常 UserRedirectRequiredException 對象 是否為空 - 3、重定向訪問 授權服務 /oauth/authorize #### 三、 OAuth3ClientAuthenticationProcessingFilter ?? OAuth3ClientContextFilter 過濾器 其要完成的工作就是 通過獲取到的code碼調用 授權服務 /oauth/token 接口獲取 token 信息,并將獲取到的token 信息解析成 OAuth3Authentication 認證對象。起源如下:
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
OAuth3AccessToken accessToken; try { accessToken = restTemplate.getAccessToken(); //1、 調用授權服務獲取token } catch (OAuth3Exception e) { BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e); publish(new OAuth3AuthenticationFailureEvent(bad)); throw bad; } try { OAuth3Authentication result = tokenServices.loadAuthentication(accessToken.getValue()); // 2、 解析token信息為 OAuth3Authentication 認證對象并返回 if (authenticationDetailsSource!=null) { request.setAttribute(OAuth3AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue()); request.setAttribute(OAuth3AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType()); result.setDetails(authenticationDetailsSource.buildDetails(request)); } publish(new AuthenticationSuccessEvent(result)); return result; } catch (InvalidTokenException e) { BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e); publish(new OAuth3AuthenticationFailureEvent(bad)); throw bad; } }
?? 整個 filter 2點功能: - restTemplate.getAccessToken(); //1、 調用授權服務獲取token - tokenServices.loadAuthentication(accessToken.getValue()); // 2、 解析token信息為 OAuth3Authentication 認證對象并返回 ??完成上面步驟后就是一個正常的security授權認證過程,這里就不再講述,有不清楚的同學可以看下我寫的相關文章。 #### 四、 AuthorizationCodeAccessTokenProvider ?? 在講述 OAuth3ClientContextFilter 時有一點沒講,那就是 UserRedirectRequiredException 是 誰拋出來的。 在講述 OAuth3ClientAuthenticationProcessingFilter 也有一點沒講到,那就是它是如何判斷出 當前 /login 是屬于 需要獲取code碼的步驟還是去獲取 token 的步驟( 當然是判斷/login 是否帶有code 參數,這里主要講明是誰來判斷的)。 這2個點都設計到了 AuthorizationCodeAccessTokenProvider 這個類。這個類是何時被調用的? 其實 OAuth3ClientAuthenticationProcessingFilter 隱藏在 restTemplate.getAccessToken(); 這個方法內部 調用的 accessTokenProvider.obtainAccessToken() 這里。 我們來看下OAuth3ClientAuthenticationProcessingFilter 的 obtainAccessToken() 方法內部源碼:
public OAuth3AccessToken obtainAccessToken(OAuth3ProtectedResourceDetails details, AccessTokenRequest request) throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException, OAuth3AccessDeniedException {
AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details; if (request.getAuthorizationCode() == null) { //1、 判斷當前參數是否包含code碼 if (request.getStateKey() == null) { throw getRedirectForAuthorization(resource, request); //2、 不包含則拋出 UserRedirectRequiredException 異常 } obtainAuthorizationCode(resource, request); } return retrieveToken(request, resource, getParametersForTokenRequest(resource, request), getHeadersForTokenRequest(request)); // 3 、 包含則調用獲取token }
整個方法內部分3步: - 1、 判斷當前參數是否包含code碼 - 2、 不包含則拋出 UserRedirectRequiredException 異常 - 3、 包含繼續獲取token ?? 最后可能有同學會問,為什么第一個客戶端單點要跳轉到授權服務登陸頁面去登陸, 而當問第二個客戶端卻沒有,其實 2次 客戶端單點的流程都是一樣的,都是授權碼模式,但為什么客戶端2 卻不需要登陸呢? 其實是因為Cookies/Session的原因,因為我們訪問同2個客戶端基本上都是在同一個瀏覽器中進行的。 不信的同學可以試試2個瀏覽器分別訪問2個單點客戶端。 ### 三、 個人總結 ??單點登陸本質上就是授權碼模式,所以理解起來還是很容易的,如果非要給個流程圖,還是那張授權碼流程圖: ![https://cache.yisu.com/upload/information/20210524/347/787919.jpg](https://cache.yisu.com/upload/information/20210524/347/787919.jpg) ?? 本文介紹 基于JWT的單點登陸(SSO)開發及原理解析 開發的代碼可以訪問代碼倉庫 ,項目的github 地址 : https://github.com/BUG9/spring-security ?? ?? ?? **如果您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!**
“JWT的單點登陸SSO開發及原理是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。