Spring Cloud實戰系列(九) - 服務認證授權Spring Cloud OAuth 2.0

weixin_33686714發表於2019-02-06

相關

  1. Spring Cloud實戰系列(一) - 服務註冊與發現Eureka

  2. Spring Cloud實戰系列(二) - 客戶端呼叫Rest + Ribbon

  3. Spring Cloud實戰系列(三) - 宣告式客戶端Feign

  4. Spring Cloud實戰系列(四) - 熔斷器Hystrix

  5. Spring Cloud實戰系列(五) - 服務閘道器Zuul

  6. Spring Cloud實戰系列(六) - 分散式配置中心Spring Cloud Config

  7. Spring Cloud實戰系列(七) - 服務鏈路追蹤Spring Cloud Sleuth

  8. Spring Cloud實戰系列(八) - 微服務監控Spring Boot Admin

  9. Spring Cloud實戰系列(九) - 服務認證授權Spring Cloud OAuth 2.0

  10. Spring Cloud實戰系列(十) - 單點登入JWT與Spring Security OAuth

前言

OAuth 2.0 是介於 使用者資源第三方應用 之間的一個 中間層,它把 資源第三方應用 隔開,使得 第三方應用 無法直接訪問 資源,從而起到 保護資源 的作用。為了訪問這種 受限資源第三方應用(客戶端)在訪問的時候需要 提供憑證

正文

1. OAuth 2.0簡介

認證授權 的過程中,主要包含以下 3 種角色:

  • 服務提供方: Authorization Server

  • 資源持有者: Resource Server

  • 客戶端: Client

OAuth 2.0認證流程 如圖所示,具體如下:

  1. 使用者資源持有者)開啟 客戶端客戶端 詢問 使用者授權

  2. 使用者 同意授權。

  3. 客戶端授權伺服器 申請授權。

  4. 授權伺服器客戶端 進行認證,也包括 使用者資訊 的認證,認證成功後授權給予 令牌

  5. 客戶端 獲取令牌後,攜帶令牌資源伺服器 請求資源。

  6. 資源伺服器 確認令牌正確無誤,向 客戶端 發放資源。

OAuth2 Provider 的角色被分為 Authorization Server授權服務)和 Resource Service資源服務),通常它們不在同一個服務中,可能一個 Authorization Service 對應 多個 Resource ServiceSpring OAuth2.0 需配合 Spring Security 一起使用,所有的請求由 Spring MVC 控制器處理,並經過一系列的 Spring Security 過濾器攔截。

Spring Security 過濾器鏈 中有以下兩個 端點,這兩個節點用於從 Authorization Service 獲取驗證授權

  • 用於 授權 的端點:預設為 /oauth/authorize

  • 用於獲取 令牌 的端點:預設為 /oauth/token

2. 新建本地資料庫

客戶端資訊 可以儲存在 資料庫 中,這樣就可以通過更改 資料庫 來實時 更新客戶端資訊 的資料。Spring OAuth2 已經設計好了資料庫的表,且不可變。首先將以下 DDL 匯入資料庫中。

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `clientdetails`
-- ----------------------------
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (
  `appId` varchar(128) NOT NULL,
  `resourceIds` varchar(256) DEFAULT NULL,
  `appSecret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `grantTypes` varchar(256) DEFAULT NULL,
  `redirectUrl` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additionalInformation` varchar(4096) DEFAULT NULL,
  `autoApproveScopes` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_access_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  `authentication` blob,
  `refresh_token` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


-- ----------------------------
--  Table structure for `oauth_approvals`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
  `userId` varchar(256) DEFAULT NULL,
  `clientId` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` datetime DEFAULT NULL,
  `lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_client_details`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(256) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_client_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_code`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code` varchar(256) DEFAULT NULL,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_refresh_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


-- ----------------------------
--  Table structure for `role`
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;


-- ----------------------------
--  Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_sb8bbouer5wak8vyiiy4pf2bx` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;


-- ----------------------------
--  Table structure for `user_role`
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  KEY `FKa68196081fvovjhkek5m97n3y` (`role_id`),
  KEY `FK859n2jvi8ivhui0rl0esws6o` (`user_id`),
  CONSTRAINT `FK859n2jvi8ivhui0rl0esws6o` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
  CONSTRAINT `FKa68196081fvovjhkek5m97n3y` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


SET FOREIGN_KEY_CHECKS = 1;
複製程式碼

3. 新建Maven專案

採用 Maven 的多 Module 的專案結構,新建一個 空白的 Maven 工程,並在 根目錄pom.xml 檔案中配置 Spring Boot 的版本 1.5.3.RELEASESpring Cloud 的版本為 Dalston.RELEASE,完整的程式碼如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/>
    </parent>

    <modules>
        <module>eureka-server</module>
        <module>service-auth</module>
        <module>service-hi</module>
    </modules>

    <groupId>io.github.ostenant.springcloud</groupId>
    <artifactId>spring-cloud-oauth2-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-oauth2-example</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
複製程式碼

4. 建立Eureka Server

4.1. 建立應用模組

新建一個 eureka-server 模組,並新增 Eureka 的相關依賴,並指定 pom.xml 的父節點如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.github.ostenant.springcloud</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>eureka-server</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>io.github.ostenant.springcloud</groupId>
        <artifactId>spring-cloud-oauth2-example</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
複製程式碼

4.2. 配置application.yml

eureka-server 模組的配置檔案 application.yml 中配置 Eureka Server 的資訊:

server:
  port: 8761
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
複製程式碼

4.3. 配置應用啟動類

最後在應用的 啟動類 上新增 @EnableEurekaServer 註解開啟 Eureka Server 的功能。

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
複製程式碼

5. 建立Uaa授權服務

5.1. 建立應用模組

新建一個 service-auth 模組,並新增以下依賴,作為 Uaa授權服務),完整的程式碼如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.github.ostenant.springcloud</groupId>
    <artifactId>service-auth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>service-auth</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>io.github.ostenant.springcloud</groupId>
        <artifactId>spring-cloud-oauth2-example</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
