Spring security 基础使用
整合springboot,基本可以说是开箱即用。甚至无需额外设置,只需要引入依赖:
1 | <dependency> |
然后直接运行,即可看到spring security默认的登陆页面。
Spring Security 的相关功能
整个springsecurity的基础功能,可以归纳为三个大部分:
- 认证 (Authenticate)
认证简单来说就是让系统知道你是谁。一般是用前端传过来的username 和password去数据库里进行获取user信息(也可以是别的比如session缓存,jwt token),这样我们就知道了当前的用户是谁。 - 鉴权 (Authorization)
就是在知道你是谁了后,你有哪些权限。 - 检查 (Check)
在知道你是谁并且确认了你的权限后,需要去跟当前要访问的资源进行匹配。匹配成功后,才会放行,否则会阻止访问并且正常会有AccessDeniedException抛出
Spring Security 的组件
过滤器链(SecurityFilterChain)
构成Spring Security的核心功能的,是由SecurityFilterChain过滤器链来实现的。其本质就是一个个一连串的过滤器,进行各自功能的check以及验证。
而在SecurityFilterChain过滤器链内,则是有各个功能自己的组件。
而这个 SecurityFilterChain过滤器链,实际上是由一个:FilterChainProxy 过滤器链代理的类来执行的。
其主要逻辑源码如下:
1 | private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) |
装饰者内部逻辑
1 | public void doFilter(final ServletRequest request, final ServletResponse response) throws IOException, ServletException { |
Security上下文(SecurityContext)
使用Context来保存上下文信息是一个常用的方法,在spring中更是有application context来保存当前应用的上下文。在springsecurity中也不例外,springsecurity有自己的securityContext。其中主要是用来保存用户的认证信息(Authentication)。
在程序中,我们可以通过 SecurityContextHolder 来获取当前线程中的 SecurityContext。
1 | SecurityContextHolder.getContext(); |
其内部是存在threadlocal里的
AuthenticateManager认证管理器
这个是认证步骤的核心逻辑。其主要功能是负责认证一个用户的身份(Authentication).
1 | // AuthenticateManager是一个接口,只有一个方法,目的就是认证身份 |
springsecurity中,我们需要从当前的请求(request)以及响应(response)中,想办法获取user的身份(Authenticate),然后放进AuthenticateManager中认证即可。
参考demo可以看springsecurity自带的UsernamePasswordAuthenticationFilter
1 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) |
问题(坑)
获取
AuthenticationManager问题。描述
关于如果在自己的代码中注入并使用
AuthenticationManager,我搜到资料表示说可以直接注入,但是我使用的springboot版本3.4.4的情况下,直接注入AuthenticationManager会找不到bean报错。意味着spring security并没有把一个默认的AuthenticationManager放到spring context中。
此时,继续按照网上的另一个注入方法:1
2
3
4
5
6
7
8
private AuthenticationConfiguration AuthenticationConfiguration;
// 使用AuthenticationConfiguration 来获取 AuthenticationManager
public AuthenticationManager authenticationManager() throws Exception {
return AuthenticationConfiguration.getAuthenticationManager();
}看起来好像没有问题,并且可以启动成功。但是使用起来还是有些问题。
如果我们在security的config中,配置了AuthenticateProvider,如下:1
2
3public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authenticationProvider(new KAuthenticationProvider()).build();
}然后我们在自己的代码中,使用刚才注册的
AuthenticationManager1
2
3
4
5
6
7
8
9
private AuthenticationManager authenticationManager;
public void check () {
UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.unauthenticated("user","123456");
Authentication result = authenticationManager.authenticate(auth);
}会发现,我们刚才的provider永远也走不到。在进一步debug下,我发现,spring security初始化
AuthenticationManager是分为两步的,这可能也是为什么我们没办法获取spring security默认给我们的authenticateManager 的原因把。其中第一步,是在org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration#httpSecurity中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
HttpSecurity httpSecurity() throws Exception {
LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
this.objectPostProcessor, passwordEncoder);
// 这里会初始化第一个authenticationManager,并且作为AuthenticationConfiguration获取到的manager
// 可以看到这里其实是把这个作为我们后面http的manager的parent manager了
authenticationBuilder.parentAuthenticationManager(authenticationManager());
authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
// @formatter:off
http
.csrf(withDefaults())
.addFilter(webAsyncManagerIntegrationFilter)
.exceptionHandling(withDefaults())
.headers(withDefaults())
.sessionManagement(withDefaults())
.securityContext(withDefaults())
.requestCache(withDefaults())
.anonymous(withDefaults())
.servletApi(withDefaults())
.apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());
// @formatter:on
applyCorsIfAvailable(http);
applyDefaultConfigurers(http);
return http;
}这里的是第一个初始化的AuthenticateManager,作为我们后面那一个的parent。然后在我们配置Secuirty的config的时候,会生成第二个AuthenticateManager:
1
2
3
4
5
6public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.
.authenticationProvider(new KAuthenticationProvider())
...
.build();
}注意,此时我们的Provider给的是当前build出来的第二个AuthenticateManager,是之前那个的child
然后看ProviderManager中,有如下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 如果当前的manager没有成功获取身份信息,则去parent中继续获取
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}可以看到这里其实是在当前判断失败时会去parent那里再去尝试的。
到现在为止,好像一切都还很正常。但是如果我要自己定义一个filter去根据user的token获取user的身份,然后再用我们拿到的
AuthenticationManager去认证的话,就会出现无法认证的情况。因为我们自己获取到的是parent,而我们把provider放到的是child中。直接使用parent的authenticate是无法调用child的方法的。复现
关于
AuthenticationManager无法直接注入,直接把下面的代码放入自己项目即可复现1
2
3
4
5
6
public class SecurityIssue {
private AuthenticationManager authenticationManager;
}无法使用自己的provider
- 首先定义一个自己的authentication
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
36
37
38
39
40
41
42
43
44package com.kz.web.test;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class MyAuthentication implements Authentication {
private boolean authState;
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public Object getCredentials() {
return null;
}
public Object getDetails() {
return null;
}
public Object getPrincipal() {
return null;
}
public boolean isAuthenticated() {
return authState;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
this.authState = isAuthenticated;
}
public String getName() {
return null;
}
}- 然后定义自己的provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.kz.web.test;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public class MyAuthenticateProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 直接给与验证通过
authentication.setAuthenticated(true);
return authentication;
}
public boolean supports(Class<?> authentication) {
System.out.println("MyAuthenticateProvider.supports");
return MyAuthentication.class.isAssignableFrom(authentication);
}
}- 定义自己的filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.kz.web.test;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class SimpleFilter extends OncePerRequestFilter {
AuthenticationManager authenticationManager;
public SimpleFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("SimpleFilter.doFilterInternal");
MyAuthentication myAuthentication = new MyAuthentication();
this.authenticationManager.authenticate(myAuthentication);
filterChain.doFilter(request, response);
}
}- 定义自己的config
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50package com.kz.web.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;
public class SimpleConfig {
private AuthenticationConfiguration authenticationConfiguration;
public AuthenticationManager manager() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login").permitAll() // 登录页放行
.requestMatchers("/public/**", "/error").permitAll() // 明确放行登录页和公共路径
.requestMatchers("/admin/**").hasAuthority("admin") // 需要 ADMIN 角色
.anyRequest().authenticated() // 其他所有路径需要认证
)
.anonymous(anon -> anon
.principal("anonymousUser") // 匿名用户
)
.formLogin(form ->
form.disable()
)
.addFilterAfter(new SimpleFilter(manager()), LogoutFilter.class)
.authenticationProvider(new MyAuthenticateProvider())
.logout(logout -> logout
.logoutUrl("/logout") // 登出URL
.logoutSuccessUrl("/login?logout") // 登出成功后跳转
.permitAll()
);
return http.build();
}
}运行上面的代码会发现自己的provider没有走进去。我下午尝试的时候还出现了循环依赖,AuthenticationManager 自己循环依赖自己。这个问题还没解决。还在看