一、重要的类
源码讲解
SecurityContextPersistenceFilter
作用
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// ensure that filter is only applied once per request
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
//每次请求开始,先从session中取认证信息
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
//取到后,放入ThreadLocal中
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger
.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
//放行,执行后续Filter和目标方法
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
//清空ThreadLocal认证信息
SecurityContextHolder.clearContext();
//认证信息存入session(之前就有,也就是覆盖一道)
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//判断是不是登录请求/login && POST,不是直接放行
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//登录成功,把认证信息存入session中
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
FilterSecurityInterceptor
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//调用目标方法前,校验是否有权限,没有权限,直接抛AccessDeniedException异常,异常Filter捕获到后,会进行相应处理
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
过滤器链
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
- 为什么引入spring-boot-starter-security,springboot项目所有资源都被保护起来了呢?
WebSecurityConfiguration
创建bean:springSecurityFilterChain
SpringBootWebSecurityConfiguration
- 默认的自动配置类,给我们配置好了认证授权规则,即所有请求都需要认证授权
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
/**
springboot默认帮我们配置好的:
所有请求都需要认证,可以是formLogin认证,httpBasic基本是不用了
*/
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
return http.build();
}
}
@ConditionalOnDefaultWebSecurity
WebSecurityConfigurerAdapter
- 如果我们需要自定义管理认证授权,我们可以重写WebSecurityConfigurerAdapter,使得默认配置类不生效
UserDetailsServiceAutoConfiguration
- 自动注入 InMemoryUserDetailsManager,基于内存的认证
UserDetailsService
- 认证数据源
二、认证
自定义资源认证规则
@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").permitAll() //放行/hello,必须在.anyRequest()之前
.anyRequest().authenticated() //其余请求需要认证
.and().formLogin() //表单登录进行认证
.and().httpBasic();
}
}
自定义登录成功处理(前后端分离)
/*
登录成功处理
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = MapUtil.of("code", "0000");
map.put("authentication", authentication);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
}
显示登录错误信息
原理:AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication
<h4>
<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></span>
</h4>
自定义登录失败(前后端分离)
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String, Object> map = MapUtil.of("code", "9999");
map.put("msg", "认证失败,用户名或密码错误");
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
}
注销登录配置
默认是 /loginout
登录成功后获取用户信息
spring security 登录成功后,会将用户信息保存在session中,还有线程绑定,
默认是单线程中获取,不能在子线程中获取
-
SecurityContextHolder
-
SecurityContextHolderStrategy
代码中获取
@RequestMapping("/hello")
public Object hello(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User principal = (User) authentication.getPrincipal();
new Thread(() -> {
Authentication authentication2 = SecurityContextHolder.getContext().getAuthentication();
User principal2 = (User) authentication2.getPrincipal();
log.info("principal2:{}", principal2);
}).start();
return authentication;
}
子线程中也能获取
,需要改变配置:
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
页面中获取
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
- 页面加入命名空间、使用
<html lang="en" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!--获取认证用户名-->
<ul>
<li sec:authentication="principal.username"></li>
<li sec:authentication="principal.authorities"></li>
<li sec:authentication="principal.accountNonExpired"></li>
<li sec:authentication="principal.accountNonLocked"></li>
<li sec:authentication="principal.credentialsNonExpired"></li>
</ul>