SpringSecurity笔记

SpringSecurity笔记

一. SpringSecurity使用

1. 依赖引入

org.springframework.boot:spring-boot-starter-security:2.2.9.RELEASE
io.jsonwebtoken:jjwt:0.12.6(可选)

2. SecurityConfig配置

2.1. 过滤链配置

Spring Security的一些安全配置,暂不关心

//过滤链配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    http
        .authorizeHttpRequests(authorize -> authorize//开启授权
            .requestMatchers("/login","/api/register")//放行请求
            .permitAll()//允许所有人访问
            .anyRequest()//允许所有请求
            .authenticated()//认证后访问自动授权
        )
        .formLogin(Customizer.withDefaults())//使用默认的登陆登出页面进行授权登陆
        .rememberMe(Customizer.withDefaults());// 启用“记住我”功能的。允许用户在关闭浏览器后,仍然保持登录状态,直到他们主动注销或超出设定的过期时间。
    http.csrf(csrf -> csrf.disable());//关闭csrf
    return http.build();
}

关于放行路径的配置:

如果在 application.properities 中配置的有 server.servlet.context-path=/api 前缀的话,在放行路径中不需要写 /api。

如果 @RequestMapping(value = "/test/") 中写的是 /test/, 那么放行路径必须也写成 /test/, (/test)是不行的,反之亦然。

如果 @RequestMapping(value = "/test") 链接 /test 后面要加查询字符的话(/test?type=0),不要写成 @RequestMapping(value = "/test/")

2.2. 采用基于内存的用户认证

基于内存的方法,直接在SecurityConfig中注册为Bean即可

@Bean
public UserDetailsService userDetailsService() {
	// 创建基于内存的用户信息管理器
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
 
	
    manager.createUser(
	    // 创建UserDetails对象,用于管理用户名、用户密码、用户角色、用户权限等内容
	    User.withDefaultPasswordEncoder().username("user").password("user123").roles("USER").build()
    ); 		
    // 如果自己配置的有账号密码, 那么上面讲的 user 和 随机字符串 的默认密码就不能用了
    return manager;
}

public UserDetailsService userDetailsService()方法中使用了InMemoryUserDetailsManager,显示创建并存储了用户信息,说明对用户信息的相关调用被封装在其内部。

追踪InMemoryUserDetailsManager源码可以得到其继承关系:

interface UserDetailsService
    |
    interface UserDetailsManager  interface UserDetailsPasswordService
        |                               |
        class InMemoryUserDetailsManager

我们来关心它们的实现内容。

//UserDetailsService声明了loadUserByUsername方法,要求根据用户名返回UserDetails类型用户信息。
public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
//UserDetailsPasswordService声明了updatePassword,要求更新UserDetails内的密码为newPassword,通常为系统操作
public interface UserDetailsPasswordService {
   UserDetails updatePassword(UserDetails user, String newPassword);
}
//UserDetailsManager继承自UserDetailsService,在loadUserByUsername方法的基础上,声明了创建,更新,删除,更改密码,及判断存在这五个与用户信息操作相关的方法,此处的changePassword要求校验旧密码后更改为新密码/
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);
}
//此处省去了接口中方法的实现,留下了InMemoryUserDetailsManager单独实现的方法
//无参及有参构造方法,
//createUserDetails用于根据用户信息构建用户,返回封装好的User,
//最会传给createUser,再封装为MutableUser添加到内存用户管理器,保存于private final Map<String, MutableUserDetails> users = new HashMap();
//setSecurityContextHolderStrategy和setAuthenticationManager则是一些配置方法,此处暂不关心
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

   protected final Log logger = LogFactory.getLog(this.getClass());
   private final Map<String, MutableUserDetails> users = new HashMap();
   private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
   private AuthenticationManager authenticationManager;

   public InMemoryUserDetailsManager() {
   }

   public InMemoryUserDetailsManager(Collection<UserDetails> users) {
      Iterator var2 = users.iterator();

      while(var2.hasNext()) {
         UserDetails user = (UserDetails)var2.next();
         this.createUser(user);
      }

   }

   public InMemoryUserDetailsManager(UserDetails... users) {
      UserDetails[] var2 = users;
      int var3 = users.length;

      for(int var4 = 0; var4 < var3; ++var4) {
         UserDetails user = var2[var4];
         this.createUser(user);
      }

   }

   public InMemoryUserDetailsManager(Properties users) {
      Enumeration<?> names = users.propertyNames();
      UserAttributeEditor editor = new UserAttributeEditor();

      while(names.hasMoreElements()) {
         String name = (String)names.nextElement();
         editor.setAsText(users.getProperty(name));
         UserAttribute attr = (UserAttribute)editor.getValue();
         Assert.notNull(attr, () -> {
            return "The entry with username '" + name + "' could not be converted to an UserDetails";
         });
         this.createUser(this.createUserDetails(name, attr));
      }

   }

   private User createUserDetails(String name, UserAttribute attr) {
      return new User(name, attr.getPassword(), attr.isEnabled(), true, true, true, attr.getAuthorities());
   }

   public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
      Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
      this.securityContextHolderStrategy = securityContextHolderStrategy;
   }

   public void setAuthenticationManager(AuthenticationManager authenticationManager) {
      this.authenticationManager = authenticationManager;
   }
}

