SpringCloud OAuth2實現單點登錄以及OAuth2源碼原理解析


SpringCloud OAuth2實現單點登錄以及OAuth2源碼原理解析

意識流丶
42019.05.22 14:39:50字數 2,594閱讀 9,647
單點登錄(Single Sign On),簡稱為 SSO,是目前比較流行的企業業務整合的解決方案之一。SSO的定義是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。
Spring Security OAuth 是建立在 Spring Security 的基礎之上 OAuth2.0 協議實現的一個類庫
Spring Security OAuth2 為 Spring Cloud 搭建認證授權服務(能夠更好的集成到 Spring Cloud 體系中)

單點登錄主要包括
服務端:一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
客戶端:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成后方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可

相關版本:
SpringBoot:2.1.5.RELEASE
SpringCloud :Greenwich.SR1

認證中心Server
1.引入OAuth2依賴和web依賴(不加啟動時會報無法訪問javax.servlet.Filter)
OAuth2中包含spring-cloud-starter-security和spring-security-oauth2-autoconfigure


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
2.創建驗證用戶,設置用戶名和密碼並設置角色權限

@Component
public class SSOUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        String user="user";
        if( !user.equals(s) ) {
            throw new UsernameNotFoundException("用戶不存在");
        }
        return new User( s, passwordEncoder.encode("123456"), 
              AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}
3.認證服務器配置
①加入@EnableAuthorizationServer注解來啟動OAuth2.0授權服務機制
②通過繼承AuthorizationServerConfigurerAdapter並且覆寫其中的三個configure方法來進行配置
3.1.ClientDetailsServiceConfigurer
用於定義客戶詳細信息服務的配置器。客戶端詳情信息進行初始化,能夠把客戶端詳情信息寫在內存中或者是通過數據庫來存儲調取詳情信息。

多個客戶端來連接Spring OAuth2 Auth Server,需要在配置類里為inMemory生成器定義多個withClients