複製程式碼

開啟 spring-cloud-starter-oauth2 依賴包可以看到,它已經整合了以下 3起步依賴

  • spring-cloud-starter-security

  • spring-security-oauth2

  • spring-security-jwt

5.2. 配置application.yml

service-oauth 模組中的 application.yml 完成如下配置:

spring:
  application:
    name: service-auth
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

server:
  context-path: /uaa
  port: 5000

security:
  oauth2:
    resource:
      filter-order: 3
# basic:
#   enabled: false

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
複製程式碼

配置 security.oauth2.resource.filter-order3,在 Spring Boot 1.5.x 版本之前,可以省略此配置。

5.3. 配置安全認證

由於 auth-service 需要對外暴露檢查 TokenAPI 介面,所以 auth-service 其實也是一個 資源服務,需要在 auth-service 中引入 Spring Security,並完成相關配置,從而對 auth-service資源 進行保護。

WebSecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceDetail userServiceDetail;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.authorizeRequests().anyRequest().authenticated()
            .and()
            .csrf().disable();
        // @formatter:on
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    public @Bean AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
複製程式碼

UserServiceDetail.java

@Service
public class UserServiceDetail implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username);
    }
}
複製程式碼

配置表的關係對映類 User,需要實現 UserDetails 介面:

@Entity
public class User implements UserDetails, Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column
    private String password;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private List<Role> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    // setter getter

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
複製程式碼

配置表的關係對映類 Role,需要實現 GrantedAuthority 介面:

@Entity
public class Role implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    // setter getter

    @Override
    public String getAuthority() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}
複製程式碼

UserRepository.java

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}
複製程式碼

5.4. 配置Authentication Server

配置 認證伺服器,使用 @EnableAuthorizationServer 註解開啟 Authorization Server,對外提供 認證授權 的功能。

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    // 將Token儲存在記憶體中
    // private TokenStore tokenStore = new InMemoryTokenStore();
    private TokenStore tokenStore = new JdbcTokenStore(dataSource);

    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserServiceDetail userServiceDetail;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 將客戶端的資訊儲存在記憶體中
        clients.inMemory()
              // 建立了一個client名為browser的客戶端
              .withClient("browser")
              // 配置驗證型別
              .authorizedGrantTypes("refresh_token", "password")
              // 配置客戶端域為“ui”
              .scopes("ui")
              .and()
              .withClient("service-hi")
              .secret("123456")
              .authorizedGrantTypes("client_credentials", "refresh_token","password")
              .scopes("server");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 配置Token的儲存方式
        endpoints.tokenStore(tokenStore)
                // 注入WebSecurityConfig配置的bean
                .authenticationManager(authenticationManager)
                // 讀取使用者的驗證資訊
                .userDetailsService(userServiceDetail);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        // 對獲取Token的請求不再攔截
        oauthServer.tokenKeyAccess("permitAll()")
                 // 驗證獲取Token的驗證資訊
                .checkTokenAccess("isAuthenticated()");
    }
}
複製程式碼

5.5. 開啟Resource Server

在應用的啟動類上,使用 @EnableResourceServer 註解 開啟資源服務,應用需要對外暴露獲取 tokenAPI 介面。

@EnableEurekaClient
@EnableResourceServer
@SpringBootApplication
public class ServiceAuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceAuthApplication.class, args);
    }
}
複製程式碼

本例採用 RemoteTokenService 這種方式對 token 進行 驗證。如果 其他資源服務 需要驗證 token,則需要遠端呼叫 授權服務 暴露的 驗證 tokenAPI 介面。

@RestController
@RequestMapping("/users")
public class UserController {

    @RequestMapping(value = "/current", method = RequestMethod.GET)
    public Principal getUser(Principal principal) {
        return principal;
    }
}
複製程式碼

6. 編寫service-hi資源服務

6.1. 建立應用模組

