SpringCloud OAuth2實(shí)現(xiàn)單點(diǎn)登錄以及OAuth2源碼原理解析

單點(diǎn)登錄(Single Sign On),簡稱為 SSO,是目前比較流行的企業(yè)業(yè)務(wù)整合的解決方案之一。SSO的定義是在多個應(yīng)用系統(tǒng)中,用戶只需要登錄一次就可以訪問所有相互信任的應(yīng)用系統(tǒng)。

Spring Security OAuth 是建立在 Spring Security 的基礎(chǔ)之上 OAuth2.0 協(xié)議實(shí)現(xiàn)的一個類庫

Spring Security OAuth2 為 Spring Cloud 搭建認(rèn)證授權(quán)服務(wù)(能夠更好的集成到 Spring Cloud 體系中)

單點(diǎn)登錄主要包括

服務(wù)端:一個第三方授權(quán)中心服務(wù)(Server),用于完成用戶登錄,認(rèn)證和權(quán)限處理
客戶端:當(dāng)用戶訪問客戶端應(yīng)用的安全頁面時,會重定向到授權(quán)中心進(jìn)行身份驗(yàn)證,認(rèn)證完成后方可訪問客戶端應(yīng)用的服務(wù),且多個客戶端應(yīng)用只需要登錄一次即可

相關(guān)版本:

SpringBoot:2.1.5.RELEASE
SpringCloud :Greenwich.SR1

認(rèn)證中心Server

1.引入OAuth2依賴和web依賴(不加啟動時會報無法訪問javax.servlet.Filter)

OAuth2中包含spring-cloud-starter-securityspring-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.創(chuàng)建驗(yàn)證用戶,設(shè)置用戶名和密碼并設(shè)置角色權(quán)限

@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.認(rèn)證服務(wù)器配置

①加入@EnableAuthorizationServer注解來啟動OAuth2.0授權(quán)服務(wù)機(jī)制
②通過繼承AuthorizationServerConfigurerAdapter并且覆寫其中的三個configure方法來進(jìn)行配置
3.1.ClientDetailsServiceConfigurer

用于定義客戶詳細(xì)信息服務(wù)的配置器。客戶端詳情信息進(jìn)行初始化,能夠把客戶端詳情信息寫在內(nèi)存中或者是通過數(shù)據(jù)庫來存儲調(diào)取詳情信息。

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

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 定義了兩個客戶端應(yīng)用的通行證
        clients.inMemory()// 使用in-memory存儲
                .withClient("ben1")// client_id
                .secret(new BCryptPasswordEncoder().encode("123456"))// client_secret
                .authorizedGrantTypes("authorization_code", "refresh_token")// 該client允許的授權(quán)類型
                .scopes("all")// 允許的授權(quán)范圍
                .autoApprove(false)
                //加上驗(yàn)證回調(diào)地址
                .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");
    }

必須設(shè)置回調(diào)地址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根據(jù)ClientDetailsServiceConfigurer配置,交給ClientDetailsServiceBuilder的實(shí)現(xiàn)類通過ClientBuilder創(chuàng)建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

ClientBuilderClientDetailsServiceBuilder的一個內(nèi)部類,其中build()會被ClientDetailsServiceConfiguration所調(diào)用

ClientDetailsServiceBuilder部分源碼

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

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

    //創(chuàng)建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:第三方應(yīng)用和授權(quán)服務(wù)器之間的安全憑證(可理解為密碼)
scope:指定客戶端申請的權(quán)限范圍,可選值包括read,write,trust;其實(shí)授權(quán)賦予第三方用戶可以在資源服務(wù)器獲取資源,第三方訪問資源的一個權(quán)限,訪問范圍。
resourceIds:客戶端所能訪問的資源id集合
authorizedGrantTypes:此客戶端可以使用的授權(quán)類型,默認(rèn)為空。
可選值包括authorization_code,password,refresh_token,implicit,client_credentials
最常用的grant_type組合有: "authorization_code,refresh_token"(針對通過瀏覽器訪問的客戶端); "password,refresh_token"(針對移動設(shè)備的客戶端)
registeredRedirectUris:客戶端的重定向URI
autoApproveScopes:設(shè)置用戶是否自動Approval操作, 默認(rèn)值為 false,
可選值包括 true,false, read,write.
該字段只適用于grant_type="authorization_code的情況,當(dāng)用戶登錄成功后,
若該值為true或支持的scope值,則會跳過用戶Approve的頁面, 直接授權(quán).
authorities:指定客戶端所擁有的Spring Security的權(quán)限值。
accessTokenValiditySeconds:設(shè)定客戶端的access_token的有效時間值(單位:秒),可選, 若不設(shè)定值則使用默認(rèn)的有效時間值(60 * 60 * 12, 12小時).
refreshTokenValiditySeconds:設(shè)定客戶端的refresh_token的有效時間值(單位:秒),可選, 若不設(shè)定值則使用默認(rèn)的有效時間值(60 * 60 * 24 * 30, 30天).
additionalInformation:這是一個預(yù)留的字段,在Oauth的流程中沒有實(shí)際的使用,可選,但若設(shè)置值,必須是JSON格式的數(shù)據(jù)