@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 定義了兩個客戶端應用的通行證
        clients.inMemory()// 使用in-memory存儲
                .withClient("ben1")// client_id
                .secret(new BCryptPasswordEncoder().encode("123456"))// client_secret
                .authorizedGrantTypes("authorization_code", "refresh_token")// 該client允許的授權類型
                .scopes("all")// 允許的授權范圍
                .autoApprove(false)
                //加上驗證回調地址
                .redirectUris("http://localhost:8086/login")
                .and()
                .withClient("ben2")
                .secret(new BCryptPasswordEncoder().encode("123456"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all")
                .autoApprove(false)
                .redirectUris("http://localhost:8087/login");
    }
必須設置回調地址redirectUris,並且格式是http://客戶端IP:端口/login的格式,否則會報OAuth Error error=”invalid_request”, error_description=”At least one redirect_uri must be registered with the client.”

 

原理如下圖:

 
ClientDetailsServiceConfigurer原理圖.png
ClientDetailsServiceConfiguration根據ClientDetailsServiceConfigurer配置,交給ClientDetailsServiceBuilder的實現類通過ClientBuilder創建Client

ClientDetailsServiceConfigurer 核心源碼


public class ClientDetailsServiceConfigurer extends SecurityConfigurerAdapter<ClientDetailsService, ClientDetailsServiceBuilder<?>> {
    public InMemoryClientDetailsServiceBuilder inMemory() throws Exception {
        InMemoryClientDetailsServiceBuilder next = ((ClientDetailsServiceBuilder)this.getBuilder()).inMemory();
        this.setBuilder(next);
        return next;
    }

    public JdbcClientDetailsServiceBuilder jdbc(DataSource dataSource) throws Exception {
        JdbcClientDetailsServiceBuilder next = ((ClientDetailsServiceBuilder)this.getBuilder()).jdbc().dataSource(dataSource);
        this.setBuilder(next);
        return next;
    }
    ......
}
ClientDetailsServiceBuilder
ClientBuilder是ClientDetailsServiceBuilder的一個內部類,其中build()會被ClientDetailsServiceConfiguration所調用

ClientDetailsServiceBuilder部分源碼


public class ClientDetailsServiceBuilder<B extends ClientDetailsServiceBuilder<B>> 
              extends SecurityConfigurerAdapter<ClientDetailsService, B> 
              implements SecurityBuilder<ClientDetailsService> {
    private List<ClientDetailsServiceBuilder<B>.ClientBuilder> clientBuilders = new ArrayList();

   //設置Client並把其放到list
    public ClientDetailsServiceBuilder<B>.ClientBuilder withClient(String clientId) {
        ClientDetailsServiceBuilder<B>.ClientBuilder clientBuilder = new ClientDetailsServiceBuilder.ClientBuilder(clientId);
        this.clientBuilders.add(clientBuilder);
        return clientBuilder;
    }

    //創建ClientDetailsService 
    public ClientDetailsService build() throws Exception {
        Iterator var1 = this.clientBuilders.iterator();

        while(var1.hasNext()) {
            ClientDetailsServiceBuilder<B>.ClientBuilder clientDetailsBldr = (ClientDetailsServiceBuilder.ClientBuilder)var1.next();
            this.addClient(clientDetailsBldr.clientId, clientDetailsBldr.build());
        }

        return this.performBuild();
    }
    
    public final class ClientBuilder {
        private final String clientId;
        private Collection<String> authorizedGrantTypes;
        private Collection<String> authorities;
        private Integer accessTokenValiditySeconds;
        private Integer refreshTokenValiditySeconds;
        private Collection<String> scopes;
        private Collection<String> autoApproveScopes;
        private String secret;
        private Set<String> registeredRedirectUris;
        private Set<String> resourceIds;
        private boolean autoApprove;
        private Map<String, Object> additionalInformation;

        private ClientDetails build() {
            BaseClientDetails result = new BaseClientDetails();
            result.setClientId(this.clientId);
            result.setAuthorizedGrantTypes(this.authorizedGrantTypes);
            result.setAccessTokenValiditySeconds(this.accessTokenValiditySeconds);
            result.setRefreshTokenValiditySeconds(this.refreshTokenValiditySeconds);
            result.setRegisteredRedirectUri(this.registeredRedirectUris);
            result.setClientSecret(this.secret);
            result.setScope(this.scopes);
            result.setAuthorities(AuthorityUtils.createAuthorityList((String[])this.authorities.toArray(new String[this.authorities.size()])));
            result.setResourceIds(this.resourceIds);
            result.setAdditionalInformation(this.additionalInformation);
            if (this.autoApprove) {
                result.setAutoApproveScopes(this.scopes);
            } else {
                result.setAutoApproveScopes(this.autoApproveScopes);
            }

            return result;
        }

        private ClientBuilder(String clientId) {
            this.authorizedGrantTypes = new LinkedHashSet();
            this.authorities = new LinkedHashSet();
            this.scopes = new LinkedHashSet();
            this.autoApproveScopes = new HashSet();
            this.registeredRedirectUris = new HashSet();
            this.resourceIds = new HashSet();
            this.additionalInformation = new LinkedHashMap();
            this.clientId = clientId;
        }
        ......
    }
    ......
}
客戶端信息配置屬性說明:
clientId:(必須的)第三方用戶的id(可理解為賬號)。
clientSecret:第三方應用和授權服務器之間的安全憑證(可理解為密碼)
scope:指定客戶端申請的權限范圍,可選值包括read,write,trust;其實授權賦予第三方用戶可以在資源服務器獲取資源,第三方訪問資源的一個權限,訪問范圍。
resourceIds:客戶端所能訪問的資源id集合
authorizedGrantTypes:此客戶端可以使用的授權類型,默認為空。
可選值包括authorization_code,password,refresh_token,implicit,client_credentials
最常用的grant_type組合有: "authorization_code,refresh_token"(針對通過瀏覽器訪問的客戶端); "password,refresh_token"(針對移動設備的客戶端)
registeredRedirectUris:客戶端的重定向URI
autoApproveScopes:設置用戶是否自動Approval操作, 默認值為 false,
可選值包括 true,false, read,write.
該字段只適用於grant_type="authorization_code的情況,當用戶登錄成功后,
若該值為true或支持的scope值,則會跳過用戶Approve的頁面, 直接授權.
authorities:指定客戶端所擁有的Spring Security的權限值。
accessTokenValiditySeconds:設定客戶端的access_token的有效時間值(單位:秒),可選, 若不設定值則使用默認的有效時間值(60 * 60 * 12, 12小時).
refreshTokenValiditySeconds:設定客戶端的refresh_token的有效時間值(單位:秒),可選, 若不設定值則使用默認的有效時間值(60 * 60 * 24 * 30, 30天).
additionalInformation:這是一個預留的字段,在Oauth的流程中沒有實際的使用,可選,但若設置值,必須是JSON格式的數據

具體可參考:http://andaily.com/spring-oauth-server/db_table_description.html
ClientDetailsServiceConfiguration
ClientDetailsServiceConfiguration 依據配置,由ClientDetailsServiceBuilder創建ClientDetailsService
ClientDetailsServiceConfiguration核心源碼


@Configuration
public class ClientDetailsServiceConfiguration {
    private ClientDetailsServiceConfigurer configurer = 
              new ClientDetailsServiceConfigurer(new ClientDetailsServiceBuilder());

    @Bean
    @Lazy
    @Scope(
        proxyMode = ScopedProxyMode.INTERFACES
    )
    public ClientDetailsService clientDetailsService() throws Exception {
        return ((ClientDetailsServiceBuilder)this.configurer.and()).build();
    }
    ......
}
InMemoryClientDetailsServiceBuilder和JdbcClientDetailsServiceBuilder均繼承於ClientDetailsServiceBuilder,都會重寫performBuild(),因為ClientDetailsServiceBuilder的build()需要調用performBuild()

InMemoryClientDetailsServiceBuilder核心源碼


public class InMemoryClientDetailsServiceBuilder 
          extends ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder> {
    private Map<String, ClientDetails> clientDetails = new HashMap();

    protected ClientDetailsService performBuild() {
        InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService();
        clientDetailsService.setClientDetailsStore(this.clientDetails);
        return clientDetailsService;
    }
    ......
}
JdbcClientDetailsServiceBuilder核心源碼


public class JdbcClientDetailsServiceBuilder 
              extends ClientDetailsServiceBuilder<JdbcClientDetailsServiceBuilder> {
    private Set<ClientDetails> clientDetails = new HashSet();
    private DataSource dataSource;
    private PasswordEncoder passwordEncoder;

    protected ClientDetailsService performBuild() {
        Assert.state(this.dataSource != null, "You need to provide a DataSource");
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(this.dataSource);
        if (this.passwordEncoder != null) {
            clientDetailsService.setPasswordEncoder(this.passwordEncoder);
        }

        Iterator var2 = this.clientDetails.iterator();

        while(var2.hasNext()) {
            ClientDetails client = (ClientDetails)var2.next();
            clientDetailsService.addClientDetails(client);
        }

        return clientDetailsService;
    }
    ......
}
同理:創建出的ClientDetailsService也分為InMemoryClientDetailsService和JdbcClientDetailsService
InMemoryClientDetailsService核心源碼


public class InMemoryClientDetailsService implements ClientDetailsService {
    private Map<String, ClientDetails> clientDetailsStore = new HashMap();

    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        ClientDetails details = (ClientDetails)this.clientDetailsStore.get(clientId);
        if (details == null) {
            throw new NoSuchClientException("No client with requested id: " + clientId);
        } else {
            return details;
        }
    }
    ......
}
InMemoryClientDetailsService將ClientDetails存儲到Hashmap中

