Spring Security 多过滤链的使用

一、背景

在我们实际的开发过程中,有些时候可能存在这么一些情况,某些api 比如: /api/** 这些是给App端使用的,数据的返回都是以JSON的格式返回,且这些API的认证方式都是使用的TOKEN进行认证。而除了 /api/** 这些API之外,都是给网页端使用的,需要使用表单认证,给前端返回的
都是某个页面。

二、需求

1、给客户端使用的api

  1. 拦截 /api/**所有的请求。

  2. /api/**的所有请求都需要ROLE_ADMIN的角色。

  3. 从请求头中获取 token,只要获取到token的值,就认为认证成功,并赋予ROLE_ADMIN到角色。

  4. 如果没有权限,则给前端返回JSON对象 {message:"您无权限访问"}

  5. 访问 /api/userInfo端点

    1. 请求头携带 token 可以访问。
    2. 请求头不携带token不可以访问。

2、给网站使用的api

  1. 拦截 所有的请求,但是不处理/api/**开头的请求。
  2. 所有的请求需要ROLE_ADMIN的权限。
  3. 没有权限,需要使用表单登录。
  4. 登录成功后,访问了无权限的请求,直接跳转到百度去。
  5. 构建2个内建的用户
    1. 用户一: admin/admin 拥有 ROLE_ADMIN 角色
    2. 用户二:dev/dev 拥有 ROLE_DEV 角色
  6. 访问 /index 端点
    1. admin 用户访问,可以访问。
    2. dev 用户访问,不可以访问,权限不够。

三、实现方案

方案一:

直接拆成多个服务,其中 /api/** 的成为一个服务。非/api/**的拆成另外一个服务。各个服务使用自己的配置,互不影响。

方案二

在同一个服务中编写。不同的请求使用不同的SecurityFilterChain来实现。

经过考虑,此处采用方案二来实现,因为方案一简单,使用方案二实现,也可以记录下在同一个项目中 通过使用多条过滤器链,因为并不是所有的时候,都是可以分成多个项目的。

扩展:

1、Spring Security SecurityFilterChain 的结构

SecurityFilterChain的结构

2、控制 SecurityFilterChain 的执行顺序

使用 org.springframework.core.annotation.Order 注解。

3、查看是怎样选择那个 SecurityFilterChain

查看 org.springframework.web.filter.DelegatingFilterProxy#doFilter方法

四、实现

1、app 端 Spring Security 的配置

package com.huan.study.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;

/**
 * 给 app 端用的 Security 配置
 *
 * @author huan.fu 2021/7/13 - 下午9:06
 */
@Configuration
public class AppSecurityConfig {

    /**
     * 处理 给 app(前后端分离) 端使用的过滤链
     * 以 json 的数据格式返回给前端
     */
    @Bean
    @Order(1)
    public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception {
        // 只处理 /api 开头的请求
        return http.antMatcher("/api/**")
                .authorizeRequests()
                // 所有以 /api 开头的请求都需要 ADMIN 的权限
                    .antMatchers("/api/**")
                    .hasRole("ADMIN")
                    .and()
                // 捕获到异常,直接给前端返回 json 串
                .exceptionHandling()
                    .authenticationEntryPoint((request, response, authException) -> {
                        response.setStatus(HttpStatus.UNAUTHORIZED.value());
                        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                        response.setContentType(MediaType.APPLICATION_JSON.toString());
                        response.getWriter().write("{\"message:\":\"您无权访问01\"}");
                    })
                    .accessDeniedHandler((request, response, accessDeniedException) -> {
                        response.setStatus(HttpStatus.UNAUTHORIZED.value());
                        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                        response.setContentType(MediaType.APPLICATION_JSON.toString());
                        response.getWriter().write("{\"message:\":\"您无权访问02\"}");
                    })
                    .and()
                // 用户认证
                .addFilterBefore((request, response, chain) -> {
                    // 此处可以模拟从 token 中解析出用户名、权限等
                    String token = ((HttpServletRequest) request).getHeader("token");
                    if (!StringUtils.hasText(token)) {
                        chain.doFilter(request, response);
                        return;
                    }
                    Authentication authentication = new TestingAuthenticationToken(token, null,
                            AuthorityUtils.createAuthorityList("ROLE_ADMIN"));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    chain.doFilter(request, response);
                }, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

2、网站端 Spring Secuirty 的配置

package com.huan.study.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 * 给 网站 应用的安全配置
 *
 * @author huan.fu 2021/7/14 - 上午9:09
 */
@Configuration
public class WebSiteSecurityFilterChainConfig {
    /**
     * 处理 给 webSite(非前后端分离) 端使用的过滤链
     * 以 页面 的格式返回给前端
     */
    @Bean
    @Order(2)
    public SecurityFilterChain webSiteSecurityFilterChain(HttpSecurity http) throws Exception {

        AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);

        // 创建用户
        authenticationManagerBuilder.inMemoryAuthentication()
                .withUser("admin")
                    .password(new BCryptPasswordEncoder().encode("admin"))
                    .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN"))
                    .and()
                .withUser("dev")
                    .password(new BCryptPasswordEncoder().encode("dev"))
                    .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_DEV"))
                    .and()
                .passwordEncoder(new BCryptPasswordEncoder());

        // 只处理 所有 开头的请求
        return http.antMatcher("/**")
                .authorizeRequests()
                // 所有请求都必须要认证才可以访问
                    .anyRequest()
                    .hasRole("ADMIN")
                    .and()
                // 禁用csrf
                .csrf()
                    .disable()
                // 启用表单登录
                .formLogin()
                    .permitAll()
                    .and()
                // 捕获成功认证后无权限访问异常,直接跳转到 百度
                .exceptionHandling()
                    .accessDeniedHandler((request, response, exception) -> {
                        response.sendRedirect("http://www.baidu.com");
                    })
                    .and()
                .build();
    }

    /**
     * 忽略静态资源
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer( ){
        return web -> web.ignoring()
                .antMatchers("/**/js/**")
                .antMatchers("/**/css/**");

    }
}

3、控制器写法

/**
 * 资源控制器
 *
 * @author huan.fu 2021/7/13 - 下午9:33
 */
@Controller
public class ResourceController {

    /**
     * 返回用户信息
     */
    @GetMapping("/api/userInfo")
    @ResponseBody
    public Authentication showUserInfoApi() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

    @GetMapping("/index")
    public String index(Model model){
        model.addAttribute("username","张三");
        return "index";
    }
}

4、引入jar包

<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>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

五、实现效果

1、app 有权限访问 api

app 有权限访问 api

2、app 无权限访问 api

app 无权限访问 api

3、admin 用户有权限访问 网站 api

admin 用户有权限访问 网站 api

4、dev 用户无权限访问 网站 api

dev 用户无权限访问 网站 api

访问无权限的API直接跳转到 百度 首页。

六、完整代码

https://gitee.com/huan1993/Spring-Security/tree/master/multi-security-filter-chain

posted @ 2021-07-14 16:01  huan1993  阅读(277)  评论(0编辑  收藏  举报