Spring Security Oauth+JWT實現單點登錄SSO和權限控制

概述

關于SSO

網上關于SSO的介紹非常多,舉個例就是,當我們使用瀏覽器登錄了淘寶https://www.taobao.com以后,我們再訪問天貓https://www.tmall.com時就不需要再次登錄。也就是淘寶和天貓之間只需要登錄其中一個業(yè)務系統,另一個就自動登錄

運用場景

一般情況下,一個公司內肯定不止一個業(yè)務系統,公司內相互可信任的系統,我們希望可以實現和淘寶天貓相同的單點登錄效果。多個業(yè)務系統使用同一個認證服務也能簡化開發(fā)

相關知識

對于此文你所要了解的相關知識:

Spring Security Oauth+JWT單點登錄流程

實現原理

圖中一個認證服務器,兩個客戶端應用,A和B。用戶請求訪問A的服務器資源時會被引導到認證服務器進行授權,經過登錄認證和同意授權后返回授權碼給A,A的服務端收到授權碼后再次向認證服務器請求token,最終返回JWT類型的token,A解析接收到的JWT進行解析獲取到用戶相關信息保存進Spring Security的SecurityContext中實現登錄;此時用戶再次請求訪問B的服務器資源時,B會引導用戶到認證服務器進行請求授權,此時不在需要再次登錄認證,只要授權確認,確認后返回授權碼到B,B再次向認證服務器發(fā)起獲取token請求,認證服務器返回JWT,B對JWT進行解析,保存用戶進Spring Security的SecurityContext中實現登錄

實現流程

  1. 搭建SSO第三方認證授權服務中心
  2. 搭建客戶端應用1
  3. 搭建客戶端應用2
  4. 測試

前言

本文主要闡述使用Spring Security Oauth+JWT實現單點登錄和做到客戶端的權限控制,不闡述其相關源碼和實現原理

相關版本

Spring boot:2.2.0.RELEASE


實現步驟

一共創(chuàng)建三個服務,一個認證服務,兩個客戶端服務,采取單獨創(chuàng)建三個工程項目的方式,不是三個服務模塊化整合在一起。三個服務都引入相同的依賴

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
        <version>2.1.4.RELEASE</version>
</dependency>

搭建認證中心服務

  1. 配置文件
server.servlet.context-path= /server

只配置了服務的根路徑,端口用默認的8080

  1. 處理用戶信息
@Component
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /**
         * 這里實際情況應該是根據參數s查詢數據庫用戶數據
         */
        return new User(username, bCryptPasswordEncoder.encode("123"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_LOW,ROLE_MID"));
    }
}

了解Spring Security的同學應該對這個很了解這個類了(可以看我的上文http://m.itdecent.cn/p/e22fdeedc9a3了解相關),這里讓Spring Security只有輸入密碼123可以經過認證,并且這個用戶擁有ROLE_LOW和ROLE_MID的權限

  1. 認證中心服務配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private MyUserDetailService myUserDetailService;

//    @Autowired
    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.inMemory()
                .withClient("client1")
                .secret(passwordEncoder.encode("123"))
                .redirectUris("http://localhost:8081/client1/login")
                .scopes("all")
                .authorizedGrantTypes("authorization_code", "refresh_token", "password")
                .autoApprove(false)
                .and()
                .withClient("client2")
                .secret(passwordEncoder.encode("123"))
                .redirectUris("http://localhost:8082/client2/login")
                .scopes("all")
                .authorizedGrantTypes("authorization_code", "refresh_token", "password")
                .autoApprove(true);

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore())
                .accessTokenConverter(jwtAccessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("isAuthenticated()");
    }

    @Bean
    public TokenStore jwtTokenStore(){
        return  new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("testKey");
        return jwtAccessTokenConverter;
    }
}

認證中心服務配置類主要做三件事

  • @EnableAuthorizationServer注解把這個服務聲明為認證服務
  • 配置兩個客戶端相關信息,包含client_id,client_secret,redirect_uri,scope,支持的認證方式(密碼模式,授權碼模式等),以及配置允許自動授權。這里需要注意redirect_uri必須配置否則獲取授權碼時會報至少需要注冊一個回調地址的錯誤,還有配置client_secret時必須用BCryptPasswordEncoder進行編碼一下,否則會出現client_secret錯誤的問題,因為Spring Security Oauth默認情況下會對發(fā)起請求中的client_secret參數進行編碼
  • 配置生成的token類型為JWT
  • 配置token的安全約束
  1. Spring Security配置
@Configuration
@EnableWebSecurity
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.requestMatchers().antMatchers("/oauth/**", "/login/**", "/logout/**")
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**").authenticated()
                .and()
                .formLogin().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

有學習過Spring Security的同學對這個配置應該也比較熟悉了,主要就是

  • 注冊了一個BCryptPasswordEncoder實例
  • 配置了Spring Security安全規(guī)則
  • 給AuthenticationManagerBuilder配置上自定義的MyUserDetailService
  • 注冊一個AuthenticationManager的實例。如果想實現認證服務器支持密碼模式獲取token的話必須在這注冊這個實例,并且在認證服務配置類AuthorizationServerConfig中給AuthorizationServerEndpointsConfigurer配置上此實例,如下的endpoints.authenticationManager(authenticationManager)
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore())
                .accessTokenConverter(jwtAccessTokenConverter());
    }