JdbcClientDetailsService核心源碼


public class JdbcClientDetailsService 
                    implements ClientDetailsService, ClientRegistrationService {
    private String updateClientDetailsSql;
    private String updateClientSecretSql;
    private String insertClientDetailsSql;
    private String selectClientDetailsSql;
    private PasswordEncoder passwordEncoder;
    private final JdbcTemplate jdbcTemplate;
    private JdbcListFactory listFactory;

    public JdbcClientDetailsService(DataSource dataSource) {
        this.updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT;
        this.updateClientSecretSql = "update oauth_client_details set client_secret = ? where client_id = ?";
        this.insertClientDetailsSql = "insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
        this.selectClientDetailsSql = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?";
        this.passwordEncoder = NoOpPasswordEncoder.getInstance();
        Assert.notNull(dataSource, "DataSource required");
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(this.jdbcTemplate));
    }

    public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
        try {
            ClientDetails details = (ClientDetails)this.jdbcTemplate.
                                    queryForObject(this.selectClientDetailsSql, 
                      new JdbcClientDetailsService.ClientDetailsRowMapper(), 
                      new Object[]{clientId});
            return details;
        } catch (EmptyResultDataAccessException var4) {
            throw new NoSuchClientException("No client with requested id: " + clientId);
        }
    }
}
JdbcClientDetailsService則是將ClientDetails存儲在數據庫中
通過使用jdbcTemplate對數據庫進行增改查