总结:

InMemoryUserDetailsManager实现了用户信息的封装,并用final Map<String, MutableUserDetails>保存用户于内存,继承一系列接口,实现了其对应的对用户信息的读取创建修改操作

2.3. 采用基于数据库的用户认证

分析过基于内存的方法可知,我们需要的是一个可以实现UserDetailsService的class,通过重写loadUserByUsername方法,即可自定义存储用户信息的位置

这是InMemoryUserDetailsManager中的loadUserByUsername实现,要求返回UserDetails,实际返回new User,即UserDetails的实现类。具体实现内容可自行查看源码

   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
      if (user == null) {
         throw new UsernameNotFoundException(username);
      } else {
         return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
      }
   }

新写类UserDetailsServiceImpl实现UserDetailsService,重写loadUserByUsername

可以注意到最后return new UserDetailsImpl(user)与之前有所不同,后面会有。
需要注册为@Service

//UserDao是DAO层,用于数据库交互,具体使用可以去了解Mybatis。
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
    @Resource
    UserDao userDao;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.查询用户
        User user = userDao.findByName(username);
        // 2.判断用户是否存在
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        //TODO 3.返回UserDetails实现类
        return new UserDetailsImpl(user);	// UserDetailsImpl 是我们实现的类
    }
}

若我们需要自己的用户类型,写一个新的UserDetails实现类即可。此处为UserDetailsImpl

内部引用了用户类型User,实现了UserDetails中要求的方法,若有其他需求也可自行添加,这里不尝试

public class UserDetailsImpl implements UserDetails{
    private User user;	// 通过有参构造函数填充赋值的
 
    public UserDetailsImpl(User user) {
        this.user = user;
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getName();
    }
 
    @Override
    public boolean isAccountNonExpired() {  // 检查账户是否 没过期。
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {   // 检查账户是否 没有被锁定。
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {  //检查凭据(密码)是否 没过期。
        return true;
    }
 
    @Override
    public boolean isEnabled() {    // 检查账户是否启用。
        return true;
    }
    
	public User getUser() {	
	    return user;	
	}
}

总结:

UserDetailsServiceImpl覆写UserDetailsService,自定义用户读取方法,返回UserDetails,注册为Service;

UserDetailsImpl实现UserDetails,内部引用所需User对象。

3. 加密配置

3.1 加密接口

SpringSecurity通过 PasswordEncoder 接口定义了加密相关方法。

public interface PasswordEncoder {
   String encode(CharSequence rawPassword);

   boolean matches(CharSequence rawPassword, String encodedPassword);

   default boolean upgradeEncoding(String encodedPassword) {
      return false;
   }
}

它本身提供了一些实现,如需使用可查看接口实现。
PasswordEncoder实现类

3.2 使用方式

在SecurityConfig.java中注册PasswordEncoder的Bean

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();	//这里选用提供的BCryptPasswordEncoder实现,该实现采用的是BCrypt算法
    }

注意:在你注册PasswordEncoder之后,如果你没有重写它的验证相关实现类,通过SpringSecurity登录会默认进行加密对比。

二. SpringSecurity默认加载的15个过滤器

//0-4 这几个过滤器是 功能性的前置过滤器,提供了SpringSecurity的基础必要能力。
0-DisableEncodeUrlFilter
1-WebAsyncManagerIntegrationFilter
2-SecurityContextHolderFilter
3-HeaderWriterFilter
4-CsrfFilter

//5-14 则与认证和授权过程相关
5-LogoutFilter
6- UsernamePasswordAuthenticationFilter
7-DefaultLoginPageGeneratingFilter
8-DefaultLogoutPageGeneratingFilter
9-BasicAuthenticationFilter
10-RequestCacheAwareFilter
11-SecurityContextHolderAwareRequestFilter
12-AnonymousAuthenticationFilter
13-ExceptionTranslationFilter
14-AuthorizationFilter

1. DisableEncodeUrlFilter

禁止URL重新编码

使用原因:

Session会话会在cookies中保存SessionID,

如果禁用了cookie,后端的默认响应会重写url将sessionId拼接到url后面,这样sessionId就在http访问日志中暴露

public class DisableEncodeUrlFilter extends OncePerRequestFilter {
   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
           throws ServletException, IOException {
      // 对response进行包装后,继续执行后面的过滤器 
      filterChain.doFilter(request, new DisableEncodeUrlResponseWrapper(response));
   }
    
