Spring Cloud 微服务安全 | (二) 网关安全

Spring Cloud 微服务安全 | (二) 网关安全_第1张图片

前篇:Spring Cloud 微服务安全 | (一) API 安全

本篇源代码:https://github.com/hedon954/spring-security-oauth2.0

其他参考代码:OAuth2.0 内存版;OAuth2.0 数据库版


1. 微服务安全面临的挑战

Spring Cloud 微服务安全 | (二) 网关安全_第2张图片

  • 跨多个微服务的请求难以追踪
  • 容器化部署导致的证书和访问控制问题
  • 如何在微服务间共享用户登录状态
  • 多语言架构要求每个团队都有一定的安全经验
  • 性能问题

2. OAuth2.0 协议与微服务安全

Spring Cloud 微服务安全 | (二) 网关安全_第3张图片


3. 一个案例实现微服务网关安全

Spring Cloud 微服务安全 | (二) 网关安全_第4张图片


3.1 基本框架搭建

3.1.1 父工程搭建

这里只需要搭建一个空的父工程就可以了,后面在这个工程目录下创建新的 Module 即可。

image-20201012082630583

3.1.2 搭建价格服务 price-service

  • 创建工程

在父工程目录下新建一个 Module,Maven 的基础项目即可。

  • 导入 POM 坐标
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-dependenciesartifactId>
            <version>2.3.4.RELEASEversion>
            <type>pomtype>
            <scope>importscope>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-dependenciesartifactId>
            <version>Hoxton.SR3version>
            <type>pomtype>
            <scope>importscope>
        dependency>
    dependencies>
dependencyManagement>


<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
    dependency>
dependencies>
  • 主启动类
@SpringBootApplication
public class PriceServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(PriceServiceApplication.class,args);
    }
}
  • 配置端口
server:
  port: 9080
  • 编写价格实体类 Price
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Price {
    private Integer orderId;
    private Double price;
}
  • 编写价格对应的传输对象 PriceDto
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PriceDto {
    private Integer orderId;
    private Double price;
}
  • 编写价格控制器 PriceController
@RestController
@RequestMapping("/price")
public class PriceController {

    //传一个 Order Id,传回相应的价格
    @GetMapping("/{id}")
    public PriceDto getByOrderId(@PathVariable("id") Integer id){
        PriceDto priceDto = new PriceDto();
        priceDto.setOrderId(id);
        priceDto.setPrice(100.11);
        return priceDto;
    }
}

3.1.3 搭建订单服务 order-service

  • 创建工程

image-20201012082817244

Spring Cloud 微服务安全 | (二) 网关安全_第5张图片

  • 导入 POM 坐标
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-dependenciesartifactId>
            <version>2.3.4.RELEASEversion>
            <type>pomtype>
            <scope>importscope>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-dependenciesartifactId>
            <version>Hoxton.SR3version>
            <type>pomtype>
            <scope>importscope>
        dependency>
    dependencies>
dependencyManagement>


<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
    dependency>
dependencies>
  • 启动类
@SpringBootApplication
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class,args);
    }
}
  • 配置端口
server:
  port: 9090
  • 创建订单实体类 Order
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Order {
    private Integer id;
}
  • 窗口订单的数据传输对象 OrderDto
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class OrderDto {
    private Integer id;
}
  • 将价格服务的 Price 和 PriceDto 分贝拷贝到订单服务的 bean 包和 dto 包下面

  • 创建控制器 OrderController

这里先不连接数据库。

@RestController
@RequestMapping("/order")
public class OrderController {

    private RestTemplate restTemplate = new RestTemplate();

    @PostMapping("/create")
    public OrderDto create(@RequestBody OrderDto orderDto){
      	//去访问价格服务,获取订单的价格
        ResponseEntity<PriceDto> entity = restTemplate.getForEntity("http://localhost:9080/price/" + orderDto.getId(), PriceDto.class);
        PriceDto priceDto = entity.getBody();
        System.out.println(priceDto);
        return orderDto;
    }

}
  • 测试跨服务调用是否成功

Spring Cloud 微服务安全 | (二) 网关安全_第6张图片

image-20201012085344716

3.1.4 搭建认证服务器 auth-service

  • 创建工程

如上。

  • 导入 POM 依赖坐标
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-dependenciesartifactId>
                <version>2.3.4.RELEASEversion>
            dependency>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>Hoxton.SR3version>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <version>2.3.4.RELEASEversion>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
            <version>2.3.1.RELEASEversion>
        dependency>
    
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-oauth2artifactId>
            <version>2.2.1.RELEASEversion>
        dependency>
    dependencies>
  • 主启动类
@SpringBootApplication
public class AuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class,args);
    }
}
  • 配置端口
server:
  port: 9070

基本的框架搭建就先到这里,我们这里还没有引入 Zuul 网关。这里我们先打通整个 OAuth2.0 的认证和授权,后面再来引入网关,进一步实现网关安全。


3.2 认证服务器

3.2.1 授权服务器配置类

@Configuration
@EnableAuthorizationServer  //认证服务器
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    //配置客户端应用的相关信息
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        super.configure(clients);
    }

    //配置令牌管理器和令牌的存储方式
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
    }

    //配置谁能来验 token (有一些请求连验 token 的资格都没有)
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
    }
}

这里我们需要创建一个配置类(@Configuration)去继承 AuthorizationServerConfigurerAdapter 作为认证服务器(@EnableAuthorizationServer)。继承 AuthorizationServerConfigurerAdapter 是为了要实现它的 3 个方法,3 个方法的名称都为 configure,但是它们的传参是不同的。

  • configure(ClientDetailsServiceConfigurer clients):配置客户端应用的相关信息

    所谓客户端,不是指用户,而是指客户端应用,比如我们的 postman 或者一个 app。

  • configure(AuthorizationServerEndpointsConfigurer endpoints):配置令牌管理器

    这里我们到时候会配置一个 authenticationManager 来管理令牌(token)的访问端点(哪些用户可以来访问)和令牌服务(token services),还可以管理令牌的存储方式(内存方式、Jdbc 方式、自定义 ClientDetailsService 方式)。

  • configure(AuthorizationServerSecurityConfigurer security):配置令牌端点的安全约束

    这里我们会配置哪些请求可以来验 token,哪些请求需要来验 token,哪些请求不可以来验 token(直接 ko 掉),哪些请求不需要来验 token(直接放过)。