3.2.AuthorizationServerEndpointsConfigurer
用來配置授權authorization以及令牌token的訪問端點和令牌服務token services


@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
        tokenServices.setTokenStore(jwtTokenStore());
        tokenServices.setSupportRefreshToken(true);
        //獲取ClientDetailsService信息
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(jwtAccessTokenConverter());
        // 一天有效期
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1));
        endpoints.tokenServices(tokenServices);
    }
DefaultTokenService作為OAuth2中操作token(crud)的默認實現,在OAuth2框架中有着很重要的地位。使用隨機值創建令牌,並處理除永久令牌以外的所有令牌
在認證服務的 Endpoints 中, 使用的正是 DefaultTokenServices, 它為 DefaultTokenServices 提供了默認配置


public final class AuthorizationServerEndpointsConfigurer {
   private int refreshTokenValiditySeconds = 2592000;
   private int accessTokenValiditySeconds = 43200;
   private boolean supportRefreshToken = false;
   private boolean reuseRefreshToken = true;
   private TokenStore tokenStore;
   private ClientDetailsService clientDetailsService;
   private TokenEnhancer accessTokenEnhancer;
   private AuthenticationManager authenticationManager;

   private DefaultTokenServices createDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(this.tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(this.reuseRefreshToken);
        // 如果未配置, 則配置為 InMemoryClientDetailsService
        tokenServices.setClientDetailsService(this.clientDetailsService());
        tokenServices.setTokenEnhancer(this.tokenEnhancer());
        this.addUserDetailsService(tokenServices, this.userDetailsService);
        return tokenServices;
    }

    private TokenStore tokenStore() {
        // 如果未配置, 則創建
        if (this.tokenStore == null) {
            // 如果配置了 JwtAccessTokenConverter, 則創建 JwtTokenStore
            if (this.accessTokenConverter() instanceof JwtAccessTokenConverter) {
                this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter)this.accessTokenConverter());
            } else {
                // 否則, 創建 InMemoryTokenStore
                this.tokenStore = new InMemoryTokenStore();
            }
        }

        return this.tokenStore;
    }

    private TokenEnhancer tokenEnhancer() {
        // 如果未配置tokenEnhancer, 但配置了JwtAccessTokenConverter, 則將這個 convert 返回
        if (this.tokenEnhancer == null && this.accessTokenConverter() instanceof JwtAccessTokenConverter) {
            this.tokenEnhancer = (TokenEnhancer)this.accessTokenConverter;
        }

        return this.tokenEnhancer;
    }
    ......
}
核心屬性字段解析

屬性字段    作用
refreshTokenValiditySeconds    refresh_token 的有效時長 (秒), 默認 30 天
accessTokenValiditySeconds    access_token 的有效時長 (秒), 默認 12 小時
supportRefreshToken    是否支持 refresh token, 默認為 false
reuseRefreshToken    是否復用 refresh_token, 默認為 true (如果為 false, 每次請求刷新都會刪除舊的 refresh_token, 創建新的 refresh_token)
tokenStore    token 儲存器 (持久化容器)
clientDetailsService    提供 client 詳情的服務 (clientDetails 可持久化到數據庫中或直接放在內存里)
accessTokenEnhancer    token 增強器, 可以通過實現 TokenEnhancer 以存放 additional information
authenticationManager    Authentication 管理者, 起到填充完整 Authentication的作用
TokenStore令牌存儲器
OAuth2的永久令牌token管理主要交給TokenStore接口
TokenStore接口源碼如下