具體可參考:http://andaily.com/spring-oauth-server/db_table_description.html

ClientDetailsServiceConfiguration

ClientDetailsServiceConfiguration 依據(jù)配置,由ClientDetailsServiceBuilder創(chuàng)建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();
    }
    ......
}

InMemoryClientDetailsServiceBuilderJdbcClientDetailsServiceBuilder均繼承于ClientDetailsServiceBuilder,都會重寫performBuild(),因?yàn)?code>ClientDetailsServiceBuilder的build()需要調(diào)用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;
    }
    ......
}

同理:創(chuàng)建出的ClientDetailsService也分為InMemoryClientDetailsServiceJdbcClientDetailsService
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;
        }
    }
    ......
}

InMemoryClientDetailsServiceClientDetails存儲到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存儲在數(shù)據(jù)庫中
通過使用jdbcTemplate對數(shù)據(jù)庫進(jìn)行增改查

3.2.AuthorizationServerEndpointsConfigurer

用來配置授權(quán)authorization以及令牌token的訪問端點(diǎn)和令牌服務(wù)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)的默認(rèn)實(shí)現(xiàn),在OAuth2框架中有著很重要的地位。使用隨機(jī)值創(chuàng)建令牌,并處理除永久令牌以外的所有令牌
在認(rèn)證服務(wù)的 Endpoints 中, 使用的正是 DefaultTokenServices, 它為 DefaultTokenServices 提供了默認(rèn)配置

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() {
        // 如果未配置, 則創(chuàng)建
        if (this.tokenStore == null) {
            // 如果配置了 JwtAccessTokenConverter, 則創(chuàng)建 JwtTokenStore
            if (this.accessTokenConverter() instanceof JwtAccessTokenConverter) {
                this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter)this.accessTokenConverter());
            } else {
                // 否則, 創(chuàng)建 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 的有效時長 (秒), 默認(rèn) 30 天
accessTokenValiditySeconds access_token 的有效時長 (秒), 默認(rèn) 12 小時
supportRefreshToken 是否支持 refresh token, 默認(rèn)為 false
reuseRefreshToken 是否復(fù)用 refresh_token, 默認(rèn)為 true (如果為 false, 每次請求刷新都會刪除舊的 refresh_token, 創(chuàng)建新的 refresh_token)
tokenStore token 儲存器 (持久化容器)
clientDetailsService 提供 client 詳情的服務(wù) (clientDetails 可持久化到數(shù)據(jù)庫中或直接放在內(nèi)存里)
accessTokenEnhancer token 增強(qiáng)器, 可以通過實(shí)現(xiàn) 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管理OAuth2AccessTokenOAuth2AuthenticationOAuth2RefreshTokenOAuth2Authentication的對應(yīng)關(guān)系的增刪改查

官方提供的TokenStore實(shí)現(xiàn)類如下:

InMemoryTokenStore:將OAuth2AccessToken保存在內(nèi)存(默認(rèn))
JdbcTokenStore:將OAuth2AccessToken保存在數(shù)據(jù)庫
JwkTokenStore:將OAuth2AccessToken保存到JSON Web Key
JwtTokenStore:將OAuth2AccessToken保存到JSON Web Token
RedisTokenStore將OAuth2AccessToken保存到Redis

有需要也可以實(shí)現(xiàn)TokenStore接口進(jìn)行自定義

JwtTokenStore JWT令牌存儲組件,供給認(rèn)證服務(wù)器取來給授權(quán)服務(wù)器端點(diǎn)配置器
JwtAccessTokenConverter JWT訪問令牌轉(zhuǎn)換器(token生成器),按照設(shè)置的簽名來生成Token

注:JwtAccessTokenConverter實(shí)現(xiàn)了Token增強(qiáng)器TokenEnhancer接口和令牌轉(zhuǎn)換器AccessTokenConverter接口
JwtTokenStore類依賴JwtAccessTokenConverter類,授權(quán)服務(wù)器和資源服務(wù)器都需要接口的實(shí)現(xiàn)類(因此他們可以安全地使用相同的數(shù)據(jù)并進(jìn)行解碼)

需要在AuthorizationServerEndpointsConfigurer 授權(quán)服務(wù)器端點(diǎn)配置中加入

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

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

jwt具有自解釋的特性,客戶端不需要再去授權(quán)服務(wù)器認(rèn)證這個token的合法性,這里使用對稱密鑰testKey來簽署我們的令牌,意味著需要為資源服務(wù)器使用同樣的確切密鑰。
注:也支持使用非對稱加密的方式,不過有點(diǎn)復(fù)雜

3.3.AuthorizationServerSecurityConfigurer:用來配置令牌(token)端點(diǎn)的安全約束。
@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.認(rèn)證中心yml配置

server:
  servlet:
    context-path: /pjb

不加server.servlet.context-path會一直處在認(rèn)證頁面

客戶端配置

創(chuàng)建兩個客戶端應(yīng)用:client1和client2
唯一的區(qū)別是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客戶端應(yīng)用配置

配置最核心的部分是 @EnableOAuth2Sso注解來開啟SSO
@EnableWebSecurity注解讓Spring Security生效
@EnableGlobalMethodSecurity注解來判斷用戶對某個控制層的方法是否具有訪問權(quán)限

@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進(jìn)行權(quán)限攔截

@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. 默認(rèn)是一個隨機(jī)的密碼.
security.oauth2.client.user-authorization-uri:用戶跳轉(zhuǎn)去獲取access token的URI(授權(quán)端)
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客戶端跳轉(zhuǎn)URI不生效,需要在認(rèn)證中心中指定

重點(diǎn):

/oauth/authorize:驗(yàn)證
/oauth/token:獲取token
/oauth/confirm_access:用戶授權(quán)
/oauth/error:認(rèn)證失敗
/oauth/check_token:資源服務(wù)器用來校驗(yàn)token
/oauth/token_key:如果jwt模式則可以用此來從認(rèn)證服務(wù)器獲取公鑰
以上這些endpoint都在源碼里的endpoint包里面。

OAuth2獲取token的主要流程:

1.用戶發(fā)起獲取token的請求。
2.過濾器會驗(yàn)證path是否是認(rèn)證的請求/oauth/token,如果為false,則直接返回沒有后續(xù)操作。
3.過濾器通過clientId查詢生成一個Authentication對象。
4.然后會通過username和生成的Authentication對象生成一個UserDetails對象,并檢查用戶是否存在。
5.以上全部通過會進(jìn)入地址/oauth/token,即TokenEndpointpostAccessToken方法中。
6.postAccessToken方法中會驗(yàn)證Scope,然后驗(yàn)證是否是refreshToken請求等。
7.之后調(diào)用AbstractTokenGranter中的grant方法。
8.grant方法中調(diào)用AbstractUserDetailsAuthenticationProviderauthenticate方法,通過usernameAuthentication對象來檢索用戶是否存在。
9.然后通過DefaultTokenServices類從tokenStore中獲取OAuth2AccessToken對象。
10.然后將OAuth2AccessToken對象包裝進(jìn)響應(yīng)流返回。

OAuth2刷新token的流程

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

啟動測試

先啟動認(rèn)證中心,再啟動兩個客戶端

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

image.png

在認(rèn)證中心中,我設(shè)置了用戶名是user,密碼是123456,權(quán)限是ROLE_USER

注:在ClientDetailsServiceConfigurer中如果設(shè)置了autoApprovefalse
需要手動確認(rèn)授權(quán)

image.png

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

點(diǎn)擊approve確定授權(quán)

image.png

想跳過這個認(rèn)證確認(rèn)的過程,設(shè)置autoApprovetrue(推薦)

接著訪問http://localhost:8087/normal,點(diǎn)擊approve授權(quán)后也可以訪問到

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權(quán)限

image.png

但是訪問http://localhost:8087/admin 就沒權(quán)限了

image.png
Github源碼地址:https://github.com/JinBinPeng/SpringBoot-SSO
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容