3.2.1.1 配置客户端应用的相关信息

这里我们先配置到内存里面,后面会改进为数据库版本。

@Autowired
private PasswordEncoder passwordEncoder;

//配置客户端应用的相关信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            .withClient("orderService")                      //客户端ID
            .secret(passwordEncoder.encode("123456"))        //客户端密码
            .scopes("read","write")                          //用户对该客户端可以申请哪些权限
            .accessTokenValiditySeconds(3600)                //令牌的有效期(单位为秒)
            .resourceIds("order-service")                    //资源ID
            .authorizedGrantTypes("password")                //用户要访问该客户端的时候采取的授权类型
            .and()
            .withClient("priceService")
            .secret(passwordEncoder.encode("123456"))
            .scopes("read")
            .accessTokenValiditySeconds(3600)
            .resourceIds("price-service")
            .authorizedGrantTypes("password");
}
  • withClient & secret:客户端ID和客户端密码

    这里就类似于一个 username 和 password,不过它不是指用户的信息,而且一个作为客户端的应用的信息,后面我们使用 postman(也就是客户端应用)要来认证服务器获取 token 的时候,就需要携带自己的客户端 ID 和 secret 过来,让认证服务器判断它是否是认证服务器的一个客户端,然后再决定是否要发放 token。

    • 场景一:手动用 postman 去获取 token

      Spring Cloud 微服务安全 | (二) 网关安全_第7张图片

    • 场景二:资源服务器绑定客户端,在客户端要访问资源的时候,自动去认证服务器验证用户在客户端携带的 token

      Spring Cloud 微服务安全 | (二) 网关安全_第8张图片

  • scopes:权限

    这里 scopes 是规定一个用户可以拿到的关于该资源服务器的所有的权限,这里 orderService 规定了可以有 write 和 read 权限,所以这里你可以申请 write 和 read 权限。但是你这里如果想要申请 fly 权限,那就会报错啦!

    • 场景一:申请 read 权限
    • 场景二:申请 read 和 write 权限
    • 场景三:申请 fly 权限
  • accessTokenValiditySeconds:令牌的有效期,单位是秒

  • resourceIds:资源ID

    这里是配置当前这个客户端它允许访问哪些资源服务器。我们资源服务器在做配置的时候是需要配置自己的资源服务器 ID 的,它需要与认证服务器配置的某个客户端中的资源ID一一对应。

    Spring Cloud 微服务安全 | (二) 网关安全_第9张图片

  • authorizedGrantTypes:客户端的授权类型,OAuth2.0 总共有 4 种授权类型,后面再做详细介绍:

    • 授权码类型
    • 简化类型
    • 密码类型
    • 客户端类型

注意:这里的 passwordEncoder 我们来没配置。

3.2.1.2 配置令牌管理器
@Autowired
private AuthenticationManager authenticationManager;

//配置令牌管理器 => ① 管理访问端点;② 管理令牌服务(哪种类型的令牌、令牌中的用户信息、密码加密模式)
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    //这部分由 authenticationManager 来管理
    endpoints.authenticationManager(authenticationManager);
}

这里的 authenticationManager 我们后面再配,这里写上。

3.2.1.3 配置令牌端点的安全约束
//配置令牌端点的安全约束 => 配置谁能来验 token (有一些请求连验 token 的资格都没有)
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    //这里设置必须带身份信息来验证token才进行验证
    security.checkTokenAccess("isAuthenticated()");
}

这里还可以设置(本案例没有设置):

security
        .tokenKeyAccess("permitAll()")          //公有密钥的端点 /oauth/token_key 是公开的
        .checkTokenAccess("permitAll()")        //资源服务可以远程校验令牌的合法性 /oauth/check_token 是公开的
        .allowFormAuthenticationForClients();   //表单认证(申请令牌)

3.2.2 网络安全相关配置类

@Configuration
@EnableWebSecurity  //支持 WebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
3.2.2.1 配置如何构建 authenticationManager

我们前面还留着 authenticationManagerpasswordEncoer没有配置,我们可以在这个类进行配置。

在配置之前,我们可以先来看看 AuthenticationManager 这个接口有什么东西:

image-20201012145408427

它里面只有一个 authenticate() 方法,也就是负责认证的方法,返回值类型是 Authentication,我们来看看这个类:

image-20201012145602107

现在我们来配置 authenticationManage

@Autowired
private UserDetailsService userDetailsService;

//采用 BCrypt 加密  (注:如果这里注入 passwordEncoder 会报错,可以把这个注入放在主启动类里面,然后这里用 @Autowired 注入)
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}

//配置如何构建 AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
            .userDetailsService(userDetailsService)   //用户详细信息(用户名、权限等)
            .passwordEncoder(passwordEncoder());      //密码加密器
}

authenticationManager 中,这里我们就先配置两个东西 userDetailsServicepasswordEncoderuserDetailsService 我们待会再来配置,这里先写上。

3.2.2.2 构建 authenticationManager

前面只是配置了如何构建 authenticationManager,但是 authenticationManager 来没有注入到我们的 Spring IoC 容器当中,想要将其注入,这里需要重写 WebSecurityConfigurerAdapter 中的 authenticationManagerBean() 方法然后加上 @Bean 注解,从名字上看这个方法的作用就一目了然了。

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

这样就会将 authenticationManager 对象注入到 Spring IoC 容器当中了。