新建一個 service-hi 模組,這個服務作為 資源服務。在 pom.xml 檔案引入如下依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.github.ostenant.springcloud</groupId>
    <artifactId>service-hi</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>service-hi</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>io.github.ostenant.springcloud</groupId>
        <artifactId>spring-cloud-oauth2-example</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
複製程式碼

6.2. 配置application.yml

application.yml 中配置 service-hiservice-auth 中配置的 OAuth Client 資訊:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8762
spring:
  application:
    name: service-hi
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

security:
  oauth2:
    resource:
      user-info-uri: http://localhost:5000/uaa/users/current #獲取當前Token的使用者資訊
    client:
      clientId: service-hi
      clientSecret: 123456
      accessTokenUri: http://localhost:5000/uaa/oauth/token #獲取Token
      grant-type: client_credentials,password
      scope: server
複製程式碼

6.3. 配置Resource Server

server-hi 模組作為 Resource Server資源服務),需要進行 Resource Server 的相關配置,配置程式碼如下:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
              // 對使用者註冊的URL地址開放
              .antMatchers("/user/registry").permitAll()
              .anyRequest().authenticated();
    }
}
複製程式碼

6.4. 配置OAuth2 Client

@Configuration
@EnableOAuth2Client
@EnableConfigurationProperties
public class OAuth2ClientConfig {

    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        // 配置受保護資源的資訊
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor(){
        // 配置一個攔截器,對於每一個外來的請求,都會在request域內建立一個AccessTokenRequest型別的bean。
        return new OAuth2FeignRequestInterceptor(
                        new DefaultOAuth2ClientContext(),
                        clientCredentialsResourceDetails());
    }

    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        // 用於向認證伺服器服務請求token
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }
}
複製程式碼

6.5. 建立使用者註冊介面

service-auth 模組的 User.javaUserRepository.java 拷貝到 service-hi 模組中。建立 UserService 用於 建立使用者,並對 使用者密碼 進行 加密

UserService.java

@Service
public class UserService {
   private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

   @Autowired
   private UserRepository userRepository;

   public User create(String username, String password) {
      User user = new User();
      user.setUsername(username);
      String hash = encoder.encode(password);
      user.setPassword(hash);
      return userRepository.save(user);
   }
}
複製程式碼

UserController.java

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping(value = "/registry", method = RequestMethod.POST)
    public User createUser(@RequestParam("username") String username,
                           @RequestParam("password") String password) {
        return userService.create(username,password);
    }
}
複製程式碼

6.6. 建立資源服務介面

@RestController
public class HiController {
    private static final Logger LOGGER = LoggerFactory.getLogger(HiController.class);

    @Value("${server.port}")
    private String port;

    /**
     * 不需要任何許可權,只要Header中的Token正確即可
     */
    @RequestMapping("/hi")
    public String hi() {
        return "hi : " + ",i am from port: " + port;
    }

    /**
     * 需要ROLE_ADMIN許可權
     */
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @RequestMapping("/hello")
    public String hello() {
        return "hello you!";
    }

    /**
     * 獲取當前認證使用者的資訊
     */
    @GetMapping("/getPrinciple")
    public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, 
                                             Principal principal,
                                             Authentication authentication){
        LOGGER.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
        LOGGER.info(oAuth2Authentication.toString());
        LOGGER.info("principal.toString()" + principal.toString());
        LOGGER.info("principal.getName()" + principal.getName());
        LOGGER.info("authentication:" + authentication.getAuthorities().toString());

        return oAuth2Authentication;
    }
}
複製程式碼

6.6. 配置應用的啟動類

@EnableEurekaClient
@SpringBootApplication
public class ServiceHiApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceHiApplication.class, args);
    }
}
複製程式碼

依次啟動 eureka-serviceservice-authservice-hi 三個服務。

7. 使用PostMan驗證

  • 註冊一個使用者,返回註冊成功資訊

  • 基於密碼模式從認證伺服器獲取 token

  • 訪問資源服務 /hi,不需要許可權,只要 token 正確即可

  • 訪問資源服務 /hello,提示需要 ROLE_ADMIN 許可權

  • 訪問不成功,修改資料庫的 role 表,新增 許可權資訊 ROLE_ADMIN,然後在 user_role 表關聯下再次訪問

總結

本案列架構有仍有改進之處。例如在 資源伺服器 加一個 登入介面,該介面不受 Spring Security 保護。登入成功後,service-hi 遠端呼叫 auth-service 獲取 token 返回給瀏覽器,瀏覽器以後所有的請求都需要攜帶該 token

這個架構的缺陷就是,每次請求 都需要由 資源服務 內部 遠端呼叫 service-auth 服務來 驗證 token 的正確性,以及該 token 對應的使用者所具有的 許可權,多了一次額外的 內部請求開銷。如果在 高併發 的情況下,service-auth 需要以 叢集 的方式部署,並且需要做 快取處理。所以最佳方案還是結合 Spring Security OAuth2JWT 一起使用,來實現 Spring Cloud 微服務系統的 認證授權

參考

  • 方誌朋《深入理解Spring Cloud與微服務構建》

歡迎關注技術公眾號:零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章