   private static final class DisableEncodeUrlResponseWrapper extends HttpServletResponseWrapper {
      private DisableEncodeUrlResponseWrapper(HttpServletResponse response) {
         super(response);
      }
      @Override
      public String encodeRedirectURL(String url) {
         // 不重新url 直接返回 
         return url;
      }
      @Override
      public String encodeURL(String url) {
         // 不重新url 直接返回 
         return url;
      }
   }
}

2. WebAsyncManagerIntegrationFilter

Web异步处理整合过滤器

默认情况下securityContextHolderStrategy的存储策略为ThreadLocal,在ThreadLocal的存储策略下,只有当前线程可以获取到securityContextHolder。

WebAsyncManagerIntegrationFilter 通过创建拦截器的形式,将securityContextHolderStrategy传递给子线程,后续子线程可以通过该拦截器获取到用户认证信息

public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
	private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();
	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
			.getContextHolderStrategy();
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
        // 尝试获取一个 Object asyncManagerAttr = servletRequest.getAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE);
        // asyncManagerAttr 如果已存在,返回已存在的对象,
        // asyncManagerAttr 为空,并将新对方放到servletRequest.setAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, asyncManager);
		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        
        // 尝试从asyncManager 中获取已经存储的拦截器
		SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
				.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
        // 如果没有获取到
		if (securityProcessingInterceptor == null) {
            // 新建一个拦截器
			SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor();
            // 将SecurityContextHolderStrategy存储到拦截器,供子线程可获取
			interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
            // 注册拦截器放到asyncManager中
			asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, interceptor);
		}
        // 继续后续拦截器执行
		filterChain.doFilter(request, response);
	}
}

3. SecurityContextHolderFilter

持有SecurityContext的过滤器。

该Filter是用于存储用户认证信息的,他有两个重要的属性SecurityContextRepository和securityContextHolderStrategy。

public class SecurityContextHolderFilter extends GenericFilterBean { 
    // SecurityContextRepository接口 提供一种在整个请求上下文存储SecurityContext的能力
    // SecurityContextRepository接口有两个重要方法: loadContext - 获取SecurityContext loadDeferredContext-延期获取SecurityContext saveContext - 保存SecurityContext 
    // 该属性默认为 DelegatingSecurityContextRepository,DelegatingSecurityContextRepository也是实现SecurityContextRepository接口的一个代理类
    // DelegatingSecurityContextRepository允许代理多个SecurityContextRepository来实现SecurityContext的存储
    // DelegatingSecurityContextRepository对 loadContext 和 saveContext 实现
    // 默认被代理的SecurityContextRepository为:HttpSessionSecurityContextRepository 和 RequestAttributeSecurityContextRepository
    // 当调用 DelegatingSecurityContextRepository 时,他会遍历被代理的SecurityContextRepository
    // saveContext时:遍历被代理的SecurityContextRepository 都调用saveContext SecurityContext进行存储
    // loadContext时: 调用自身loadDeferredContext 获取SecurityContext
    // loadDeferredContext时:遍历被代理的SecurityContextRepository 调用loadDeferredContext 获取 SecurityContext
   private final SecurityContextRepository securityContextRepository;
 
   // 在线程中存储SecurityContext的策略
   // 默认都是ThreadLocal。
   private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
           .getContextHolderStrategy();
}

SecurityContextHolderFilter是第三个要执行的Filter,但是他是直接继承于GenericFilterBean。