搭建客戶端服務1

  1. 配置文件
server.port=8081
server.servlet.context-path=/client1

security.oauth2.client.client-id = client1
security.oauth2.client.client-secret= 123
security.oauth2.client.user-authorization-uri = http://localhost:8080/server/oauth/authorize
security.oauth2.client.access-token-uri = http://localhost:8080/server/oauth/token

security.oauth2.resource.jwt.key-uri = http://localhost:8080/server/oauth/token_key

端口設為8081,根路徑設為/client1,請求token的所必要的參數client-id、client-secret,user-authorization-uri是請求授權碼的地址,access-token-uri是請求token的地址,jwt.key-uri是獲取JWT的signKey的地址

  1. 客戶端配置文件
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class SsoClient1Config extends WebSecurityConfigurerAdapter{
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated();
    }
}

主要三個注解

  • @EnableOAuth2Sso讓該服務成為一個客戶端應用,當用戶訪問此服務下的資源時(接口)會引導用戶到認證服務器進行登錄認證,根據配置文件中的相關配置
  • @EnableWebSecurity讓Spring Security安全配置生效
  • @EnableGlobalMethodSecurity(prePostEnabled = true)讓接口方法上的權限控制生效
  1. 測試接口
@RestController
public class Client1Controller {

    @GetMapping("/high")
    @PreAuthorize("hasAuthority('ROLE_HIGH')")
    public String normal( ) {
        return "high permission";
    }

    @GetMapping("/mid")
    @PreAuthorize("hasAuthority('ROLE_MID')")
    public String medium() {
        return "mid permission";
    }

    @GetMapping("/low")
    @PreAuthorize("hasAuthority('ROLE_LOW')")
    public String admin() {
        return "low permission";
    }

}

三個接口分別要求不同的用戶權限,ROLE_HIGH、ROLE_MID、ROLE_LOW

搭建客戶端服務2

兩個客戶端其實基本一樣,我就直接上代碼了

  1. 配置文件
server.port=8082
server.servlet.context-path=/client2

security.oauth2.client.client-id = client2
security.oauth2.client.client-secret= 123
security.oauth2.client.user-authorization-uri = http://localhost:8080/server/oauth/authorize
security.oauth2.client.access-token-uri = http://localhost:8080/server/oauth/token

security.oauth2.resource.jwt.key-uri = http://localhost:8080/server/oauth/token_key
  1. 配置類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class SsoClient2Config extends WebSecurityConfigurerAdapter{
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated();
    }
}
  1. 測試接口
@RestController
public class Client2Controller {

    @GetMapping("/high")
    @PreAuthorize("hasAuthority('ROLE_HIGH')")
    public String normal( ) {
        return "high permission";
    }

    @GetMapping("/mid")
    @PreAuthorize("hasAuthority('ROLE_MID')")
    public String medium() {
        return "mid permission";
    }

    @GetMapping("/low")
    @PreAuthorize("hasAuthority('ROLE_LOW')")
    public String admin() {
        return "low permission";
    }

}

測試

  1. 單點登錄測試
    三個服務全部啟動,直接訪問客戶端1的接口localhost:8081/client1/low
    訪問low接口

    隨后被引導到登錄頁面
    登錄頁

    可以發(fā)現原本訪問的接口在localhost:8081/client1下,跳轉到了localhost:8081/server下面。輸入用戶名user,密碼123登錄
    請求授權

    跳轉到是否同意授權的頁面,點擊Authorize
    成功訪問客戶端1接口

    這時候意味著用戶已經登錄客戶端1,嘗試訪問客戶端2的接口localhost:8082/client2/low
    成功訪問客戶端2接口

    這時候沒有讓我們再次登錄就可以直接訪問了(如果你在認證服務的配置中把客戶端2的自動授權設置為false,這里會再次向你詢問是否同意授權,但是不用登錄),這已經實現單點登錄了
  2. 權限控制測試
    上面我們登錄的這個用戶只有ROLE_LOW和ROLE_MID的權限,登錄狀態(tài)下再次嘗試訪localhost:8081/client1/mid
    訪問mid成功

    可以看出訪問需要ROLE_LOW和ROLE_MID權限的接口都沒問題,再訪問http://localhost:8081/client1/high
    訪問high失敗

    訪問需要ROLE_HIGH權限的接口返回403,說明我們的權限控制也實現了

工程結構

最后上一下我的工程結構

  1. 認證中心服務


    認證中心
  2. 客戶端1


    客戶端1
  3. 客戶端2


    客戶端2

總結

  • 搭建SSO第三方認證授權服務中心
  • 搭建客戶端應用1
  • 搭建客戶端應用2
  • 測試
    Github源碼地址:https://github.com/iemi/sso
    覺得有用的同學Github給顆星星謝謝啦
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容