作者归档:Rick

SpringMVC添加过滤器Filter(一)

实现方式

第一种方式

继承自 OncePerRequestFilter 并标记 @Component 组件

@Component
public class MyFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter: MyFilter 执行之前");
        chain.doFilter(request, response);
        System.out.println("Filter: MyFilter 执行之后");
    }

}

第二种方式

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean filterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
                System.out.println("Filter: filterRegistration 执行 执行之前");
                chain.doFilter(request, response);
                System.out.println("Filter: filterRegistration 执行 执行之后");
            }
        });
        registration.addUrlPatterns("/*");
        registration.addInitParameter("paramName", "paramValue");
        registration.setName("filterRegistration");
        registration.setOrder(Integer.MIN_VALUE);
        return registration;
    }
}

这种方式可以对 Filter 做更详细的设置,如拦截的url、初始化参数、拦截器order

用途

拦截请求,可以对请求做「权限验证」,比如验证码校验。

github地址: https://github.com/jkxyx205/spring-boot-learn/tree/master/spring-mvc-filter

sharp-common集成验证Service验证

对Service中的方法的参数进行校验,该如何做呢?

Spring validator 方法级别的校验

只需要加入类注解 @Validated

@Service
@Validated
public class UserService {

    public void save(@Valid User user,
                     @Min(value = 22, message = "不能小于22岁") Integer age) {
        System.out.println("userService save...");
    }

    public void save2(@NotBlank String title) {
        System.out.println("userService save3...");
    }
}

spring 在此基础上进行了扩展,添加了 MethodValidationPostProcessor 拦截器,可以实现对方法参数的校验。

sharp-common集成验证Service验证

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Configuration
@RequiredArgsConstructor
@Import({ServiceMethodValidationInterceptor.class})
public class AppConfig {
    private final Validator validator;
    @Bean
    public ValidatorHelper validatorHelper() {
        return new ValidatorHelper(validator);
    }
}
@Service
public class UserService {

    @Validated
    public void save(@Valid User user,
                     @Min(value = 22, message = "不能小于22岁") Integer age) {
        System.out.println("userService save...");
    }

    @Validated
    public void save2(@NotBlank String title) {
        System.out.println("userService save3...");
    }
}

扫描service目录下,类名是Service结尾的类。只要在方法上加注解 @Validated

参考链接:

SpringBoot注解@SpringBootApplication

@SpringBootApplication

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication

@SpringBootApplication 注解是由 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan 三个注解合并而成的。下面分析这三个注解。

@SpringBootConfiguration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}

@SpringBootConfiguration 就是 @Configuration

@ComponentScan

@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

@ComponentScan 扫描Spring组件,比如注解Configuration Controller Service 等。如果没有指定扫描包,表示当前类的路径下。
SpringBoot排除 AutoConfigurationExcludeFilter 过滤器中的自动配置类。

AutoConfigurationExcludeFilter.java

public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
     return this.isConfiguration(metadataReader) && this.isAutoConfiguration(metadataReader);
 }

由match方法可知,如果是Configuration,同时是AutoConfiguration才会扫描。Configuration类就是在类上加入@Configuration 的类。AutoConfiguration类就是在 spring.factories 配置的类。

/spring-boot-autoconfigure/META-INF/spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
...

这些都属于自动配置类,不需要 @ComponentScan 去扫描。

EnableAutoConfiguration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration

EnableAutoConfiguration 作用:

  1. 当我们的 classpath 中存在某些 jar 或者类时,默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件,帮助我们自动配置 bean;
  2. 当我们定义自己的配置时,自动配置的 bean 将不再加载。(具体是通过 一系列的 @ConditionalOnXxx 来实现的)

EnableAutoConfiguration 是由注解 @AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class}) 组成。

  • AutoConfigurationPackage 注解的作用是将添加该注解的类所在的package 作为自动配置package进行管理。
  • @Import(AutoConfigurationImportSelector.class)
  1. 利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
  2. 调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
  3. 利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader)得到所有的组件
  4. 从META-INF/spring.factories位置来加载一个文件。
    默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
    spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factorie

@EnableXxx 注解是用在 @Configuration 标记的类上,用来自动配置和加载一些 @Configuration 类或者@bean的。 所以,@EnableXxx 一定是与 @Configuration 一起使用的。
@EnableXxx 一般会与 @Import 一起使用,通过 @Import 来达到自动配置的目的。
比如:EnableAsync EnableCaching

参考文档:

Spring Security 生成token(十三)

说明

前后端分离的项目,需要后端认证成功之后,颁发令牌 token ,下次客户端在请求资源的时候,需要携带 access_token 参数。

Spring Security 生成 token 的基本思路是:

  • 表单认证之后,在 successHandler 中根据 Authentication 生成 token,响应给前端
  • 创建 Filter TokenAuthenticationFilter 当前请求参数中有参数 access_token,解析token,获取 Authentication 放入 SecurityContext 上下文中。

开发

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
    <groupId>com.rick.common</groupId>
    <artifactId>sharp-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

token授权过滤器 TokenAuthenticationFilter.java

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String accessToken = request.getParameter("access_token");
        if (StringUtils.isBlank(accessToken)) {
            chain.doFilter(request, response);
            return;
        }

        try {
            Authentication authentication = JWTUtils.toAuthentication(accessToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } catch (Exception e) {
            HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
                    , JsonUtils.toJson(ResultUtils.exception(403, e.getMessage())));
        }
    }
}