3.2.3 用户信息配置类

现在我们来实现前面还没实现的 userDetailsService。在这之前,我们先来看看 userDetailsService 有什么东西:

Spring Cloud 微服务安全 | (二) 网关安全_第10张图片

我们可以看到它只有一个方法 loadUserByUsername(),它的返回值是一个 UserDetails 类型的,我们来看看 UserDetails 类有什么属性:

Spring Cloud 微服务安全 | (二) 网关安全_第11张图片

所以我们只需编写一个类来实现 userDetailsService 然后实现它的 loadUserByUsername() 方法就可以了,真正的场景肯定是需要访问数据库然后进行比对判断的,这里我们先简单配置,后面再转为数据库版本:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //这里表示 username 任意,密码只要是 123456 就可以了。
        return User.withUsername(username)
                .password(passwordEncoder.encode("123456"))
                .authorities("ROLE_ADMIN")  //我们这里设置一个全新叫"ROLE_ADMIN"
                .build();
    }
}

3.2.4 测试认证服务器

到此为止,认证服务器的简单配置我们就完成了,来测试一下。

  • 先启动认证服务器、订单服务、价格服务
3.2.4.1 正确测试

我们先来进行正常情况的测试。OAuth2.0 获取 token 的路径是 /oauth/token,在访问的时候需要携带两个信息:

  • 客户端信息

    Spring Cloud 微服务安全 | (二) 网关安全_第12张图片

  • 用户信息

    Spring Cloud 微服务安全 | (二) 网关安全_第13张图片

携带上面两个信息后我们就可以获取 token 了:

Spring Cloud 微服务安全 | (二) 网关安全_第14张图片
3.2.4.2 客户端ID 没有注册到认证服务器

我们改一下客户端ID,乱写一个:

Spring Cloud 微服务安全 | (二) 网关安全_第15张图片

3.2.4.3 客户端密码错误

Spring Cloud 微服务安全 | (二) 网关安全_第16张图片

3.2.4.4 用户密码错误

Spring Cloud 微服务安全 | (二) 网关安全_第17张图片

3.2.4.5 用户申请的权限资源服务器不提供

Spring Cloud 微服务安全 | (二) 网关安全_第18张图片

自此,认证服务器的测试就完成啦~下面我们来搭建资源服务器 Order-Service。


3.3 资源服务器

先给 Order-Service 的 pom 文件导入 OAuth2.0 相关的依赖坐标:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-oauth2artifactId>
    <version>2.2.1.RELEASEversion>
dependency>

3.3.1 资源服务器配置类

@Configuration
@EnableResourceServer //资源服务器
public class OAuth2ResourcesServerConfig extends ResourceServerConfigurerAdapter {

    //配置资源服务器 ID
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources);
    }

    //控制权限 => 默认所有服务都需要携带token
    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

在资源服务器配置类中,我们需要继承ResourceServerConfigurerAdapter 类,然后实现它的两个 configure() 方法。分别是:

  • configure(ResourceServerSecurityConfigurer resources):配置资源服务器 ID

    这里配置的资源服务器 ID 就是我们之前在认证服务器中注册了的资源服务器的 ID 中的某一个。

  • configure(HttpSecurity http):控制请求的访问权限

    这里我们可以配置哪些请求不需要进行认证,哪些请求需要进行认证,哪些请求需要什么样的权限才可以访问,还可以进行跨域安全相关的配置,还可以配置支持表单登录等配置。

3.3.1.1 配置资源服务器 ID
//配置资源服务器 ID
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.resourceId("order-service");
}

Spring Cloud 微服务安全 | (二) 网关安全_第19张图片

3.3.1.2 配置请求权限控制
//控制权限 => 默认所有服务都需要携带token
@Override
public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/haha/haha").permitAll() // "/haha/haha" 不需要验令牌,直接放行
            .anyRequest().authenticated();  //其他请求都必须验证令牌
}

这里我们随便写一个 /haha/haha 接口:

@RestController
@RequestMapping("/haha")
public class HahaController {

    @GetMapping("/haha")
    public String haha(){
        return "哈哈!我是 /haha/haha 接口,我不需要认证就可以访问啦!";
    }
}

3.3.2 网络安全相关配置类

@Configuration
@EnableWebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

我们前面配置了哪些请求需要验证令牌,那么问题就来了,去哪里验证令牌呢?这就是这个配置类要解决的问题。

在这个配置类中,我们需要配置认证服务器,告诉资源服务器应该去哪里验证令牌(这里其实就是 OAuth2.0 四种认证授权类型中的 —— 客户端类型)。

3.3.2.1 配置令牌验证服务
//配置令牌验证服务
@Bean
public ResourceServerTokenServices tokenServices(){
    RemoteTokenServices remote = new RemoteTokenServices();
    remote.setClientId("orderService");                 //客户端ID
    remote.setClientSecret("123456");                   //客户端secret
    remote.setCheckTokenEndpointUrl("http://localhost:9070/oauth/check_token");//配置去哪里验证 token
    return remote;
}

这部分的配置只是配置了客户端的相关信息,我们还需要配置用户相关的信息,由前面的配置我们可以知道,要来验证用户的相关信息的话那就需要一个 AuthenticationManager 了,但是这个时候我们就不能使用默认的 AuthenticationManager 了,我们需要自己写一个,然后设置上前面的 tokenSevices,让它去找认证服务器认证。

3.3.2.2 配置 AuthenticationManager
//利用上面的服务去验证 token
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    OAuth2AuthenticationManager manager = new OAuth2AuthenticationManager();
    manager.setTokenServices(tokenServices());
    return manager;
}

现在我们还需要获取用户的相关信息,那就得想到 UserDetailsService 了,所以这里我们还是需要实现这个接口:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}