该过滤器最主要的作用是:

如果请求上下文中存在 SecurityContext 则 SecurityContext存储到securityContextHolderStrategy默认是ThreadLocal

并在整个过滤器链执行完成后清除SecurityContext

存储到securityContextHolderStrategy可以保证后续的过滤器都可以从securityContextHolderStrategy中获取到SecurityContext。

doFilter逻辑:

public class SecurityContextHolderFilter extends GenericFilterBean { 
    @Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}
 
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
        // 如果已经执行过 该过滤器
		if (request.getAttribute(FILTER_APPLIED) != null) {
            // 跳过 直接执行下一个过滤器
			chain.doFilter(request, response);
			return;
		}
        // 标记改过滤器在本次请求过程中已经执行过
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        // 从SecurityContextRepository获取到SecurityContext 
		Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
		try {
            // 将SecurityContext存储到securityContextHolderStrategy中,也就是存储到线程中。
			this.securityContextHolderStrategy.setDeferredContext(deferredContext);
            // 继续执行Filter
			chain.doFilter(request, response);
		}
		finally {
            // 这时应该 后续所有的Filter都已执行完后,有回到当前Filter中
            // 请求执行完成后,清除SecurityContext
			this.securityContextHolderStrategy.clearContext();
            // 移除已执行标记
			request.removeAttribute(FILTER_APPLIED);
		}
	}
}

4. HeaderWriterFilter

头信息写入过滤器

为当前响应添加报头的过滤器实现。可以添加某些头,启用浏览器保护。像X-Frame-Options, X-XSS-Protection和X-Content-Type-Options。

public class HeaderWriterFilter extends OncePerRequestFilter {
    
    // 要写人的头信息
   private final List<HeaderWriter> headerWriters;
   // 默认是false 也就是在过滤器都执行完成后,回到该过滤器时向response中写入
   private boolean shouldWriteHeadersEagerly = false;
 
 
   // 构造方法,需要在构造该过滤器时就传入要写入ResponseHeader的头信息
   // 具体确定要写入那些头信息是由HeadersConfigurer来决定的
   // 默认是以下几个头信息
    // 0 = {XContentTypeOptionsHeaderWriter}  X-Content-Type-Options: nosniff
    // 1 = {XXssProtectionHeaderWriter}  X-XSS-Protection: 0
    // 2 = {CacheControlHeadersWriter}
    //    Header [name: Cache-Control, values: [no-cache, no-store, max-age=0, must-revalidate]]
    //    Header [name: Pragma, values: [no-cache]]
    //    Header [name: Expires, values: [0]]
    // 3 = {HstsHeaderWriter}  Strict-Transport-Security: max-age=31536000 ; includeSubDomains
    // 4 = {XFrameOptionsHeaderWriter}  X-Frame-Options: DENY
   public HeaderWriterFilter(List<HeaderWriter> headerWriters) {
      Assert.notEmpty(headerWriters, "headerWriters cannot be null or empty");
      this.headerWriters = headerWriters;
   }
   
   // 具体的过滤器执行方法
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		if (this.shouldWriteHeadersEagerly) {
            // 如果是提前写入响应头,则是直接调用了writeHeaders 方法,并继续执行过滤器
			doHeadersBefore(request, response, filterChain);
		}
		else {
            // 默认走该方法
            // 再过滤器执行完成后,再写入头信息
			doHeadersAfter(request, response, filterChain);
		}
	}
 
   private void doHeadersBefore(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
           throws IOException, ServletException {
       // 写入头信息
      writeHeaders(request, response);
      // 执行过滤器
      filterChain.doFilter(request, response);
   }
 
   private void doHeadersAfter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
           throws IOException, ServletException {
       // 对Req 和 Resp进行一个包装
      HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request, response);
      HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request, headerWriterResponse);
      try {
          // 先执行后续过滤器
         filterChain.doFilter(headerWriterRequest, headerWriterResponse);
      }
      finally {
         // 执行完后续过滤器,回到该过滤器后,写入响应头信息
         headerWriterResponse.writeHeaders();
      }
   }
 
   void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
      for (HeaderWriter writer : this.headerWriters) {
          // 将头信息写入HttpServletResponse
         writer.writeHeaders(request, response);
      }
   }
}
posted @ 2025-10-09 21:26  Insanial  阅读(5)  评论(0)    收藏  举报