安全配置 WebSecurityConfig.java

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests((requests) -> {
            ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) requests.anyRequest()).authenticated();
        })
                .formLogin()
                // 1. 认证成功后将token响应给前端
                .successHandler((request, response, authentication) -> {
                    String token = JWTUtils.createToken(authentication);
                    HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
                            , JsonUtils.toJson(ResultUtils.success(token)));
                })
                .and().exceptionHandling().authenticationEntryPoint((request, response, e) -> {
            HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
                    , JsonUtils.toJson(ResultUtils.exception(403, e.getMessage())));
        })
                .and().exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
                HttpServletResponseUtils.write(response, "application/json;charset=UTF-8"
                        , JsonUtils.toJson(ResultUtils.exception(403, e.getMessage())));
            }
        }).and().addFilterBefore(new TokenAuthenticationFilter(), BasicAuthenticationFilter.class)
                .csrf().disable();

    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}
  • 添加token授权过滤器 TokenAuthenticationFilter
  • 表单认证之后,在 successHandler 中根据 Authentication 生成 token,响应给前端
  • 认证授权的异常处理

添加用户信息 application.yml

spring:
  security:
    user:
      name: rick
      password: 123456
      roles: ADMIN

API接口 IndexController.java

@RestController
public class IndexController {

    @GetMapping
    public String index() {
        return "index";
    }

    @GetMapping("admin")
    public String admin(Authentication authentication) {
        return "admin ==> ";
    }

    @GetMapping("p1")
    @PreAuthorize("hasRole('p1')")
    public String p1(Authentication authentication) {
        return authentication.getPrincipal().toString();
    }
}

测试

没有携带token

curl -X GET \

http://127.0.0.1:8080/admin

响应

{
    "success": false,
    "code": 403,
    "msg": "Full authentication is required to access this resource"
}

认证

curl -X POST \
  'http://127.0.0.1:8080/login?username=rick&password=123456'

响应

{
    "success": true,
    "code": 0,
    "msg": "OK",
    "data": "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC94aG9wZS50b3AiLCJzdWIiOiJvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5LmNvcmUudXNlcmRldGFpbHMuVXNlciBbVXNlcm5hbWU9cmljaywgUGFzc3dvcmQ9W1BST1RFQ1RFRF0sIEVuYWJsZWQ9dHJ1ZSwgQWNjb3VudE5vbkV4cGlyZWQ9dHJ1ZSwgY3JlZGVudGlhbHNOb25FeHBpcmVkPXRydWUsIEFjY291bnROb25Mb2NrZWQ9dHJ1ZSwgR3JhbnRlZCBBdXRob3JpdGllcz1bUk9MRV9BRE1JTl1dIiwiZXhwIjoxNjM0MzYyMzIxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl19.e4-tJzixAkOk9xeoX9zkehYjaH_DmBPe9tho6cCtu6M"
}

携带token

curl -X GET \
  'http://127.0.0.1:8080/admin?access_token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC94aG9wZS50b3AiLCJzdWIiOiJvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5LmNvcmUudXNlcmRldGFpbHMuVXNlciBbVXNlcm5hbWU9cmljaywgUGFzc3dvcmQ9W1BST1RFQ1RFRF0sIEVuYWJsZWQ9dHJ1ZSwgQWNjb3VudE5vbkV4cGlyZWQ9dHJ1ZSwgY3JlZGVudGlhbHNOb25FeHBpcmVkPXRydWUsIEFjY291bnROb25Mb2NrZWQ9dHJ1ZSwgR3JhbnRlZCBBdXRob3JpdGllcz1bUk9MRV9BRE1JTl1dIiwiZXhwIjoxNjM0MzYyMzIxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl19.e4-tJzixAkOk9xeoX9zkehYjaH_DmBPe9tho6cCtu6M'

响应:token正确

admin ==> 

能正常访问资源信息。

响应:如果token错误

{
    "success": false,
    "code": 403,
    "msg": "无效的token"
}

访问没有权限的资源 /p

curl -X GET \
  'http://127.0.0.1:8080/p1?access_token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC94aG9wZS50b3AiLCJzdWIiOiJvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5LmNvcmUudXNlcmRldGFpbHMuVXNlciBbVXNlcm5hbWU9cmljaywgUGFzc3dvcmQ9W1BST1RFQ1RFRF0sIEVuYWJsZWQ9dHJ1ZSwgQWNjb3VudE5vbkV4cGlyZWQ9dHJ1ZSwgY3JlZGVudGlhbHNOb25FeHBpcmVkPXRydWUsIEFjY291bnROb25Mb2NrZWQ9dHJ1ZSwgR3JhbnRlZCBBdXRob3JpdGllcz1bUk9MRV9BRE1JTl1dIiwiZXhwIjoxNjM0MzYyMzIxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl19.e4-tJzixAkOk9xeoX9zkehYjaH_DmBPe9tho6cCtu6M'

响应

{
    "success": false,
    "code": 403,
    "msg": "Access is denied"
}

源代码:https://github.com/jkxyx205/spring-security-learn/tree/master/spring-security-token