这里我们就需要重写 loadUserByUsername() 方法,它的返回值类型是 UserDetails,我们现在来写一个User 类来实现 UserDetails,然后实现里面的方法,这里我们数据都先写死,都不需要访问数据库:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User implements UserDetails {
    
    //我们自己加3个属性
    private Integer id;
    private String username;
    private String password;

    //拥有哪些权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN");
    }

    //密码
    @Override
    public String getPassword() {
        return password;
    }

    //用户名
    @Override
    public String getUsername() {
        return username;
    }

    //是否过期 -> 没有过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //是否被锁 -> 没有被锁
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //证书是否过期 -> 没有过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //用户是否可用 -> 可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

写完 User 类后我们回到 loadUserByUsername() 这个方法上,这里为了简单,我们直接返回一个死的 User 对象就可以了:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = new User();
        user.setId(1);
        user.setUsername(username);
        return user;
    }
}

这样用户信息相关类 UserDetailsServiceImpl 就写好了。我们回到 OrderController 类

现在我们希望用户访问的接口也能知道令牌里面信息到底是哪个用户相关的信息,比如:

  • ① 只想获取用户名 username

    @PostMapping("/create")
    public OrderDto create(@RequestBody OrderDto orderDto, @AuthenticationPrincipal String username){
        System.out.println("user is " + username);
    		……………………
        return orderDto;
    }
    

    那么这里就不需要对 OAuth2WebSecurityConfig类再做任何配置了,默认就可以获得了。

  • ② 希望获取整个用户 User 的所有信息

    @PostMapping("/create")
    public OrderDto create(@RequestBody OrderDto orderDto, @AuthenticationPrincipal User user){
        System.out.println("user is " + user);
    		…………………………
        return orderDto;
    }
    

    那么这里就可以配置一个令牌转换器了,它可以将令牌中的用户信息解析出来并进行封装。

  • ③ 获取用户 User 的某一个属性

    @PostMapping("/create")
    public OrderDto create(@RequestBody OrderDto orderDto, @AuthenticationPrincipal(expression = "#this.id") Integer id){
        System.out.println("userId is " + id);
    		……………………
        return orderDto;
    }
    

    这个的话就需要在 ② 的基础上用 expression 表达式 来获取了,比如这里只要获取 id。

3.3.2.3 配置令牌解析器

来到 OAuth2WebSecurityConfig类 加上:

@Bean
public AccessTokenConverter accessTokenConverter(){
    DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
    tokenConverter.setUserTokenConverter();
}

写到这里,它需要设置一个 UserTokenConverter,所以我们需要主动在这个方法里面实例化一个 UserTokenConverter 实现类,为什么需要它呢?因为我们想要拿到用户信息,但是我们到目前为止还没有看到 UserDetailsService 所以这个 UserTokenConverter 就是用来放 UserDetailsService 的:

@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;

@Bean
public AccessTokenConverter accessTokenConverter(){
    DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
    DefaultUserAuthenticationConverter authenticationConverter = new DefaultUserAuthenticationConverter();
    authenticationConverter.setUserDetailsService(userDetailsService);
    tokenConverter.setUserTokenConverter(authenticationConverter);
    return tokenConverter;
}

配置为后,我们将其设置到 AuthenticationManager 中:

//配置令牌验证服务
@Bean
public ResourceServerTokenServices tokenServices(){
    RemoteTokenServices remote = new RemoteTokenServices();
    remote.setClientId("orderService");                 //客户端ID
    remote.setClientSecret("123456");                   //客户端secret
    remote.setCheckTokenEndpointUrl("http://localhost:9070/oauth/check_token");//配置去哪里验证 token
    remote.setAccessTokenConverter(accessTokenConverter()); //令牌解析器
    return remote;
}

这样就可以在接口获取到用户的所有信息了:

@PostMapping("/create")
public OrderDto create(@RequestBody OrderDto orderDto, @AuthenticationPrincipal User user){

3.3.4 测试资源服务器

现在我们来测试资源服务器,让它先去认证服务器拿到 token,然后用户携带这个 token 来发送请求,看看效果。

  • 启动 order-service,price-service,auth-service

    Spring Cloud 微服务安全 | (二) 网关安全_第20张图片
3.3.4.1 正确测试
  • 获取token

    Spring Cloud 微服务安全 | (二) 网关安全_第21张图片

  • 拿 token 去创建订单

    先传递 Order 的相关数据:

    Spring Cloud 微服务安全 | (二) 网关安全_第22张图片

    然后携带 token 进行访问:

    Spring Cloud 微服务安全 | (二) 网关安全_第23张图片

    没有问题,再来看看后台是否有打印用户的相关信息:

    image-20201012161253615
  • 这次不携带任何信息来访问 /haha/haha 接口,我们前面设置了它是不需要认证的:

    Spring Cloud 微服务安全 | (二) 网关安全_第24张图片

3.3.4.2 没有 token

Spring Cloud 微服务安全 | (二) 网关安全_第25张图片

3.3.4.3 token 无效

Spring Cloud 微服务安全 | (二) 网关安全_第26张图片

没有问题。


3.4 改进一:实现 ACL 权限控制

我们前面只是实现了 token 的获取和认证,还没有实现对不同请求的不同权限的控制。我们现在来改进一下。

3.4.1 给 OrderController 加一个 Get 类型的接口

@GetMapping("/{id}")
public OrderDto getById(@PathVariable("id")Integer id){
    OrderDto orderDto = new OrderDto();
    orderDto.setId(id);
    return orderDto;
}

3.4.2 来到 OAuth2ResourcesServerConfig 配置控制访问权限的 configure 方法

//控制权限 => 默认所有服务都需要携带token
@Override
public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/haha/haha").permitAll() // "/haha/haha" 不需要认证,直接放行
            .antMatchers(HttpMethod.POST).access("#oauth2.hasScope('write')")  //POST 请求必须有 write 权限
            .antMatchers(HttpMethod.GET).access("#oauth2.hasScope('read')")		 //GET 请求必须有 read 权限
            .anyRequest().authenticated();  //其他请求都必须经过认证,但具体权限不做约束
}

