基本使用

1、导入依赖

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

2、直接启动springboot项目,此时的security就会生效,并使用自带的过滤器
3、访问当前项目的任一请求,就会出现自带的登陆页面
image.png
4、账号密码存在当前项目的内存中即 InMemoryUserDetailsManager,账号为 user,密码在控制台输出
image.png

原理

官方文档:
Architecture :: Spring Security

HTTP请求的处理程序的典型分层

一般的http请求,客户端向应用程序发送一个请求,容器创建一个FilterChain,其中包含Filter实例和Servlet,它们应该根据请求URI的路径来处理HttpServlet请求。在Spring MVC应用程序中,Servlet是DispatcherServlet的一个实例。一个Servlet最多可以处理一个HttpServlet请求和HttpServlet响应。
image.png

委托代理过滤器

在security中,使用了以下几个过滤器用于动态配置过滤器链

  • DelegatingFilterProxy:Spring提供了一个名为DelegatingFilterProxy的Filter实现,它允许Servlet容器的生命周期和Spring的ApplicationContext之间进行桥接
  • FilterChainProxy: 是Spring Security提供的一种特殊过滤器,允许通过SecurityFilterChain将其委托给许多过滤器实例。由于FilterChainProxy是一个Bean,它通常被包装在DelegatingFilterProxy中
  • SecurityFilterChain:FilterChainProxy使用SecurityFilterChain来确定应为当前请求调用哪些Spring Security Filter实例

image.png
安全过滤器通过SecurityFilterChain API插入到FilterChainProxy中。这些过滤器可以用于多种不同的目的,如身份验证、授权、漏洞保护等。过滤器按特定顺序执行,以确保它们在正确的时间被调用,例如,执行身份验证的过滤器应在执行授权的过滤器之前被调用。通常不需要知道Spring Security的过滤器的顺序。但是,有时了解排序是有益的,如果您想了解它们,可以检查FilterOrderRegistration代码。

默认过滤器链

1、DefaultSecurityFilterChain 就是一个默认的 SecurityFilterChain
2、在 DefaultSecurityFilterChain 类中打断点测试
image.png
3、默认的15个过滤器
image.png

配置登录的账号密码

官方文档:
Username/Password Authentication :: Spring Security
1、创建配置类,并创建 UserDetailsService 的 bean,这样就会使用自定义的这个 bean 去进行校验

@Configuration
public class SecurityConfig { 

    @Bean
    public UserDetailsService userDetailsService() { 
        UserDetails userDetails = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(userDetails);
    } 
} 

2、以上是创建了一个 InMemoryUserDetailsManager 的 bean 对象,将账号密码写在代码中
3、此时重新启动项目,登录的账号密码就被设置好了
image.png

基于内存的用户认证流程

程序启动时:

  • 创建 InMemoryUserDetailsManager 对象

image.png

  • 创建 User 对象,封装用户名密码

image.png

  • 使用 InMemoryUserDetailsManager 将 User 存入内存

image.png
校验用户时:

  • UsernamePasswordAuthenticationFilter 过滤器调用 attemptAuthentication 方法进行校验

image.png

  • 调用 InMemoryUserDetailsManager 的 loadUserByUsername 方法从内存中获取 User 对象

通过数据库设置账号密码

1、查看 UserDetailsService 接口的实现类,
image.png
2、创建 UserDetailsService 实现类

@Service
public class UserDetailsServiceImpl implements UserDetailsService { 

    @Resource
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
        SysUser user = Optional.ofNullable(username)
                .map(it -> sysUserService.getOne(Wrappers.<SysUser>lambdaQuery()
                        .eq(SysUser::getName, it)
                        .last("limit 1")))
                .orElseThrow(() -> new RuntimeException("获取用户信息失败"));
        // 转为 UserDetails 对象
        return LoginUser.builder()
                .sysUser(user)
                .build();
    } 
} 

3、创建 UserDetails 实现类

@Data
@Builder
public class LoginUser implements UserDetails { 
    /**
     * 系统用户
     */
    private SysUser sysUser;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { 
        return null;
    } 

    @Override
    public String getPassword() { 
        return sysUser.getPassword();
    } 

    @Override
    public String getUsername() { 
        return sysUser.getName();
    } 

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

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

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

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

4、增加配置类,设置密码加密的 bean

@Configuration
public class SecurityConfig { 

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() { 
        return new BCryptPasswordEncoder();
    } 
} 

5、现在登录功能就是使用数据库中获取的用户信息了

Security配置

1、创建 WebSecurityConfigurerAdapter 实现类,重写 configure(HttpSecurity http) 方法,就可以配置过滤器链,设置请求过滤、登录页、登录页、添加过滤器等操作

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter { 

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() { 
        return new BCryptPasswordEncoder();
    } 

    @Override
    protected void configure(HttpSecurity http) throws Exception { 
        http
            // 过滤请求
            .authorizeRequests()
            // 对以下接口放行
            .antMatchers("/hello", "/login")
            .permitAll()
            // 除上面外的所有请求全部需要鉴权认证
            .anyRequest().authenticated().and().headers().frameOptions().disable();
        // 去掉这一行就没有默认的登录页面了
        http.formLogin();
    } 
} 

2、配置自定义过滤器,新增 JwtAuthenticationTokenFilter 模仿使用 jwt 进行拦截

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { 

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 
        //获取请求携带的令牌
        String token = request.getHeader("Authorization");
        System.out.println(token);
        //放行
        filterChain.doFilter(request, response);
    } 
} 

