SpringSecurity

最近面试了 JD 厂的网络安全部门,有以下几个问题答得不是很好,本文对 SpringSecurity 相关知识点进行梳理。

执行流程

加载配置文件,DelegatingFilterProxy 完成初始化操作,从容器中获取到 FilterChainProxy 对象,顺序调用过滤器。

  • UsernamePasswordAuthenticationFilter 认证
  • ExceptionTranslationFilter 异常
  • FilterSecurityInterceptor 授权

SpringSecurity 如何实现认证和授权?

认证

LoginServiceImpl.java (核心:将用户名、密码、权限,封装 Authentication)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
UserDetails userDetails = loadUserByUsername(username);
            if (!passwordEncoder.matches(password, userDetails.getPassword())) {
                Asserts.fail("密码不正确");
            }
            if (!userDetails.isEnabled()) {
                Asserts.fail("帐号已被禁用");
            }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
            updateLoginTimeByUsername(username);
            insertLoginLog(username);

SecurityConfig.java 认证过滤器配置(跨域、白名单、JWT 过滤器、动态权限校验过滤器?)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
  ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
  //不需要保护的资源路径允许访问
  for (String url : ignoreUrlsConfig.getUrls()) {
    registry.antMatchers(url).permitAll();
  }
  //允许跨域请求的OPTIONS请求
  registry.antMatchers(HttpMethod.OPTIONS)
    .permitAll();
  // 任何请求需要身份认证
  registry.and()
    .authorizeRequests()
    .anyRequest()
    .authenticated()
    // 禁用了 CSRF 保护,并配置了使用无状态的会话管理策略
    .and()
    .csrf()
    .disable()
    .sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    // 自定义权限拒绝处理类
    .and()
    .exceptionHandling()
    .accessDeniedHandler(restfulAccessDeniedHandler)
    .authenticationEntryPoint(restAuthenticationEntryPoint)
    // 自定义权限拦截器JWT过滤器
    .and()
    .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
  //有动态权限配置时添加动态权限校验过滤器
  if (dynamicSecurityService != null) {
    registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
  }
  return httpSecurity.build();
}

登录

  • 调用 ProviderManager 的方法进行认证,通过 Jwts.builder() 生成 JWT(无状态SessionCreationPolicy.STATELESS
    • Claims 对象:存储用户 id 和自定义键值对
    • Expiration:过期时间
    • SignWith:加密方式和盐值
  • 实现 UserDetails 接口
  • 密码加密采用:BCryptPasswordEncoder
  • 将用户信息存入 Redis 中

每次请求

  • 继承 OncePerRequestFilter,定义 jwt 认证过滤器,从 header.Authorization 字段中获取 token,解析获得用户 id
  • 查询 Redis ,将用户信息放到 SecurityContextHolder 中

授权

FilterSecurityInterceptor 从 SecurityContextHolder 中获取 Authentication 里的权限信息。

基于注解:

  • 启动类配置:EnableGlobalMethodSecurity(prePostEnabled = true)
  • 在接口方法前
    • @PreAuthorize(hasAuthority('test'))
    • @PreAuthorize(hasAnyAuthority('streamer','owner'))
    • @PreAuthorize(hasRole('test')):会拼接 “ROLE_",不常用
    • 自定义
      • @Component(“beanName”)
      • @PreAuthorize("@beanName.method(’test’)”)

AdminUserDetails.java

1
2
3
4
5
6
7
8
9
public class AdminUserDetails implements UserDetails {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //返回当前用户的角色
        return resourceList.stream()
                .map(role -> new SimpleGrantedAuthority(role.getId() + ":" + role.getName()))
                .collect(Collectors.toList());
    }
}

什么是 CSRF攻击,SpringSecurity 如何防范?

CSRF:跨站请求伪造,利用受信任用户的身份执行未经授权的操作

  1. 生成一个随机的 CSRF Token,并将其嵌入到每个需要防范 CSRF 攻击的表单中。
  2. 将 CSRF Token 设置为 HttpOnly Cookie:将 CSRF Token 存储在 HttpOnly Cookie 中,以防止它被 JavaScript 访问。
  3. Spring Security 默认启用 CSRF 保护,它会拦截所有非 GET 请求。如果 Token 不匹配,则拒绝请求。
  4. 将 Cookie 的 SameSite 属性设置为 “Strict” 或 “Lax”,以限制跨站点请求。
    1. “Strict” 模式:将完全禁止第三方站点发送带有 Cookie 的跨站请求
    2. “Lax” 模式:仅允许安全的 GET 请求带有 Cookie。

如果有了 JWT token,就不需要 CSRF token 了,关闭即可:

1
2
3
4
registry.csrf()
    .disable()
    .sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

对信息安全和数据安全的理解?

措施:认证授权、加密传输、防火墙、安全审计、HTTPS、安全漏洞(跨站脚本攻击、SQL 注入攻击)、数字签名……

如何保证缓存数据的安全?

  • 脱敏,只缓存非敏感数据
  • 加密
  • 过期策略
    • no-enviction:不淘汰,满了就报错(默认)
    • volatile-:从已过期的数据集(server.db[i].expires)中挑选
      • ttl:剩余 ttl 越短的优先淘汰
      • random:随机
      • lru:最近最少使用【业务数据有冷热区分,且有置顶需求】
      • lfu:最少频率使用
    • allkeys-:针对全体
      • random【无明显冷热数据区分时】
      • lru【业务数据有冷热区分】
      • lfu【短时高频】
  • 缓存隔离:不同应用使用独立的缓存实例(docker容器)

如何允许跨域?

跨域:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。

  • 域名不同
  • 域名相同,端口不同

通过 SpringMVC 的 CorsFilter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class GlobalCorsConfig {
    /**
     * 允许跨域调用的过滤器
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        //允许所有域名进行跨域调用
        config.addAllowedOriginPattern("*");
        //该用法在SpringBoot 2.7.0中已不再支持
        //config.addAllowedOrigin("*");
        //允许跨越发送cookie
        config.setAllowCredentials(true);
        //放行全部原始头信息
        config.addAllowedHeader("*");
        //允许所有请求方法跨域调用,比如 GET/POST/PUT/DELETE
        config.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

单点登录 SSO = Single Sign On

一次登录,访问所有信任的应用系统。

  1. 集成认证中心到应用系统中,如 spring-security-oauth2
  2. 在认证中心中配置应用系统的信息,包括应用名称、域名、回调地址,同时为用户分配唯一 id
  3. 集成认证中心提供的 SDK 或库
  4. 用户未登录时,重定向(redirect)到认证中心进行登录,验证完身份重定向回应用系统,在 URL 参数中携带用户的身份标识符
  5. 记录用户登录状态于 Session 中

需求:五次登录失败锁定三十分钟

  1. 创建一个实现AuthenticationFailureHandler接口的类 failureHandler,用于处理认证失败的情况。
  2. 在 SecurityConfig extends WebSecurityConfigurerAdapter 中配置http.formLogin().failureHandler(failureHandler)
  3. 在失败处理器中,可以获取登录失败的用户名,然后将其作为键存储到Redis中,并将失败次数作为值进行计数。同时,设置键的过期时间为30分钟。
  4. 在登录前,判断用户是否是登录失败 5 次以上。
0%