3.4.3 测试

  • 我们先获取一个只有 read 权限的 token

    Spring Cloud 微服务安全 | (二) 网关安全_第27张图片

  • 访问 GET 类型的接口:

    Spring Cloud 微服务安全 | (二) 网关安全_第28张图片

    没有问题。

  • 访问 POST 类型的接口:

    Spring Cloud 微服务安全 | (二) 网关安全_第29张图片

    403:已认证,但是没权限。

  • 我们现在来获取一个拥有 read 和 write 的用户然后在来访问 POST 请求:

    Spring Cloud 微服务安全 | (二) 网关安全_第30张图片

    没有问题。

这样一个最基本的 ACL 权限控制就完成了。


3.5 改进二:将客户端应用的数据持久化

我们之前客户端应用使用的是 inMemory() 将其放到内存里,这样只要服务器一重启就会将 token 清空。即使是不清空,两台不同机器之前验证内存里面的 token 也是不通过的。所以我们现在要做两件事:

  • 将客户端应用的数据持久化
  • 将 token 持久化

3.5.1 给 auth-service 加上数据库相关的依赖坐标

<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>8.0.21version>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-jdbcartifactId>
    <version>2.3.4.RELEASEversion>
dependency>

3.5.2 创建数据库并建表

这里需要建立 OAuth2.0 规定的一系列数据库表,关于这些表的每一个字段的具体作用,其实从名字就可以很容易推断出来了。当然读者可以自行查阅其他相关信息,这里就不进行赘述了。

create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

create table oauth_code (
  code VARCHAR(256), authentication BLOB
);

create table oauth_approvals (
   userId VARCHAR(256),
   clientId VARCHAR(256),
   scope VARCHAR(256),
   status VARCHAR(10),
   expiresAt DATETIME,
   lastModifiedAt DATETIME
);

3.5.3 将客户端应用信息持久化

Spring Cloud 微服务安全 | (二) 网关安全_第31张图片

这里 client_secret 在存储的时候我们需要存密文,我们可以建立一个测试方法来对 123456 进行加密,然后再将密文存到数据库中:

Spring Cloud 微服务安全 | (二) 网关安全_第32张图片

然后修改 认证服务器配置类 OAuth2AuthServerConfig 中存储客户端应用的方式,我们这里采用默认 jdbc 存储。这样在客户端获取 token 的时候,OAuth2.0 就会将 token 存到数据库中:

//注入默认的数据源
@Autowired
private DataSource dataSource;

//配置客户端应用的相关信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.jdbc(dataSource);
}

3.5.3 Token 持久化

客户端应用的持久化我们已经做好了,现在我们要将 token 持久化,这里我们需要配置一个叫 tokenStore 的对象,从名字上看就可以知道它就是负责来存储 token 的了,现在来到 认证服务器配置类 OAuth2AuthServerConfig :

//注入默认的数据源
@Autowired
private DataSource dataSource;

//token 存储器
@Bean
public TokenStore tokenStore(){
    return new JdbcTokenStore(dataSource);
}

配置完这个后,我们来修改 OAuth2AuthServerConfig 中的 令牌管理器配置方法configure(AuthorizationServerEndpointsConfigurer endpoints)

//配置令牌管理器
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
            .tokenStore(tokenStore())  //配置令牌存储模式
            .authenticationManager(authenticationManager);
}

这样就可以将 token 持久化了,当客户端获取 token 的时候,就将 token 的相关信息存储到数据库中。

3.5.4 配置数据库连接相关信息

来到 auth-serviceapplication.yml

server:
  port: 9070

spring:
  application:
    name: auth-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/imooc-security?useSSL=false&useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai
    username: root
    password: root

3.5.5 测试

Spring Cloud 微服务安全 | (二) 网关安全_第33张图片

获取 token 没有问题,来看看数据库有没有存储 token:

image-20201012174328242

没有问题~ 下面我们来引入网关,完成我们整个微服务的安全认证。


3.6 搭建网关

3.6.1 网关解决的问题

我们先来看看就目前为止我们的案例中还存在着什么样的问题:

  • 安全处理和业务逻辑耦合,增加了复杂性和变更成本。
  • 随着业务节点增加,认证服务器压力增大。
  • 多个微服务同时暴露,增加了外部访问的复杂性。

加入网关后:

Spring Cloud 微服务安全 | (二) 网关安全_第34张图片

解决了:

  • 将业务逻辑与安全处理解耦,因为现在负责跟认证服务器打交道的只有网关,而不是其他各个微服务。
  • 网关的扩缩容比业务微服务的扩缩容幅度要小得多,所以对认证服务器压力的增长会得到大大缓解。
  • 暴露统一端口,客户端只需要知道网关的端口就可以了,不需要知道每一个微服务的端口。

3.6.2 创建网关模块 zuul-service

然后导入 pom 坐标依赖,这里先不引入安全相关的依赖,我们先实现路由转发功能。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-dependenciesartifactId>
                <version>2.3.4.RELEASEversion>
            dependency>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>Hoxton.SR3version>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-zuulartifactId>
            <version>2.2.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>com.fasterxml.jackson.coregroupId>
            <artifactId>jackson-databindartifactId>
            <version>2.11.1version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.12version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <version>2.3.4.RELEASEversion>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.21version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
            <version>2.3.4.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
            <version>2.3.4.RELEASEversion>
        dependency>
    dependencies>

启动类:这里需要加入 @EnableZuulProxy 来表示该服务将作为 Zuul 网关来使用。

@SpringBootApplication
@EnableZuulProxy //作为网关
public class ZuulServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulServiceApplication.class,args);
    }
}

3.6.3 实现网关路由转发

server:
  port: 9527
spring:
  application:
    name: zuul-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/imooc-security?useSSL=false&useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai
    username: root
    password: root

