基於Oauth2,springsecurity單點登錄SSO,前後端分離和SPA方式實現方式。

基於Oauth2,springsecurity單點登錄SSO,前後端分離和SPA方式實現方式。

在接到需求要做SPA方式的單點登錄的需求,發現好多的坑,之前我們接觸的只是瀏覽器的單點登錄,基於session的或者是基於app的基於token的,app類似SPA方式,但是有個不同點,就是在多個app或者多個SPA下怎麼做單點登錄。一開始以爲很容易。但是在搞一段時間啊後發現自己越走越黑,越走越遠,總結下來自己對協議理解還是不夠透徹,對之前理解的前後端分離的SSO還是止步於session的交互方式。在涉及到多個域之間換取token還是有一些問題。
廢話不說了。希望對現在在做了前後端分離的你有所幫助。

發展歷史

從OAuth1到OAuth2
1.0協議每個token都有一個加密,2.0則不需要。這樣來看1.0似乎更加安全,但是2.0要求使用https協議,安全性也更高一籌。
1.0只有一個用戶授權流程。2.0可以從多種途徑獲取訪問令牌
a)授權碼 b)客戶端私有證書 c)資源擁有者密碼證書 d)刷新令牌 e)斷言證書
2.0的用戶授權過程有2步,1.0的授權分3步,

在這裏插入圖片描述

OAuth2涉及角色

資源擁有者
可以是一個人也可以是一個公司實體,對資源持有的實體。

資源服務
受保護的資源,可以使用token令牌來訪問

客戶端
需要請求資源的應用客戶端,PC,APP

認證服務
發放令牌的服務,驗證資源所有者並獲得授權

協議流程

在這裏插入圖片描述

授權模式

密碼憑證授權模式
第三方Web服務器端應用與第三方原生App
在這裏插入圖片描述
在這裏插入圖片描述

密碼模式(resource owner password credentials)
這種模式是最不推薦的,因爲client可能存了用戶密碼
這種模式主要用來做遺留項目升級爲oauth2的適配方案
當然如果client是自家的應用,也可以.
支持refresh token

授權碼授權模式
密碼模式:第一方單頁應用與第一方原生App
在這裏插入圖片描述

授權碼模式是四種模式中最繁瑣也是最安全的一種模式。

1.client向資源服務器請求資源,被重定向到授權服務器(AuthorizationServer)
2.瀏覽器向資源擁有者索要授權,之後將用戶授權發送給授權服務器
3.授權服務器將授權碼(AuthorizationCode)轉經瀏覽器發送給client
4.client拿着授權碼向授權服務器索要訪問令牌
5.授權服務器返回Access Token和Refresh Token給cilent

簡化授權模式
第三方單頁面應用
在這裏插入圖片描述
簡化模式相對於授權碼模式省略了,提供授權碼,然後通過服務端發送授權碼換取AccessToken的過程。

1.client請求資源被瀏覽器轉發至授權服務器
2.瀏覽器向資源擁有者索要授權,之後將用戶授權發送給授權服務器
3.授權服務器將AccessToken以Hash的形式存放在重定向uri的fargment中發送給瀏覽器
4.瀏覽器訪問重定向URI
5.資源服務器返回一個腳本,用以解析Hash中的AccessToken
6.瀏覽器將Access Token解析出來
7.將解析出的Access Token發送給client

一般簡化模式用於沒有服務器端的第三方單頁面應用,因爲沒有服務器端就無法使用授權碼模式。

客戶端憑據模式
沒有用戶參與的,完全信任的服務器端服務
在這裏插入圖片描述

這是一種最簡單的模式,只要client請求,我們就將AccessToken發送給它。
(A)客戶端向認證服務器進行身份認證,並要求一個訪問令牌。
(B)認證服務器確認無誤後,向客戶端提供訪問令牌。

代碼解讀

在這裏插入圖片描述
在這裏插入圖片描述
debug可以看見所有的授權模式
在這裏插入圖片描述
以下是我操作跟蹤源代碼的步驟,還有遇到的一些問題。

1.訪問授權地址如上圖,state是推薦項可以不寫
http://www.clouds1000.com/oauth/authorize?response_type=code&client_id=026f49c0-1a53-4031-9a2a-7899819548ec&redirect_uri=http://www.clouds1000.com&scope=all
2.出現User must be authenticated with Spring Security before authorization can be completed.需要登錄,增加配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private SelfAuthenticationSuccessHandler selfAuthenticationSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
        super.configure(http);
    }
}
3.error="invalid_request", error_description="At least one redirect_uri must be registered with the client."
說明沒有做授權地址覆蓋重寫ClientDetailsServiceConfigurer 
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //添加客戶端信息
        clients.inMemory()                  // 使用in-memory存儲客戶端信息
                .withClient("janle")
                .redirectUris("http://www.clouds1000.com");
    }
}
4.出現錯誤error="invalid_grant", error_description="A client must have at least one authorized grant type."
clients.inMemory()                  // 使用in-memory存儲客戶端信息
                .withClient("janle")
                .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                .redirectUris("http://www.clouds1000.com");