public interface TokenStore {
    OAuth2Authentication readAuthentication(OAuth2AccessToken var1);

    OAuth2Authentication readAuthentication(String var1);

    void storeAccessToken(OAuth2AccessToken var1, OAuth2Authentication var2);

    OAuth2AccessToken readAccessToken(String var1);

    void removeAccessToken(OAuth2AccessToken var1);

    void storeRefreshToken(OAuth2RefreshToken var1, OAuth2Authentication var2);

    OAuth2RefreshToken readRefreshToken(String var1);

    OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken var1);

    void removeRefreshToken(OAuth2RefreshToken var1);

    void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken var1);

    OAuth2AccessToken getAccessToken(OAuth2Authentication var1);

    Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String var1, String var2);

    Collection<OAuth2AccessToken> findTokensByClientId(String var1);
}
TokenStore管理OAuth2AccessToken 與OAuth2Authentication和OAuth2RefreshToken與OAuth2Authentication的對應關系的增刪改查

官方提供的TokenStore實現類如下:
InMemoryTokenStore:將OAuth2AccessToken保存在內存(默認)
JdbcTokenStore:將OAuth2AccessToken保存在數據庫
JwkTokenStore:將OAuth2AccessToken保存到JSON Web Key
JwtTokenStore:將OAuth2AccessToken保存到JSON Web Token
RedisTokenStore將OAuth2AccessToken保存到Redis
有需要也可以實現TokenStore接口進行自定義

JwtTokenStore JWT令牌存儲組件,供給認證服務器取來給授權服務器端點配置器
JwtAccessTokenConverter JWT訪問令牌轉換器(token生成器),按照設置的簽名來生成Token
注:JwtAccessTokenConverter實現了Token增強器TokenEnhancer接口和令牌轉換器AccessTokenConverter接口
JwtTokenStore類依賴JwtAccessTokenConverter類,授權服務器和資源服務器都需要接口的實現類(因此他們可以安全地使用相同的數據並進行解碼)

需要在AuthorizationServerEndpointsConfigurer 授權服務器端點配置中加入


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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("testKey");
        return converter;
    }
jwt具有自解釋的特性,客戶端不需要再去授權服務器認證這個token的合法性,這里使用對稱密鑰testKey來簽署我們的令牌,意味着需要為資源服務器使用同樣的確切密鑰。
注:也支持使用非對稱加密的方式,不過有點復雜

3.3.AuthorizationServerSecurityConfigurer:用來配置令牌(token)端點的安全約束。

@Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.tokenKeyAccess("isAuthenticated()");
    }
4.Spring Security安全配置

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("SSOUserDetailsService")
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        authenticationProvider.setHideUserNotFoundExceptions(false);
        return authenticationProvider;
    }

    @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) {
        auth.authenticationProvider(authenticationProvider());
    }
}
注入UserDetailsService時需要加上@Qualifier("SSOUserDetailsService"),否則會報Could not autowire. There are more than one bean of 'UserDetailsService' type.

5.認證中心yml配置

server:
  servlet:
    context-path: /pjb
不加server.servlet.context-path會一直處在認證頁面

客戶端配置
創建兩個客戶端應用:client1和client2
唯一的區別是client1的端口是8086,client2的端口是8087

1.依賴引入

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
2.SSO客戶端應用配置
配置最核心的部分是 @EnableOAuth2Sso注解來開啟SSO
@EnableWebSecurity注解讓Spring Security生效
@EnableGlobalMethodSecurity注解來判斷用戶對某個控制層的方法是否具有訪問權限


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated();
    }
}
3.客戶端控制層,@PreAuthorize進行權限攔截

@RestController
public class ClientController {

    @GetMapping("/normal")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public String normal( ) {
        return "用戶頁面";
    }

    @GetMapping("/medium")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public String medium() {
        return "這也是用戶頁面";
    }

