Spring Security In Action 读书笔记
Spring Security in Action
2023-7-30 Just Book, Just learning!
这本书适用于初学者,简单的探讨 ss 认证,权限控制, 安全防护,OAth2 的使用,并没有涉及具体的架构(只有一个简单的认证架构图),其中权限控制讲的内容太少了!
Spring Security 是什么样的?我大概能干什么?
在这里我先简单的把 ss 引入先有的 spring 生态,ss 是基于 servlet 的 filter 来实现的,与 spring mvc 集成在一起。
上图简单的描述了 ss 的架构,我们可以创建很多个过滤器来认证,鉴权,保护 web 应用。
- authentication filter 是进入 ss 的入口,同时也是出口。在入口它会将 httpRequest 转变为 ss 中的输入对象,在出口会将认证结果记录到 security context 中。
- authentication manager 会把身份认证委托给 authentication provider 组件。
- authentication provider 会针对特定的认证方式,实现具体的身份验证逻辑,并且它把与用户相关的和密码相关的操作分解到 user detial service 和 password encoder 两个组件上。
- security context 会记录用户的认证信息,它会使用 cookie 等技术记住用户的请求。
覆盖默认配置
ss 默认提供 http basic 认证,默认用户名为 user, 启动项目时会打印对应的密码,并且默认所有请求都必须得到认证。
当我们想要提供新的认证方式时需要提供新的 authentication provider,因为它是实现具体认证逻辑的地方。
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
# 关键在这里,每个 authenticationProvider 会告诉 authentication manager
# 它能处理那些认证类型,所以当我们修改认证方式时要覆盖默认实现类,提供特定的实现类
boolean supports(Class<?> authentication);
}
由于 ss 将用户管理和密码管理分离成出来了,所以我们也可以只替换 user details serivce 或 password encoder。
Tip: 当替换默认配置的 user details service 时,必须也把 password encoder 替换了。
覆盖默认配置:
- 当我们在配置类中注入 user details serivce 或 password encoder Bean 时,其会自动覆盖掉默认配置
- 当我们想要修改认证逻辑或认证类型时需要通过注入 SecurityFilterChain 实现
# 注入自定义 AuthenticationProvider @Bean CustomAuthenticationProvider customAuthenticationProvider() { return new CustomAuthenticationProvider(); } @Bean SecurityFilterChain configure(HttpSecurity http) throws Exception { # HttpSecurity 几乎可以做任何事情!! # 设置认证类型 http.httpBasic(Customizer.withDefaults()); # 设置自定义 AuthenticationProvider http.authenticationProvider(customAuthenticationProvider()); # ..... http.authorizeHttpRequests(c -> c.anyRequest().authenticated()); return http.build(); } # 这里有必要介绍一下 Customizer 接口,它其实就是个回调对象 @FunctionalInterface public interface Customizer<T> { void customize(T t); static <T> Customizer<T> withDefaults() { return (t) -> { }; } }
- 在端点级别控制授权
@Bean SecurityFilterChain configure(HttpSecurity http) throws Exception { http.httpBasic(Customizer.withDefaults()); http.authenticationProvider(customAuthenticationProvider()); # 该方法也接收一个 Customizer 回调,用于在端点级别控制授权 http.authorizeHttpRequests(c -> c.anyRequest().authenticated()); return http.build(); }
管理用户
ss 将用户相关操作分离成了独立的组件,并且制定了大量的接口,其中 user details service 和 user details manager 实现用户相关操作的具体逻辑,user details 是 ss 定义的用户接口,Granted authority是 ss 定义的权限接口
Tips: 我们为用户配置的 role,会被添加 ROLE_ 前缀后,存储在用户权限中。我们甚至可以通过添加带有 ROLE_ 前缀的权限来为用户添加权限角色。
这些接口都很简单,一眼就可以看出其作用,那就在此一一介绍一下:
- UserDetails
// 在 ss 眼中的用户模型 public interface UserDetails extends Serializable { String getUsername(); #A String getPassword(); Collection<? extends GrantedAuthority> getAuthorities(); #B // 我们可以实现这些功能,也可以简单的返回 true boolean isAccountNonExpired(); #C boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
- GrantedAuthority
public interface GrantedAuthority extends Serializable { // 返回权限标识 String getAuthority(); }
- UserDetailsService
public interface UserDetailsService { // 认证最核心的操作 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
- UserDetailsManager
// 将用户管理,与安全框架耦合在一切好吗? // 感觉独立成用户模块来管理更好呢! public interface UserDetailsManager extends UserDetailsService { void createUser(UserDetails user); void updateUser(UserDetails user); void deleteUser(String username); void changePassword(String oldPassword, String newPassword); boolean userExists(String username); }
这些接口很简单,可以很容易的通过扩展 UserDetailsService 来实现自己的用户认证,甚至通过 UserDetails 实现用户状态管控功能。
可以通过 User 类来快速构建 UserDetail 实例
UserDetails u1 = User.withUsername("ivan")
.password("123456")
.authorities("read")
.roles("admin") // 会转换为 ROLE_admin 添加到用户权限中
.build();
密码管理
Spring Security 认为直接把密码存储起来不安全,应该加密后再存储。
PasswordEncoder 提供了更加便捷的加密操作和对比明文与秘文操作。
并且 Spring Security 的 Crypto 模块,提供了一定的加密功能。
PasswordEncoder
public interface PasswordEncoder {
// 加密
String encode(CharSequence rawPassword);
// 对比
boolean matches(CharSequence rawPassword, String encodedPassword);
// 如果返回true,会对加密结果再进行一次加密
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
Spring Security 还提供了一些具体的实现如下:
Pbkdf2PasswordEncoder:
PasswordEncoder p = New Pbkdf2PasswordEncoder(“secret”, 16, 310000,
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
// PBKDF2WithHmacSHA1
// PBKDF2WithHmacSHA256
// PBKDF2WithHmacSHA512
BCryptPasswordEncoder:
PasswordEncoder p = new BCryptPasswordEncoder();
PasswordEncoder p = new BCryptPasswordEncoder(4); // 4 ~ 32
SecureRandom salt = SecureRandom.getInstanceStrong();
PasswordEncoder p = new BCryptPasswordEncoder(4, salt);
SCryptPasswordEncoder:
PasswordEncoder p = new SCryptPasswordEncoder(163845, 8, 1, 32, 64)
做好的设计就是最容易变化的设计,加密方式也在不断的淘汰,进化。ss 给我提供了一个特殊的实现 DelegatingPasswordEncoder 来应对这种变化,使得我们可以自主选择加密方式来对用户密码进行保护。
DelegatingPasswordEncoder 在加密时会根据密码前缀把加密任务委托给对应的机密器,在对比时也根据密码前缀把对比任务委托出去。
1. 构建 DelegatingPasswordEncoder
// 创建加密前缀与加密器的映射关系
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
return new DelegatingPasswordEncoder("bcrypt", encoders);
2. 加密
String pwd = "123456";
String encode = delegatingPasswordEncoder.encode("{noop}" + pwd);
3. 比对
delegatingPasswordEncoder.matches("{noop}" + pwd, encode);
Spring Security Crypto 模块
ss 加密模块主要提供两类工具密钥生成器和加密器
生成密钥
主要分为字符串和字节数组两种类型的密钥生成器
String:
StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey(); // 默认 8 字节,编码为 16 进制字符串
Bytes:
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
byte [] key = keyGenerator.generateKey(); // 默认每次生成的不一样
int keyLength = keyGenerator.getKeyLength();
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(16); // 设置字节长度
BytesKeyGenerator keyGenerator = KeyGenerators.shared(16); // 每次生成一样的结果
byte [] key1 = keyGenerator.generateKey();
byte [] key2 = keyGenerator.generateKey(); // key1 == key2
Filter Chain
这本书对于 Security Filter 谈论的非常少!
脑海中最先冒出来的就是多个验证过滤器同时存在会怎么样?过滤器的执行顺序?过滤器处于SpringMvc的那部分?默认提供那些过滤器......
Security Filter 存在哪?
其基于 servletFilter 实现,但是并不是 ServletFilter 如下图:
SecurityFilter 被套娃在了一个 servletFilter 内!
默认过滤器,过滤器顺序?
TIP: 有很多默认过滤器,但只需知道关键的几个过滤器来辅助添加过滤器即可!
启动应用后会按照顺序打印存在的所有过滤器!
TIP: 可添加过滤器可以在某个过滤器之前或之后,以及同一位置。Spring security 不保证同一位置的过滤器执行顺序!
我们无法改变默认过滤器的顺序,但可以在任意的位置添加过滤器,所以过滤器的相对顺序是不会改变的。
多个验证过滤器不起冲突吗?
当然不会,所有过滤器共享同一个 context,通过检查上下文中的验证事件对象 Authentication , 就可以解决一切事端了。
自定义过滤器
# 实现 Filter 接口
public class RequestValidationFilter
implements Filter { #A
@Override
public void doFilter(
ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain)
throws IOException, ServletException {
// ...
}
}
# 添加过滤器
http.addFilterBefore(newFilter, positionFilter)
http.addFilterAfter(newFilter, positionFilter)
http.addFilterAt(newFilter, positionFilter)
实现认证
Spring security 提供的认证框架大多都是基于前后端不分离模型,而本节介绍的也都是如此,所以我在一下主要介绍一下如果自定义认证过程,如何使用 SecurityContext
A Filter 的责任是把请求中认证相关的数据提取到一个对象中,把它叫做认证对象,来作为 ss 框架认证的输入。
A Manager 的责任是根据输入把认证任务委托给对应的 A Provider。
A Provider 的责任是根据认证对象来认证是否通过认证。
Security Context 的责任是作为同一 Filter Chain 的上下文来保存认证结果!
以下是要讨论的细节:
- 认证对象是什么?
- A Manager 如何进行认证委托的?
- 认证结果是什么,有什么用?
- 认证后,后续登录还需要再次认证吗?
- 自定义认证流程示例
认证对象是什么?
无论何种认证方式,本质上都是信息对比!
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
// 在认证完毕后最好删除密码相关信息
Object getCredentials();
Object getDetails();
// 该方法返回对象主体,往往返回 UserDetails
Object getPrincipal();
// ss 表示认证对象是否认证成功
boolean isAuthenticated();
// 为了安全考量,该方法最好禁止设置 truer,通过构造函数来直接构建已授权的认证对象
void setAuthenticated(boolean isAuthenticated)
throws IllegalArgumentException;
}
在自定义认证对象时,越简单越明了,尽量不要实现以下接口!
ss 也提供了大量的具体实现,他们都实现了另一个接口:
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
......
// 关键方法
public String getName() {
if (this.getPrincipal() instanceof UserDetails) {
return ((UserDetails)this.getPrincipal()).getUsername();
} else if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
return ((AuthenticatedPrincipal)this.getPrincipal()).getName();
} else if (this.getPrincipal() instanceof Principal) {
return ((Principal)this.getPrincipal()).getName();
} else {
return this.getPrincipal() == null ? "" : this.getPrincipal().toString();
}
}
......
}
// 该接口的目的是在验证结束后清除敏感信息,被 ss 框架内部使用
public interface CredentialsContainer {
void eraseCredentials();
}
具体实现比如 UsernamePasswordAuthenticationToken......
A Manager 如何进行认证委托的?
A Manager 的常用实现为 ProviderManager 类,针对该类探讨委托具体过程。
可以简单的把认证对象看作一把🔓,AuthenticationProvider 看作各种开锁工具, AuthenticationMananger 看作开锁匠。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
// 用于查看是否支持该🔓类型
boolean supports(Class<?> authentication);
}
ProviderManager 委托过程:
-
通过构造函数,注册 AuthenticationProvider
-
将连续尝试 AuthenticationProvider 列表,直到 AuthenticationProvider 指示它能够验证传递的 Authentication 对象的类型。然后将尝试使用该 AuthenticationProvider 进行身份验证。
如果多个 AuthenticationProvider 支持传递的 Authentication 对象,则第一个能够成功验证(返回打开的🔓,即 isAuthentication() 返回 true 的 Authentication 对象) Authentication 对象的对象将确定 result ,并覆盖早期支持 AuthenticationProvider 抛出的任何可能的 AuthenticationException 。身份验证成功后,不会尝试后续的 AuthenticationProvider 。如果任何支持 AuthenticationProvider 的身份验证均未成功,则将重新抛出最后抛出的 AuthenticationException 。
-
成功后将 Authentication 或异常抛给 Filter
认证结果是什么,有什么用?
认证结果就是打开的🔓,即 isAuthentication() 返回 true 的 Authentication 对象。
TIP: SecurityContextHolder 存在 3 种上下文管理方式:
- (default)MODE_THREADLOCAL 允许每个线程将自己的详细信息存储在安全上下文。在每个请求一个线程的 Web 应用程序中,这是一种常见的方法,因为每个请求都有一个单独的线程。
- MODE_INHERITABLETHREADLOCAL 与MODE_THREADLOCAL 类似,但是如果是异步方法,还指示 Spring Security 将安全上下文复制到下一个线程。
- MODE_GLOBAL 使应用程序的所有线程看到相同的内容
认证结果会被存储到全部 filter 共享的 SecurityContext 上下文中,它有一下几个作用:
- 告诉其他 filter,该请求通过认证了
- 供 cotroller 使用登录的用户信息
1. 通过全局的 SecurityContextHolder 来获取 @GetMapping("/hello") public String hello() { SecurityContext context = SecurityContextHolder.getContext(); Authentication a = context.getAuthentication(); return "Hello, " + a.getName() + "!"; } 2. 通过参数获取 @GetMapping("/hello") public String hello(Authentication a) { #A return "Hello, " + a.getName() + "!"; }
ss 是否默认提供 cookie 功能? 即下次登录是否还需要认证?
默认提供了 cookie 机制来维持用户的会话状态!
添加的自定义认证过滤器,要首先查看一下 context,看看当前请求是否已经认证完毕。因为cookie过滤器一定在认证过滤器之前进行认证。
自定义认证过滤器
注意构建每个组件,完成各自的责任
自定义认证对象(🔓)
public class CustomerAuthentication implements Authentication {
private UserDetails userDetails;
private Boolean authenticated;
private String details;
public CustomerAuthentication(UserDetails userDetails, String sender, Boolean authenticated) {
this.authenticated = authenticated;
this.userDetails = userDetails;
// Http sender 字段
this.details = sender;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return userDetails.getAuthorities();
}
@Override
public Object getCredentials() {
return userDetails.getPassword();
}
@Override
public Object getDetails() {
return details;
}
@Override
public Object getPrincipal() {
return userDetails;
}
@Override
public boolean isAuthenticated() {
return authenticated;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Can't set authenticated!");
}
this.authenticated = isAuthenticated;
}
@Override
public String getName() {
return userDetails.getUsername();
}
}
自定义认证过滤器
public class CustomerFilter extends OncePerRequestFilter {
private AuthenticationManager authenticationManager;
public CustomerFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String name = request.getParameter("name");
String pwd = request.getParameter("pwd");
String sender = request.getHeader("sender");
UserDetails build = User.withUsername(name)
.password(pwd)
.authorities("!")
.build();
CustomerAuthentication customerAuthentication = new CustomerAuthentication(build, sender, false);
Authentication authenticate = authenticationManager.authenticate(customerAuthentication);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authenticate);
filterChain.doFilter(request, response);
}
}
自定义开锁工具
public class CustomerProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
public CustomerProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UserDetails principal = (UserDetails) authentication.getPrincipal();
if (userDetailsService.loadUserByUsername(authentication.getName()) == null) {
throw new UsernameNotFoundException("Can't found user " + authentication.getName());
}
if (!passwordEncoder.matches(authentication.getCredentials().toString(), userDetailsService.loadUserByUsername(authentication.getName()).getPassword())) {
throw new AuthenticationCredentialsNotFoundException("Compare false");
}
if (authentication.getDetails() == null || authentication.getDetails().equals("")) {
throw new UsernameNotFoundException("Can't find sender!");
}
return new CustomerAuthentication(principal, authentication.getDetails().toString(), true);
}
@Override
public boolean supports(Class<?> authentication) {
return CustomerAuthentication.class.isAssignableFrom(authentication);
}
}
配置类
@Configuration
public class ProjectConfig {
@Bean
@Order(1)
public UserDetailsService userDetailsService() {
UserDetails build = User.withUsername("ivan")
.password("123456")
.authorities("read")
.build();
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(build);
return inMemoryUserDetailsManager;
}
@Bean
@Order(2)
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
@Order(3)
public AuthenticationProvider authenticationProvider() {
return new CustomerProvider(userDetailsService(), passwordEncoder());
}
@Bean
@Order(4)
public AuthenticationManager authenticationManager() {
return new ProviderManager(authenticationProvider());
}
@Bean
@Order(5)
public Filter filter() {
return new CustomerFilter(authenticationManager());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(filter(), BasicAuthenticationFilter.class)
.authorizeRequests(c -> c.anyRequest().authenticated());
return http.build();
}
}
配置端点级授权:限制访问
授权是应用程序决定是否允许经过身份验证的请求的过程。授权总是在身份验证之后发生
提取端点,设置权限的方式有很多,一下分别进行介绍:
mvcMathcers 针对 springmvc 实现,可以识别出 /hello == /hello/,会对相等的 url 进行一致的权限控制,而 antMathcers, regexMatchers,则是对 url 的硬编码。
- requestMatchers:
requestMatchers 是一种更通用的方法,它允许你传递一个或多个 RequestMatcher 对象,用于匹配请求。这些 RequestMatcher 可以使用不同的匹配条件,如 URL、HTTP 方法、Headers 等。你可以自定义 RequestMatcher 实现来定义特定的匹配逻辑。 - antMatchers:
antMatchers 允许你使用 Ant 风格的路径模式来匹配请求。Ant 风格的路径模式类似于正则表达式,但更简单。你可以使用通配符 ? 匹配单个字符,* 匹配零个或多个字符,以及 ** 递归匹配零个或多个目录 - regexMatchers:
regexMatchers 允许你使用正则表达式来匹配请求的路径。这种方式更灵活,可以实现更复杂的匹配逻辑,但也更复杂. - mvcMathcers:
mvcMatchers 方法是针对 Spring MVC 控制器的映射路径进行匹配的。它与 antMatchers 和 regexMatchers 相似,但是更加针对 Spring MVC 的控制器方法。使用 mvcMatchers 可以更方便地与 Spring MVC 控制器的路径模式进行匹配,而无需使用硬编码的路径字符串。
设置权限:
- hasRole()
- hasAnyRole()
- hasAuthority()
- hasAnyAuthority()
- access("spel 表达式")
- denyAll()
- permitAll()
TIP: 在设置端点授权时,必须先设置请求路径范围小的配置,再设置范围大的配置,比如 anyRequest 相关配置应该再最后配置
@Configuration
public class ProjectConfig {
// Omitted code
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.requestMatchers(new AntPathRequestMatcher("/hr/**")).hasRole("MANAGER")
.requestMatchers(new AntPathRequestMatcher("/product/{code:^[0-9]*$}")).permitAll()
.mvcMatchers("/hello").hasRole("ADMIN")
.mvcMatchers("/hello").hasRole("ADMIN")
.regexMatchers(".*/(us|uk|ca)+/(en|fr).*").authenticated()
.anyRequest().hasRole("ADMIN")
.anyRequest().hasAnyRole("ABC")
.anyRequest().access("hasAuthority('WRITE')")
.anyRequest().hasAuthority("READ")
.anyRequest().hasAnyAuthority("READ")
.anyRequest().denyAll()
);
return http.build();
}
}
CSRF
csrf 是通过利用用户的登录状态,来进行恶意操作。但是它只对 cookie 会话有效!
如果我们使用 jwt 来维护 http 状态,csrf 根本无法利用用户的登录状态,因为 jwt 请求头没有 cookie 的自携带特性。
如果不使用 cookie,还有必要设置 csrf 防护吗?我感觉没必要!
即使没必要,也来看看 ss 如何实现 csrf 防护的。
ss 使用 csrf-token 来为用户提供一个新令牌,客户端在发起异化操作时,必须携带该令牌。由于那些恶意代码无法获取 csrf-token,所以可以起到防护的作用。
所以主要的逻辑就是要验证令牌,以及创建新令牌:CsrfFilter 在认证过滤器之前,目的就是为请求生成 csrf-token,并在每次请求中进行比对,如果比对失败会返回 403.
该过滤器主要分为 3 大组件:
- CsrfTokenRepository 接口,用于创建,保存,获取 Token,属于工具类。
- CsrfTokenRequestHandler 接口,是 csfr 防护的核心逻辑。
- CsrfToken 接口,表示 token 主体。
public interface CsrfToken extends Serializable {
String getHeaderName(); // 设置 csrf-token 所处的请求头名
// 当成功验证 csrftoken 后,会把该对象存储到 request 中,供后面的 filter 使用,在此设置其在request 中的属性名
String getParameterName();
// csrf-token
String getToken();
}
默认实现,DefaultCsrfToken
public interface CsrfTokenRepository {
CsrfToken generateToken(HttpServletRequest request);
void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
CsrfToken loadToken(HttpServletRequest request);
}
自定义 csrf 防护
@Getter @Setter @ToString
public class Token {
private Long id;
private String identify;
private String token;
}
// 通过 mybatis 把 token 存到数据库中
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
@Resource
private TokenMapper tokenMapper;
@Override
public CsrfToken generateToken(HttpServletRequest request) {
String uuid = UUID.randomUUID().toString();
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
}
@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
// 假设每个用户都会发送该头部,来标识自己的身份
String identify = request.getHeader("X-IDENTIFIER");
Token tokenBYIdentifier = tokenMapper.findTokenBYIdentifier(identify);
if (tokenBYIdentifier != null) {
tokenBYIdentifier.setToken(token.getToken());
tokenMapper.updateToken(tokenBYIdentifier);
} else {
Token token1 = new Token();
token1.setToken(token.getToken());
token1.setIdentify(identify);
tokenMapper.saveToken(token1);
}
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
String identify = request.getHeader("X-IDENTIFIER");
if (identify == null) {
return null;
}
Token tokenBYIdentifier = tokenMapper.findTokenBYIdentifier(identify);
if (tokenBYIdentifier == null) {
return null;
}
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", tokenBYIdentifier.getToken());
}
}
config:
@Bean
public CsrfTokenRepository customCsrfTokenRepository() {
return new CustomCsrfTokenRepository();
}
@Bean
public SecurityFilterChain config(HttpSecurity http) throws Exception {
http.csrf(
c -> {
c.csrfTokenRepository(customCsrfTokenRepository());
}
);
http.addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class)
.authorizeRequests(
c -> c.anyRequest().permitAll()
);
return http.build();
}
CORS
注解:
@CrossOrigin({"example.com","example.org"}).
配置:
http.cors(c -> {
CorsConfigurationSource source = request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(
List.of("example.com", "example.org"));
config.setAllowedMethods(
List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
return config;
};
c.configurationSource(source);
})
全局方法安全性:预授权和后授权
基于 Spring AOP 实现的方法级权限控制
这些权限验证操作,完全可以在 coroller, service 中实现,但是这样就把业务和授权混杂在一起了,业务就无法复用了!因为权限框架可能随时会变化!
ss 提供通过方法注释来对方法的执行进行权限控制功能,它提供了多种注解类型,以下介绍如何开启,使用这些注解:
-
开启全局方法安全性
// ss 提供了 3 中注解类型: prePostEnabled(常用), securedEnabled,jsr250Enabled @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启指定类型的注解 class ProjectConfig { ... }
-
使用 preostEnable 类型注解,其包含 @PreAuthorize, @PostAuthorize 两个注解,类似 assess 权限设置方法,都接收 SpEL 表达式作为参数。
TIPS: 在 SpEL 中有些内置对象 returnObject,authentication, 以及内置语法 #argName
@PreAuthorize("hasAnyAuthority('admin', 'market')") // 内置 returnObject,引用方法的返回值 @PostAuthorize("returnObject.targetUser.contains(authentication.name)") // 使用 #argName 语法,引用方法参数 @PreAuthorize("#name == authentication.principal.username") /** 当权限控制非常复杂时,可以使用 PermissionEvaluator 接口, @PreAuthorize(“@PermissionEvaluator实例.hasPermission('')”) 我们只需将实现该接口的实例注入,然后通过 @实例名 语法在 SpEL 调用即可! */ // 该接口的目的就是为了把那些复杂权限验证的操作从 SpEL 中分离出来 public interface PermissionEvaluator extends AopInfrastructureBean { // 最好在 PostAuthorize 中调用该函数 // targetDomainObject 接收 returnObject // permission 接收操作的权限要求 boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission); // 在 PreAuthorize 中调用该函数 // targetId 接收请求参数 // targetType 接收目标类型(我也不知道它的真实作用) // permission 接收操作的权限要求 boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission); }
全局方法安全性:预过滤和后过滤
假设您不想禁止对某个方法的调用,但您想确保发送给该方法的参数遵循某些规则。或者,在另一种情况下,您希望确保在有人调用该方法后,该方法的调用者仅收到返回值的授权部分。我们将这种功能命名为过滤,并将其分为两类:
- Prefiltering: 框架在调用之前过滤参数的值
- Postfiltering: 框架在调用之后过滤返回值
TIPS: 只能过滤数组,集合对象!!
本文作者:ivanohohoh
本文链接:https://www.cnblogs.com/ivanohohoh/p/17608586.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步