# 网关配置
zuul:
  # 配置网关路由
  routes:
    token:  #路由到认证服务器
      url: http://localhost:9070
    order:  #路由到订单服务
      url: http://localhost:9090
  sensitive-headers:  #清空敏感头

前面是数据库的配置,不赘述,重点来看一下网关的配置。

这里我们写了两个路由,分别路由到认证服务器 auth-service 和订单服务器 order-service。

  • 如果路径是以 “/token” 开始的,那就转发到 auth-service
  • 如果路径是以 “/order” 开始的,那就转发到 order-service

重点是这里有个 sensitive-headers。这是敏感头,默认是 cookie、set-cookie 和 authentication 这 3 个,设为敏感头的话 Zuul 将不会传递它们到下一个过滤器,这里我们需要传递,所以我们将敏感头设为空。

启动 4 个服务,测试一下路由转发是否成功:

Spring Cloud 微服务安全 | (二) 网关安全_第35张图片

我们先来获取 token,因为我们现在设置了网关,所以要访问的是网关的端口 9527,然后要路由到 auth-service,所以后面需要加 /token,然后再加上获取 token 的接口路径,如下:

Spring Cloud 微服务安全 | (二) 网关安全_第36张图片

现在拿 token 来创建订单:

Spring Cloud 微服务安全 | (二) 网关安全_第37张图片

都没有问题。


3.6.4 实现网关安全 —— 流控

我们先来回顾之前 API 安全认证那张图:

Spring Cloud 微服务安全 | (二) 网关安全_第38张图片

所以我们第一步要做的就是流控,用 Zuul 网关来做流控的话是非常简单的。我们可以利用一个开源项目 zuul-ratelimit 然后再做简单的配置就可以了。

  • 先导入相关 pom 依赖坐标

    
    <dependency>
        <groupId>com.marcosbarbero.cloudgroupId>
        <artifactId>spring-cloud-zuul-ratelimitartifactId>
        <version>2.2.2.RELEASEversion>
    dependency>
    
  • 加入 Spring Data JPA 依赖

    因为 zuul-ratelimit 在做流控的时候,需要将流控相关的信息进行存储,它支持 JPA,也支持 Redis,这里我们存储在数据库(生产上用 redis ),所以引入 JPA:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-jpaartifactId>
        <version>2.3.4.RELEASEversion>
    dependency>
    
  • 配置数据库和 JPA:

    server:
      port: 9527
    spring:
      application:
        name: zuul-service
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/imooc-security?useSSL=false&useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai
        username: root
        password: root
      jpa:
        generate-ddl: true
        show-sql: true
    
  • 配置流控相关信息:

    # 网关配置
    zuul:
      # 配置网关路由
      routes:
        token:  #路由到认证服务器
          url: http://localhost:9070
        order:  #路由到订单服务
          url: http://localhost:9090
      sensitive-headers:  #清空敏感头
      # 配置 zuul 流控
      ratelimit:
        enabled: true             # 支持流控
        repository: JPA           # 存到 JPA,生产上是存到 Redis
        default-policy-list:      # 默认的流控策略(2.4.x 版本的 zuul-limit 好像没有这个,最好使用 2.2.x 低版本的 zuul-limit
          - limit: 2
            quota: 1              # quota=1 & limit=2 表示在 1 秒中之内可以接受 2 次请求
            refresh-interval: 1   # 刷新间隔
            type:                 # 限流的类型
              - url               # 根据 url 来限流,如 /a,/b
              - httpmethod        # 根据 httpmethod 来限流,如 get,post
    #         - user              # 根据用户来限流,需要引入 spring security,一般不用
    #         - origin            # 根据用户的 ip 地址来限流
    #   policy-list:      #对每一个路由到的微服务量身定制一个流控策略,而不采用默认的全局流控策略
    #     token:
    #       - limit: 10
    #         quota: 1
    #         refresh-interval: 1
    #         type:
    

这样流控就完成了。


3.6.5 实现网关安全 —— 认证

网关的认证授权都是通过过滤器和拦截器来实现的,我们现在开始来写过滤器。

构建 OAuth2 过滤器 OAuth2Filter

@Component
public class OAuth2Filter extends ZuulFilter {

    //过滤类型
    @Override
    public String filterType() {
        return null;
    }

    //过滤器优先级
    @Override
    public int filterOrder() {
        return 0;
    }

    //是否要进行过滤(即该过滤器是否要生效)
    @Override
    public boolean shouldFilter() {
        return false;
    }

    //具体的过滤操作 => 认证逻辑
    @Override
    public Object run() throws ZuulException {
        return null;
    }
}

Zuul 网关中的过滤器都需要继承 ZuulFilter 这个类,每一个继承该类的过滤器都需要实现 4 个方法:

  • filterType():过滤类型
    • pre:请求进来前执行,一般都是这个。
    • post:请求完成后执行。
    • error:请求异常后执行。
    • routing:将请求路由到微服务,一般直接在 yml 配置就好了。
  • filterOrder():过滤器优先级
    • 数值越小优先级越高
    • 流控我们不需要写过滤器,所以这里我们把认证过滤器优先级设为 1。
  • shouldFilter():过滤器是否生效
    • 我们这里设置永远生效(true)。
  • run():执行具体的过滤操作
    • 我们这是一个认证过滤器,所以这里我们就写我们的认证逻辑。

我们的设置如下:

/**
 * 认证
 *
 * @author Hedon Wang
 * @create 2020-10-13 11:47
 */
@Component
public class OAuth2Filter extends ZuulFilter {

    //过滤类型
    @Override
    public String filterType() {
        return "pre";
    }

    //过滤器优先级
    @Override
    public int filterOrder() {
        return 1;
    }

    //是否要进行过滤(即该过滤器是否要生效)
    @Override
    public boolean shouldFilter() {
        return true;
    }

    //具体的过滤操作 => 认证逻辑
    @Override
    public Object run() throws ZuulException {
        System.out.println("认证开始=========>>>>>>>");

        //获取当前请求的上下文 => 为了获取当前的请求对象
        RequestContext requestContext = RequestContext.getCurrentContext();
        //获取当前的请求对象
        HttpServletRequest request = requestContext.getRequest();

        /**
         * ============================================
         *                  下面开始认证
         * ============================================
         */
      
        //① 判断请求是否需要认证
        if (StringUtils.startsWith(request.getRequestURI(),"/token")){
            //以 '/token' 开头的请求是发往认证服务器的,是不需要经过认证的
            return null;
        }
        //② 需要认证的话是否携带认证信息了
        String authHeader = request.getHeader("Authorization");
        if (StringUtils.isBlank(authHeader)){
            //没带信息,往下走,这只是为了审计,后面肯定是走不到底的
            return null;
        }
        //③ 带的认证信息类型是否正确 => 需要以  bearer 开头
        if (!StringUtils.startsWithIgnoreCase(authHeader,"bearer ")){
            //OAuth2.0 是 bearer 类型的,如果不是,那也继续往下走,然后审计,后面肯定也是走不到底的
            return null;
        }
        //④ 类型正确的话解析后看看是否有权限
        try{
            //将令牌传到认证服务器去进行鉴别,并将鉴别结果封装到我们自定义的封装类 TokenInfo 中
            TokenInfo info = getTokenInfo(authHeader);
            //正常获取到 tokenInfo 的话,放到请求域中
            request.setAttribute("tokenInfo",info);
        }catch (Exception e){
            //如果获取 tokenInfo 过程抛出异常
            System.out.println("Get token info failed");
        }
        return null;
    }
}

这里面的 TokenInfo 类型和 getTokenInfo() 方法我们都没定义和实现。下面来实现。

  • TokenInfo 类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class TokenInfo {
    
        /**
         * =================================================
         *      必须按照以下字段进行命名,OAuth2.0 才能精准封装
         * =================================================
         */
        private boolean active;       //令牌是否可用
        private String client_id;     //令牌是发给哪个客户端应用的
        private String[] scope;       //令牌拥有的对资源服务器的操作权限
        private String user_name;     //令牌发送给哪个用户
        private String[] aud;         //令牌可以访问哪些资源服务器
        private Date exp;             //令牌过期时间
        private String[] authorities; //令牌拥有的所有权限信息
    
    }
    
  • getTokenInfo():传递 token 去认证服务器进行认证,然后将认证结果封装到 TokenInfo 对象中,在 OAuth2Filter 类中实现:

    //将令牌转发到认证服务器去验证,并将验证信息并放到我们自定义的封装类 TokenInfo 中
    private TokenInfo getTokenInfo(String authHeader) {
        //去掉 token 的前缀 bearer
        String token = StringUtils.substringAfter(authHeader,"bearer ");
        //认证服务器检查 token 的地址
        String oauthServiceCheckTokenUrl = "http://localhost:9070/oauth/check_token";
        //封装请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        //网关也是一个客户端,我们也需要将它注册到认证服务器中
        headers.setBasicAuth("zuul","123456");
        //封装请求实体
        MultiValueMap<String,String> params = new LinkedMultiValueMap<>();
        params.add("token",token);
        HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers);
        /**
         * 发送请求
         *
         *  参数1:请求连接
         *  参数2:请求类型
         *  参数3:请求数据
         *  参数4:请求结果封装到哪里
         */
        ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceCheckTokenUrl, HttpMethod.POST, entity, TokenInfo.class);
        //拿到响应实体
        TokenInfo tokenInfo = response.getBody();
        System.out.println("tokenInfo is "+tokenInfo);
        return tokenInfo;
    }
    

    这里我们首先要取出用户携带的 token,然后指定网关是哪个客户端,将它们封装到一个 HttpEntity 请求实体中,然后发送给认证服务器去进行认证。注意这里我们需要将网关注册到认证服务器里面,在数据库中添加:

    image-20201013171146615


3.6.6 实现网关安全 —— 审计

认证后面就是审计,现在我们来实现审计的过滤器。

@Component
public class AuditLogFilter extends ZuulFilter {

    //在请求进来前执行
    @Override
    public String filterType() {
        return "pre";
    }

    //在认证后面
    @Override
    public int filterOrder() {
        return 2;
    }

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

  	//打印审计信息
    @Override
    public Object run() throws ZuulException {
        System.out.println("审计开始 =======>>>>>>>");
        return null;
    }
}

同样如前面所说,Zuul 中的过滤器都要继承 ZuulFilter 并实现它的 4 个方法。我们这里审计就不搞数据库了,就简单输出一句话就行了,意思到位了就行。这里只是在请求进来的时候进行了审计,如果要在请求完成或者请求异常的时候进行审计,其实我们就可以修改上面 filterType 中的值了,设为 post 就是请求完成的时候进行审计,设为 error 就是请求异常的时候进行审计。


3.6.7 实现网关安全 —— 授权

授权我们还是写一个过滤器 AuthorizationFilter,具体实现代码如下:

