最近面试了 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:跨站请求伪造,利用受信任用户的身份执行未经授权的操作
- 生成一个随机的 CSRF Token,并将其嵌入到每个需要防范 CSRF 攻击的表单中。
- 将 CSRF Token 设置为 HttpOnly Cookie:将 CSRF Token 存储在 HttpOnly Cookie 中,以防止它被 JavaScript 访问。
- Spring Security 默认启用 CSRF 保护,它会拦截所有非 GET 请求。如果 Token 不匹配,则拒绝请求。
- 将 Cookie 的 SameSite 属性设置为 “Strict” 或 “Lax”,以限制跨站点请求。
- “Strict” 模式:将完全禁止第三方站点发送带有 Cookie 的跨站请求
- “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
一次登录,访问所有信任的应用系统。
- 集成认证中心到应用系统中,如 spring-security-oauth2
- 在认证中心中配置应用系统的信息,包括应用名称、域名、回调地址,同时为用户分配唯一 id
- 集成认证中心提供的 SDK 或库
- 用户未登录时,重定向(redirect)到认证中心进行登录,验证完身份重定向回应用系统,在 URL 参数中携带用户的身份标识符
- 记录用户登录状态于 Session 中
需求:五次登录失败锁定三十分钟
- 创建一个实现
AuthenticationFailureHandler
接口的类 failureHandler,用于处理认证失败的情况。 - 在 SecurityConfig extends WebSecurityConfigurerAdapter 中配置
http.formLogin().failureHandler(failureHandler)
- 在失败处理器中,可以获取登录失败的用户名,然后将其作为键存储到Redis中,并将失败次数作为值进行计数。同时,设置键的过期时间为30分钟。
- 在登录前,判断用户是否是登录失败 5 次以上。