3、在 SecurityConfig 配置类中添加过滤器
image.png
4、这个时候把默认的登录页去掉,然后进行访问测试
image.png
image.png
5、直接访问放行的端口是正常并且每个请求都会经过自定义的过滤器
6、但是如果访问未放行的路径则会报错,提示“未获权限”
image.png
7、这个时候如果在 jwt 过滤器中往 security 的上下文设置 Authentication,则所有请求都会获取访问权限
image.png
8、再次访问未放行的路径则可以正常进入
image.png
9、只不过 jwt 过滤器中的登录信息需要从 token 中获取并验证是否有效,是否过期然后再放行
10、至此,还需要配置一个登录接口,用户登录成功之后放回一个 jwt 的 token 给前端,后续前端请求的时候携带上这个 token 即可继续访问
11、在 SecurityConfig 配置类中配置登出接口,创建 logoutSuccessHandler 登出处理类,在登出时候将 jwt 的 token 删除掉

/**
 * 登出成功处理
 */
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;

// 添加Logout filter
http.logout().logoutUrl("/sys/user/logout").logoutSuccessHandler(logoutSuccessHandler);

12、创建登出处理类

@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { 

    @Autowired
    private JwtUtils jwtUtils;
    @Autowired
    private RedisService redisService;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 
        // 获取请求携带的令牌
        String token = request.getHeader("Authorization");
        Payload payload = jwtUtils.validate(token);
        String key = payload.getUuid();
        // 删除用户记录
        redisService.removeKey(key);
        // 继续响应
        try { 
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print("注销成功");
        }  catch (IOException e) { 
            e.printStackTrace();
        } 
    } 

} 

配置登录接口

方式一

在 security 中,都是通过调用 AuthenticationManager 接口的 authenticate 方法进行鉴权的,所以我们只需要在我们自己的登录接口中调用到这个鉴权方法即可
image.png
1、AuthenticationManager 是一个 security 的接口,所以我们需要找到他的实现类,而在 WebSecurityConfigurerAdapter 类中,存在一个方法用于创建 AuthenticationManager 对象,但这个对象并未被注册成为 bean,所以还无法直接使用
image.png
image.png
2、同时 WebSecurityConfigurerAdapter 也是我们配置 security 时需要继承的类,所以只需要在我们自定义的 security 配置类中重写这个方法并注册为 bean 即可

@Configuration
@EnableAuthorizationServer
public class SecurityConfig extends WebSecurityConfigurerAdapter { 

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

3、配置自定义登录接口

@RestController
@RequestMapping("/sys/user")
public class SysUserController { 
    @Autowired
    private SysUserService sysUserService;

    @PostMapping("/login")
    public ResponseData<Authentication> login(@Validated @RequestBody SysLoginBody body) { 
        return ResponseData.success(sysUserService.login(body));
    } 
} 
@Service
public class SysUserService { 
    @Autowired
    private AuthenticationManager authenticationManager;

    public Authentication login(SysLoginBody body) { 
        Authentication authentication;
        try { 
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(username, password);
            // 调用登录鉴权方法
            authentication = authenticationManager.authenticate(authenticationToken);
        }  catch (BadCredentialsException e) { 
            throw new FlowException("密码有误");
        }  catch (Exception e) { 
            throw new FlowException(e.getMessage());
        } 
        return authentication;
    } 
} 

方式二

通过自定义过滤器进行拦截

  1. 创建 AbstractHttpConfigurer 继承类,添加自定义过滤器 WebPageLoginFilter,配置在 UsernamePasswordAuthenticationFilter 之前,即不用默认的登录过滤器了
@Slf4j
public class LoginFilterDsl extends AbstractHttpConfigurer<LoginFilterDsl, HttpSecurity> { 
    @Override
    public void init(HttpSecurity builder) throws Exception { 
        super.init(builder);
    } 

    @Override
    public void configure(HttpSecurity builder) throws Exception { 
        super.configure(builder);
        log.info("[{ } ] configure", getClass().getSimpleName());
        ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
        WebLoginFilter webLoginFilter = context.getBean(WebLoginFilter.class);
        builder.addFilterBefore(webLoginFilter, UsernamePasswordAuthenticationFilter.class);
    } 

    public static LoginFilterDsl webLoginFilterDsl() { 
        return new LoginFilterDsl();
    } 
} 
  1. 仿照 UsernamePasswordAuthenticationFilter 过滤器,新增 AbstractAuthenticationProcessingFilter 实现类的过滤器,创建构造方法,实现 attemptAuthentication() 鉴权方法
public class WebLoginFilter extends AbstractAuthenticationProcessingFilter { 

    private static final AntPathRequestMatcher MATCHER = new AntPathRequestMatcher("/web-login", HttpMethod.POST.name());

    public WebLoginFilter() { 
        super(MATCHER);
    } 

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { 
        return null;
    } 
} 
  1. 在 WebSecurityConfig 配置类中使用 LoginFilterDsl 配置类即可
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 

    @Override
    public void configure(HttpSecurity http) throws Exception { 
        // 获取直接在这里添加过滤器也可以,就不用创建 LoginFilterDsl 配置类了
        http.apply(webLoginFilterDsl());
    } 
} 

原理解释

  1. 因为继承了 AbstractAuthenticationProcessingFilter 过滤器,并调用了父类的构造方法,所以会将我们自定义的登录路径配置进去

image.png
image.png

  1. 而 AbstractAuthenticationProcessingFilter 是实现了 Filter 接口

image.png

  1. 所以 AbstractAuthenticationProcessingFilter 就会被调用 doFilter() 方法,判断当前路径是否为登录路径,是的话就会调用 attemptAuthentication() 方法进行登录

image.png
image.png
image.png

  1. 所以当我们访问 /web-login 路径的时候就会调用我们自定义的登陆方法

image.png

示例代码

晓江/spring-security-test