@Component
public class AuthorizationFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    //在"进来的审计"后面
    @Override
    public int filterOrder() {
        return 3;
    }

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

    //授权
    @Override
    public Object run() throws ZuulException {
        System.out.println("授权开始=============>>>>>>>>>>>>>>>>>>");
        
        //拿到当前请求上下文
        RequestContext requestContext = RequestContext.getCurrentContext();
        //拿到请求体
        HttpServletRequest request = requestContext.getRequest();

        /**
         * ============================================
         *                  下面开始授权
         * ============================================
         */
        //① 该请求是否需要权限
        if (isNeedAuth(request)){
            //② 需要认证,那就拿出 tokenInfo
            TokenInfo tokenInfo = (TokenInfo) request.getAttribute("tokenInfo");
            //③ tokenInfo 是否存在,存在是否可用
            if (tokenInfo != null && tokenInfo.isActive()){
                //④ 认证成功(能拿到用户信息且有效) => 是否有权限
                if (hasPermissions(tokenInfo,request)){
                    //有权限就放行
                }else{
                    //没有权限,记录审计日志
                    System.out.println("异常审计===============>>>>>403 错误,授权失败....");
                    handleError(403,requestContext);
                }
            }else{
                /**
                 * tokenInfo 不存在或者不可用情况一:本来就不需要携带
                 */
                if (StringUtils.startsWith(request.getRequestURI(),"/token")){
                    // '/token' 开头的请求放行
                }else{
                    /**
                     * tokenInfo 不存在或者不可用情况二:需要带来认证
                     *      不存在:没传 tokenInfo
                     *      不可用:token 过期或无效
                     */
                    System.out.println("异常审计=============>>>>>401 错误,认证失败...");
                    //处理异常
                    handleError(401,requestContext);
                }
            }
        }

        /**
         * 进到下一个过滤器,3种情况:
         *
         *  1. isNeedAuth 返回 false,也就是请求不需要认证授权
         *  2. 到 ④ 后检查到用户是有权限的,就正常放行
         *  3. 请求路径是以 /token 开头的,其实这也是不需要认证授权的情况
         *
         */
        return null;
    }

    //判断是否拥有权限
    private boolean hasPermissions(TokenInfo tokenInfo,HttpServletRequest request) {
        //这里按道理是要查询数据库然后进行对比的,这里我们随意些,就给50%的几率可以通过
        return RandomUtils.nextInt() % 2 == 0;
    }

    //处理异常 => 发送异常信息,并且停止前行
    private void handleError(int status, RequestContext requestContext) {
        requestContext.getResponse().setContentType("application/json");
        requestContext.setResponseStatusCode(status);
        requestContext.setResponseBody("{\"message: \": \"auth fail\"}");
        //不要往后走了
        requestContext.setSendZuulResponse(false);
    }

    //判断当前请求是否需要认证
    private boolean isNeedAuth(HttpServletRequest request) {
        //这里简单搞,都要经过认证
        return true;
    }
}

我们具体来分析一下 run() 方法:

首先我们要先通过 当前请求上下文 来拿到 当前请求 HttpServletRequest,然后对这个 request 进行认证授权,具体流程如下图:

image-20201013183704167

这样我们整个网关安全的 流控->认证->审计->授权 逻辑就完成啦~ 现在已经可以通过网关来跟认证服务器打交道从而实现业务逻辑和认证逻辑的分离了。

3.6.8 删掉 order-service 中跟安全认证相关的代码

在测试之前,我们先来删除掉 order-service 中跟安全认证相关的代码,来验证我们的 “解耦” 是否真实有效。

  • 删除掉 pom 依赖坐标

    Spring Cloud 微服务安全 | (二) 网关安全_第39张图片

  • 删除跟安全认证相关的类

    Spring Cloud 微服务安全 | (二) 网关安全_第40张图片

  • 出现一个问题:

    删除上面那些类之后就出现了一个问题,之前我们在接口处获取用户信息的方式就无法执行了:

    Spring Cloud 微服务安全 | (二) 网关安全_第41张图片

    那么我们现在如果想要获取用户相关的信息该怎么做呢?

  • 演示如何获取 username

    这里我们演示如何获取 username,当然要获取其他的信息也是同理的。

    这里需要从 RequestHeader 中来获取 username:

    Spring Cloud 微服务安全 | (二) 网关安全_第42张图片

    那么我们在哪里放这个 username 呢?我们需要来到网关授权的地方 AuthorizationFilter ,在我们对用户进行认证并且授权的时候,我们顺带把 username 放到 requestHeader当中就可以了:

    Spring Cloud 微服务安全 | (二) 网关安全_第43张图片


3.7 测试

现在我们可以来测试整个网关的安全认证了。

3.7.1 获取 token

我们先获取 token :

Spring Cloud 微服务安全 | (二) 网关安全_第44张图片

看看后台的日志打印:

image-20201013220117518

没有问题。

3.7.2 携带 token 创建订单

Spring Cloud 微服务安全 | (二) 网关安全_第45张图片

我们携带刚刚的 token 来创建订单,发现是没有问题的。现在来看看后台的日志打印:

image-20201013220248107

还可以再来看 order-service 中接口有没有获取到 requestHeader 中的用户信息:

Spring Cloud 微服务安全 | (二) 网关安全_第46张图片

没有问题。

但是注意,我们前面设置了有 50% 的几率会授权不通过的,我们再多试几次,看看不通过的情况:

Spring Cloud 微服务安全 | (二) 网关安全_第47张图片

这就是不通过的情况,认证通过但是授权失败的时候,返回的是 403 错误码,我们来看看后台日志打印:

image-20201013220421857

3.7.3 不携带 token 创建订单

Spring Cloud 微服务安全 | (二) 网关安全_第48张图片

不携带 token 的话就认证失败了,返回 401,后台:

Spring Cloud 微服务安全 | (二) 网关安全_第49张图片

3.7.4 携带无效 token 创建订单

Spring Cloud 微服务安全 | (二) 网关安全_第50张图片

一样是认证失败。

3.7.5 流控测试

我们前面还加了流控,现在我们来快速访问创建订单这个接口,看看什么反应:

Spring Cloud 微服务安全 | (二) 网关安全_第51张图片

可以看到,当我们快速访问超过流控限制的时候,会报 429 错误。

至此,整个网关安全的案例就搭建完毕了~ 后面会再继续更新 SSO 单点登录和整合 JWT 令牌等学习笔记,敬请期待~
如果有什么错误的地方,还请批评指正~


你可能感兴趣的:(Java学习,JavaEE,Spring,Security,网关,spring,java,spring,security,oauth2)