zl程序教程

您现在的位置是:首页 >  后端

当前栏目

spring boot:接口站增加api版本号后的安全增强(spring boot 2.3.3)

SpringBoot接口安全API 增加 增强 2.3
2023-09-14 08:59:33 时间

一,接口站增加api版本号后需要做安全保障?

1,如果有接口需要登录后才能访问的,

  需要用spring security增加授权

 

2,接口站需要增加api版本号的检验,必须是系统中定义的版本号才能访问,

   避免乱填值刷接口的情况

 

说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

         对应的源码可以访问这里获取: https://github.com/liuhongdi/

说明:作者:刘宏缔 邮箱: 371125307@qq.com

 

二,演示项目的相关信息

1,项目地址:

https://github.com/liuhongdi/apiversionsecurity

 

2,功能说明:

          演示了接口站增加api版本号后的安全增强

 

3,项目结构:如图:

三,配置文件说明

1,pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

 

2,application.properties

#error
server.error.include-stacktrace=always
#errorlog
logging.level.org.springframework.web=trace

 

四,java代码说明

1,SecurityConfig.java

@Configuration
@EnableWebSecurity
 public class SecurityConfig extends WebSecurityConfigurerAdapter {

     @Override
     protected void configure(HttpSecurity http) throws Exception {
                  //login和logout
                  http.formLogin()
                        .defaultSuccessUrl("/v2/home/home")
                        .failureUrl("/login-error.html")
                        .permitAll()
                        .and()
                        .logout();

                  //匹配的页面,符合限制才可访问
                  http.authorizeRequests()
                 .antMatchers("/v*/home/**").hasAnyRole("ADMIN","DEV")
                 .antMatchers("/v*/goods/**").hasAnyRole("ADMIN","USER");
                  //剩下的页面,允许访问
                 http.authorizeRequests().anyRequest().permitAll();
     }

     @Autowired
     public  void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
         //添加两个账号用来做测试
         auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                 .withUser("lhdadmin")
                 .password(new BCryptPasswordEncoder().encode("123456"))
                 .roles("ADMIN","USER")
                 .and()
                 .withUser("lhduser")
                 .password(new BCryptPasswordEncoder().encode("123456"))
                 .roles("USER");
     }
 }

 

2,Constants.java

public class Constants {
    //api version
    public final static List API_VERSION_LIST = Arrays.asList("1","1.0","1.5","1.8","2","2.0");
}

定义了api版本号常量

 

3,ApiVersionCondition.java

//实现RequestCondition
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
    //api版本号
    private String apiVersion;
    //版本号的格式,如: /v[1-n]/api/test or /v1.5/home/api
    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("/v((\\d+\\.\\d+)|(\\d+))/");
    public ApiVersionCondition(String apiVersion) {
        this.apiVersion = apiVersion;
    }

    //将不同的筛选条件进行合并
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
        return new ApiVersionCondition(other.getApiVersion());
    }

    //版本比对,用于排序
    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        //优先匹配最新版本号
        return compareTo(other.getApiVersion(),this.apiVersion)?1:-1;
    }

    //获得符合匹配条件的ApiVersionCondition
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
        if (m.find()) {
            String version = m.group(1);
            //如果版本号不是list中则返回
            if (!Constants.API_VERSION_LIST.contains(version)) {
                return null;
            }
            if (compareTo(version,this.apiVersion)){
                return this;
            }
        }
        return null;
    }
    //compare version
    private boolean compareTo(String version1,String version2){
        if (!version1.contains(".")) {
            version1 += ".0";
        }
        if (!version2.contains(".")) {
            version2 += ".0";
        }
        String[] split1 = version1.split("\\.");
        String[] split2 = version2.split("\\.");
        for (int i = 0; i < split1.length; i++) {
            if (Integer.parseInt(split1[i])<Integer.parseInt(split2[i])){
                return false;
            }
        }
        return true;
    }

    public String getApiVersion() {
        return apiVersion;
    }
}

对版本号的解析和处理

 

4,ApiVersionRequestMappingHandlerMapping.java

//扩展RequestMappingHandlerMapping
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    //类上有 @ApiVersion注解时生效
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return createRequestCondition(apiVersion);
    }

    //方法上有 @ApiVersion注解时生效
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return createRequestCondition(apiVersion);
    }

    //返回ApiVersionCondition
    private RequestCondition<ApiVersionCondition> createRequestCondition(ApiVersion apiVersion) {
        return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
    }
}

定义注解的生效条件

 

5,WebMvcConfig.java

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    //在获取RequestMappingHandlerMapping时
    //返回我们自定义的ApiVersionRequestMappingHandlerMapping
    @Override
    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping();
    }
}

使自定义的版本号解析生效

 

6,ApiVersion.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiVersion {
    //版本号的值,从1开始
    String value() default "1";
}

自定义版本号的注解

 

7,HomeController.java

@RestController
@RequestMapping("/{version}/home")
public class HomeController {

    //匹配版本v1的访问
    @ApiVersion("1")
    @GetMapping
    @RequestMapping("/home")
    public String home01(@PathVariable String version) {
        return "home v1 : version:" + version;
    }

    //匹配版本v2的访问
    @ApiVersion("2.0")
    @GetMapping
    @RequestMapping("/home")
    public String home02(@PathVariable String version) {
        String username = SessionUtil.getCurrentUserName();
        String url = ServletUtil.getRequest().getRequestURL().toString();
        return "home v2 version: " + version+":username:"+username+";url:"+url;
    }

    //匹配版本v1.5-2.0的访问
    @ApiVersion("1.5")
    @GetMapping
    @RequestMapping("/home")
    public String home15(@PathVariable String version) {
        return "home v1.5 version: " + version;
    }

}

 

7,其他非关键代码请访问github

 

五,测试效果

1,有权限访问的演示:

访问:

http://127.0.0.1:8080/v2/home/home

会跳转到登录页面:

 

 我们用lhdadmin这个账号登录:

可以正常访问

用未定义的版本号访问时会报错,如图:

 

 如果一个版本号在方法没有定义,则会访问到相应的下一个版本:

如图:

 

 没有方法标注1.8,但有方法上标注了1.5,所以访问到了注解版本号1.5的这个方法

 

2,演示无权限的访问:

http://127.0.0.1:8080/v1.8/goods/goodsone

会跳转到登录页面

 

 用lhduser这个账号登录

 

 可以访问goods接口

因为没有访问home接口的授权,所以访问时会报错,如图:

六,查看spring boot版本

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.3.RELEASE)