5.出現授權信息,選擇授權,跳轉地址如下,返回對應的code碼
http://www.clouds1000.com/?code=OZiJN8
6.訪問時候一直彈出basic的頁面,不能登錄,檢查密碼加解密是否正確
7.所有的都好了以後返回401,需要檢查代碼中的客戶端的配置是否正確,
clients.inMemory()                  // 使用in-memory存儲客戶端信息
                .withClient("janle")
                .secret("{bcrypt}" + new BCryptPasswordEncoder().encode("janleSecret"))
                .authorizedGrantTypes("password", "authorization_code", "refresh_token", "client_credentials")
                .scopes("all")
                .authorities("oauth2")  //是否遺漏該項
                .redirectUris("http://www.clouds1000.com");
8.這一步測試code碼模式已經好了,發現使用密碼模式時候找不到對應的token生成TokenGranter,由於authenticationManager爲空的話會構建CompositeTokenGranter對應的4個授權模式,具體代碼在org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters
調整代碼如下:
EnableWebSecurity中:
 /**
     * 密碼模式需要重寫配置
     *
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
@EnableAuthorizationServer中:
 @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //在WebSecurityConfigurerAdapter的實現類當中,重寫,使用密碼模式引入,不然不會加載這種模式
        endpoints.authenticationManager(authenticationManager);
        super.configure(endpoints);
    }

9.測試密碼模式
post提交http://www.clouds1000.com/oauth/token
grant_type=password
username=user_1
password=123456
scope=all

10.再次測試code碼模式輸入輸入對應返回code參數
[{"key":"grant_type","value":"authorization_code","description":""},{"key":"client_id","value":"janle","description":""},{"key":"redirect_uri","value":"http://www.clouds1000.com","description":""},{"key":"client_secret","value":"janleSecret","description":""},{"key":"code","value":"yD2dHH","description":""}]

總體的代碼結構和調用跟蹤,綠色是接口,黃色的是類。
在這裏插入圖片描述
繼續源碼解讀,標紅的地方注意下就好。
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述在這裏插入圖片描述

在我們系統的中設計

基於APP的實現使用流程。在這裏插入圖片描述
但是這個在單一的SPA應用下是可以的,如果在多個SPA應用下不能適用。所以在這種情況下我們需要利用主域的session來做token的交換。所以這樣我們前後端分離是好事,但是分離以後卻帶來了不好的事情。

SSO實現流程分析

基於Oauth2前後端分離SSO失敗流程
在這裏插入圖片描述

具體需要注意的地方解釋說明了:
4.登錄成功後攜帶code碼跳轉到前端client.7bule.com
8.重定向返回的是api.7bule.com的請求地址,由於應用api.7bule.com做了session的請求處理,
9.前端只能看到後臺跳轉走地址,不能獲取任何後臺返回來的信息,所以不能拿到請求後臺的token值

基於Oauth2的SSO適用前後端分離單應用
在這裏插入圖片描述
這種只能適用於SPA的模式,不能應用於多個SPA之間的跳轉。

基於Oauth2的SSO適用多個SPA應用
在這裏插入圖片描述
這種支持簡單的實現了跨域跳轉基於session會話的單點登錄。需要做一些優化,要不有安全問題。分享過的PPT,可以隨意下載。
https://download.csdn.net/download/u013642886/11216813

參考文獻

https://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://tools.ietf.org/html/rfc6749#section-1.3.1
http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 恩謙提供

具體代碼

環境準備

前端頁面地址: fe.clouds1000.com
前端調用後臺接口地址 api.clouds1000.com
SSO認證服務地址 passport.clouds1000.com
前端無權限處理頁 fe.clouds1000.com/oauth

具體僞代碼如下:

axios 響應攔截器處理 無權限後會將當前頁面地址進行緩存
Axios.interceptors.response.use(res => {
  NProgress.done()
  const status = Number(res.status) || 200
  const message = res.data.msg || errorCode[status] || errorCode['default']
  if (status === 401) {
    store.dispatch('FedLogOut').then(() => {
		setStore({name: 'history_path', location.href }) // 僞代碼
      router.push({ path: '/oauth' })
    })
    return
  }
)}

//路由導航守衛處理
router.beforeEach((to, from, next) => {
  // 緩衝設置
  if (to.meta.keepAlive === true && store.state.tags.tagList.some(ele => {
    return ele.value === to.fullPath
  })) {
    to.meta.$keepAlive = true
  } else {
    NProgress.start()
    if (to.meta.keepAlive === true && validatenull(to.meta.$keepAlive)) {
      to.meta.$keepAlive = true
    } else {
      to.meta.$keepAlive = false
    }
  }
  const meta = to.meta || {}
  if (store.getters.access_token) {
    if (store.getters.isLock && to.path !== lockPage) {
      next({ path: lockPage })
    } else if (to.path === '/login') {
      next({ path: '/' })
    } else {
      if (store.getters.roles.length === 0) {
        let loading = Loading.service({
          lock: true,
          text: `登錄中,請稍後。。。`,
          spinner: 'el-icon-loading'
        })
        store.dispatch('GetUserInfo').then(() => {
          loading.close()
          next({ ...to, replace: true })
        }).catch(() => {
          loading.close()
          store.dispatch('FedLogOut').then(() => {
			setStore({name: 'history_path', location.href }) // 僞代碼
            next({ path: '/oauth' })
          })
        })
      } else {
        const value = to.query.src || to.fullPath
        const label = to.query.name || to.name
        if (meta.isTab !== false && !validatenull(value) && !validatenull(label)) {
          store.commit('ADD_TAG', {
            label: label,
            value: value,
            params: to.params,
            query: to.query,
            group: router.$avueRouter.group || []
          })
        }
        next()
      }
    }
  } else {
    if (meta.isAuth === false) {
      next()
    } else {
	setStore({name: 'history_path', location.href }) // 僞代碼
      next('/oauth')
    }
  }
})

無權限處理

在oauth頁做如下配置
1.無權限跳轉至/oauth 頁 ,需要請求跳轉頁的地址  請求地址爲   api.clouds1000.com/oauth/loginuri
2.拿到響應後需要做decodeURIComponent ,然後通過location.href 進行跳轉
3.在 sso 認證中心進行登錄 passport.clouds1000.com/login
4.登錄成功後,認證中心會攜帶code值重定向回前端無權限處理頁
5. 通過獲取querySting內的code,調業務系統的獲取token的接口,設置token api.clouds1000.com/auth/token
6. 拿到響應後設置token, 請求相應業務接口
7. 從緩存中獲取之前存儲的歷史記錄頁,跳轉回無權限之前的頁面
代碼:
created () {
    if (this.$route.query.code) {
      let query = this.$route.query
      AdminService.login({ code: query.code }).then(res => {
        console.log(res)
        this.loading.close()
        this.$store.commit('SET_ACCESS_TOKEN', res.access_token)
        AdminService.infos().then(() => {
			// TODO  拿緩存跳轉原頁面
		})
        AdminService.user()
      })
    } else {
      AdminService.oauth().then(res => {
        window.location.href = decodeURIComponent(res)
      })
    }
  }

後端的配置

 <dependency>
    <groupId>com.thclouds.ppassport</groupId>
    <artifactId>ppassport-auth</artifactId>
    <version>1.0-SNAPSHOT</version>
 </dependency>


@SpringBootApplication
public class UiApplication {
    public static void main(String[] args) {
        SpringApplication.run(UiApplication.class, args);
    }
}

@EnableResourceServer
@Configuration
public class Oauth2ClientConfig extends AbstractSecurityConfig {
}


security:
  path:
	//需要忽略的地址。
    ignores: /,/index,/static/**,/css/**, /image/**, /favicon.ico, /js/**,/plugin/**,/avue.min.js,/img/**,/fonts/**  
  oauth2:
    client:
      client-id: 業務系統的client-id
      client-secret: 業務系統的client-secret
      user-authorization-uri: http://passport.clouds1000.com/oauth/authorize
      access-token-uri: http://passport.clouds1000.com/oauth/token
      scope: all
      registered-redirect-uri: http://前端業務地址/oauth
    resource:
      token-info-uri: http://passport.clouds1000.com/oauth/check_token
      user-info-uri: http://passport.clouds1000.com/user
      jwt:
        #需要攜帶client
        key-uri: http://passport.clouds1000.com/oauth/token_key
        key-value: janle
       

具體的demo地址 https://download.csdn.net/download/u013642886/11501079

https://github.com/ljz0721cx/passport

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章