Spring Cloud实战系列(九) - 服务认证授权Spring Cloud OAuth 2.0

12,945 阅读7分钟

相关

  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与微服务构建》

欢迎关注技术公众号:零壹技术栈

零壹技术栈

本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。