    @GetMapping("/admin")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String admin() {
        return "管理員頁面";
    }
}
4.客戶端yml配置如下

server:
  port: 8086
security:
  oauth2:
    client:
      client-id: ben1
      client-secret: 123456
      user-authorization-uri: http://localhost:8080/pjb/oauth/authorize
      access-token-uri: http://localhost:8080/pjb/oauth/token
    resource:
      jwt:
        key-uri: http://localhost:8080/pjb/oauth/token_key
配置說明
security.oauth2.client.client-id:指定OAuth2 client ID.
security.oauth2.client.client-secret:指定OAuth2 client secret. 默認是一個隨機的密碼.
security.oauth2.client.user-authorization-uri:用戶跳轉去獲取access token的URI(授權端)
security.oauth2.client.access-token-uri:指定獲取access token的URI(令牌端)
security.oauth2.resource.jwt.key-uri:JWT token的URI
需要確保以上URL都是存在的,不然啟動會報錯

注:在客戶端配置文件中指定security.oauth2.client.registered-redirect-uri客戶端跳轉URI不生效,需要在認證中心中指定
重點:
/oauth/authorize:驗證
/oauth/token:獲取token
/oauth/confirm_access:用戶授權
/oauth/error:認證失敗
/oauth/check_token:資源服務器用來校驗token
/oauth/token_key:如果jwt模式則可以用此來從認證服務器獲取公鑰
以上這些endpoint都在源碼里的endpoint包里面。

OAuth2獲取token的主要流程:
1.用戶發起獲取token的請求。
2.過濾器會驗證path是否是認證的請求/oauth/token,如果為false,則直接返回沒有后續操作。
3.過濾器通過clientId查詢生成一個Authentication對象。
4.然后會通過username和生成的Authentication對象生成一個UserDetails對象,並檢查用戶是否存在。
5.以上全部通過會進入地址/oauth/token,即TokenEndpoint的postAccessToken方法中。
6.postAccessToken方法中會驗證Scope,然后驗證是否是refreshToken請求等。
7.之后調用AbstractTokenGranter中的grant方法。
8.grant方法中調用AbstractUserDetailsAuthenticationProvider的authenticate方法,通過username和Authentication對象來檢索用戶是否存在。
9.然后通過DefaultTokenServices類從tokenStore中獲取OAuth2AccessToken對象。
10.然后將OAuth2AccessToken對象包裝進響應流返回。

OAuth2刷新token的流程
刷新token(refresh token)的流程與獲取token的流程只有⑨有所區別:
獲取token調用的是AbstractTokenGranter中的getAccessToken方法,然后調用tokenStore中的getAccessToken方法獲取token。
刷新token調用的是RefreshTokenGranter中的getAccessToken方法,然后使用tokenStore中的refreshAccessToken方法獲取token。

啟動測試

 

先啟動認證中心,再啟動兩個客戶端

訪問客戶端http://localhost:8086/normal會跳轉到Spring Security的登錄認證頁,也就是認證中心登錄頁

 
image.png

在認證中心中,我設置了用戶名是user,密碼是123456,權限是ROLE_USER

注:在ClientDetailsServiceConfigurer中如果設置了autoApprovefalse
需要手動確認授權

 
image.png

 

在client1上URL中包含的信息
http://localhost:8080/pjb/oauth/authorize?client_id=ben1&redirect_uri=http://localhost:8086/login&response_type=code&state=4hBAab

點擊approve確定授權

 
image.png

想跳過這個認證確認的過程,設置autoApprove 為true(推薦)

接着訪問http://localhost:8087/normal,點擊approve授權后也可以訪問到

 
image.png

 

在client2上URL中包含的信息
http://localhost:8080/pjb/oauth/authorize?client_id=ben2&redirect_uri=http://localhost:8087/login&response_type=code&state=3EpENW

 
image.png

訪問http://localhost:8087/medium也是沒問題的,都是ROLE_USER權限

 
image.png

但是訪問http://localhost:8087/admin 就沒權限了

 
image.png
Github源碼地址:https://github.com/JinBinPeng/SpringBoot-SSO


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM