Spring Security 02 搭建环境

news/2024/12/28 22:36:58/

搭建环境

导入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>

编写 Controller

@RestController
public class HelloController {@GetMapping("/hello")public String hello() throws JsonProcessingException {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();Object principal = authentication.getPrincipal();return new ObjectMapper().writeValueAsString(principal);}}

启动项目

访问:

http://localhost:8080/hello,页面会跳转到

http://localhost:8080/login 进行登入

- 默认⽤户名为: user
- 默认密码为: 控制台打印的 uuid 

 

这就是 Spring Security 的强⼤之处,只需要引⼊⼀个依赖,所有的接⼝就会⾃动保护起来!思考 ?

  • 为什么引⼊ Spring Security 之后没有任何配置所有请求就要认证呢?
  • 在项⽬中明明没有登录界⾯, 登录界⾯ 怎么来的呢?
  • 为什么使⽤ user 和 控制台密码 能登陆,登录时验证数据源存在哪⾥呢?

实现原理

Architecture :: Spring Security

Spring Security的Servlet支持是基于Servlet过滤器的客户端向应用程序发送一个请求,容器创建一个FilterChain,其中包含Filter实例和Servlet,应该根据请求URI的路径来处理HttpServletRequest。在Spring MVC应用程序中,Servlet 是 DispatcherServlet 的一个实例。

由于一个Filter只影响下游的Filter实例和Servlet,所以每个Filter的调用顺序是非常重要的。

Spring 提供了一个名为 Delegating FilterProxy 的过滤器实现,允许在Servlet容器的生命周期和 Spring 的ApplicationContext 之间建立桥梁。Servlet容器允许通过使用自己的标准来注册 Filter 实例,但它不知道 Spring 定义的 Bean。你可以通过标准的 Servlet 容器机制来注册 DelegatingFilterProxy,但将所有工作委托给实现 Filter的 Spring Bean。

DelegatingFilterProxy 的另一个好处是,它允许延迟查找 Filter Bean实例。这一点很重要,因为在容器启动之前,容器需要注册Filter实例。然而,Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,这在需要注册Filter 实例之后才会完成。

 

Servlet 容器允许使用自己的标准来注册Filter实例,自定义过滤器并不是直接放在 Web 项⽬的原⽣过滤器链中,⽽是通过⼀个 FlterChainProxy 来统⼀管理。 Spring Security 中的过滤器链通过 FilterChainProxy 嵌⼊到 Web 项⽬的原⽣过滤器链中。 FilterChainProxy 作为⼀个顶层的管理者,将统⼀管理 Security Filter。

FilterChainProxy 本身是通过 Spring 框架提供的 DelegatingFilterProxy 整合到原⽣的过滤器链中。

 

servlet 与 spring 之间的联系:

https://www.cnblogs.com/shawshawwan/p/9002126.html

为什么不直接注册到 Servlet 容器 或者 DelegatingFilterProxy ?

SecurityFilterChain 中注册的是 Bean,这些 Bean 是注册在 FilterChainProxy 中的,相对于直接注册到 Servelt 容器 或者 DelegatingFilterProxy,FilterChainProxy提供了许多优势:

  • 它为 Spring Security 的所有 Servlet 支持提供了一个起点,方便代码调试
  • 由于 FilterChainProxy 是 Spring Security 使用的核心,它可以执行一些不被视为可有可无的任务
  • 它在确定何时应该调用 SecurityFilterChain 方面提供了更大的灵活性。在 Servlet 容器中,Filter 实例仅基于 URL 被调用。然而,FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用

源码解析

SpringBootWebSecurityConfiguration

这个类是 spring boot ⾃动配置类,通过这个源码得知,默认情况下对所有请求进⾏权限控制:


/*** {@link Configuration @Configuration} class securing servlet applications.** @author Madhura Bhave*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {/*** The default configuration for web security. It relies on Spring Security's* content-negotiation strategy to determine what sort of authentication to use. If* the user specifies their own {@code WebSecurityConfigurerAdapter} or* {@link SecurityFilterChain} bean, this will back-off completely and the users* should specify all the bits that they want to configure as part of the custom* security configuration.*/@Configuration(proxyBeanMethods = false)@ConditionalOnDefaultWebSecuritystatic class SecurityFilterChainConfiguration {@Bean@Order(SecurityProperties.BASIC_AUTH_ORDER)SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated();http.formLogin();http.httpBasic();return http.build();}}/*** Adds the {@link EnableWebSecurity @EnableWebSecurity} annotation if Spring Security* is on the classpath. This will make sure that the annotation is present with* default security auto-configuration and also if the user adds custom security and* forgets to add the annotation. If {@link EnableWebSecurity @EnableWebSecurity} has* already been added or if a bean with name* {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been configured by the user, this* will back-off.*/@Configuration(proxyBeanMethods = false)@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)@ConditionalOnClass(EnableWebSecurity.class)@EnableWebSecuritystatic class WebSecurityEnablerConfiguration {}}

这就是为什么在引⼊ Spring Security 中没有任何配置情况下,请求会被拦截的原因!

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(DefaultWebSecurityCondition.class)
public @interface ConditionalOnDefaultWebSecurity {}
class DefaultWebSecurityCondition extends AllNestedConditions {DefaultWebSecurityCondition() {super(ConfigurationPhase.REGISTER_BEAN);}@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })static class Classes {}@ConditionalOnMissingBean({org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.class,SecurityFilterChain.class })@SuppressWarnings("deprecation")static class Beans {}}

通过上⾯对⾃动配置分析,我们也能看出默认⽣效条件为:

  • 条件⼀ classpath中存在 SecurityFilterChain.class, httpSecurity.class
  • 条件⼆ 没有⾃定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class

补充说明:

@ConditionalOnClass:当项目中存在他条件中的某个类时才会使标有该注解的类或方法生效;

@ConditionalOnMissingBean:判断 Spring 容器中该 bean 实例是否存在,存在则不注入,没有就注入

流程分析 

 

  1. 请求 /hello 接⼝,在引⼊ spring security 之后会先经过⼀些列过滤器
  2. 在请求到达 FilterSecurityInterceptor时,发现请求并未认证。请求拦截下来,并抛出 AccessDeniedException 异常
  3. 抛出 AccessDeniedException 的异常会被 ExceptionTranslationFilter 捕获,这个 Filter 中会调⽤ LoginUrlAuthenticationEntryPoint#commence⽅法给客户端返回 302,要求客户端进⾏重定向到 /login ⻚⾯。
  4. 客户端发送 /login 请求。
  5. /login 请求会再次被拦截器中 DefaultLoginPageGeneratingFilter 拦截到,并在拦截器中返回⽣成登录⻚⾯。

默认用户生成

1.查看 SecurityFilterChainConfiguration.defaultSecurityFilterChain() ⽅法表单登录

2.处理登录为 FormLoginConfigurer 类中 调⽤ UsernamePasswordAuthenticationFilter 这个类实例

3.查看类中 UsernamePasswordAuthenticationFilter.attempAuthentication() ⽅法得知实际调⽤ AuthenticationManager 中 authenticate ⽅法

 4.调⽤ ProviderManager 类中⽅法 authenticate

5.调⽤了 ProviderManager 实现类中 AbstractUserDetailsAuthenticationProvider 类中⽅法

6.最终调⽤实现类 DaoAuthenticationProvider 类中⽅法⽐较 

 

看到这⾥就知道默认实现是基于 InMemoryUserDetailsManager 这个类,也就是内存的实现!

UserDetailService 

UserDetailService 是顶层⽗接⼝,接⼝中 loadUserByUserName ⽅法是⽤来在认证时进⾏⽤户名认证⽅法,默认实现使⽤是内存实现,如果想要修改数据库实现我们只需要⾃定义 UserDetailService 实现,最终返回 UserDetails 实例即可。

public interface UserDetailsService {/*** Locates the user based on the username. In the actual implementation, the search* may possibly be case sensitive, or case insensitive depending on how the* implementation instance is configured. In this case, the <code>UserDetails</code>* object that comes back may have a username that is of a different case than what* was actually requested..* @param username the username identifying the user whose data is required.* @return a fully populated user record (never <code>null</code>)* @throws UsernameNotFoundException if the user could not be found or the user has no* GrantedAuthority*/UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailServiceAutoConfigutation 

@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,AuthenticationManagerResolver.class },type = { "org.springframework.security.oauth2.jwt.JwtDecoder","org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector","org.springframework.security.oauth2.client.registration.ClientRegistrationRepository","org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {private static final String NOOP_PASSWORD_PREFIX = "{noop}";private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);@Bean@Lazypublic InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,ObjectProvider<PasswordEncoder> passwordEncoder) {SecurityProperties.User user = properties.getUser();List<String> roles = user.getRoles();return new InMemoryUserDetailsManager(User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build());}private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {String password = user.getPassword();if (user.isPasswordGenerated()) {logger.warn(String.format("%n%nUsing generated security password: %s%n%nThis generated password is for development use only. "+ "Your security configuration must be updated before running your application in "+ "production.%n",user.getPassword()));}if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {return password;}return NOOP_PASSWORD_PREFIX + password;}}

结论

  • 从⾃动配置源码中得知当 classpath 下存在 AuthenticationManager 类
  • 当前项⽬中,系统没有提供 AuthenticationManager.class、AuthenticationProvider.class、 UserDetailsService.class、AuthenticationManagerResolver.class实例

默认情况下都会满⾜,此时Spring Security会提供⼀个 InMemoryUserDetailManager 实例

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {private final User user = new User();public User getUser() {return this.user;}public static class User {/*** Default user name.*/private String name = "user";/*** Password for the default user name.*/private String password = UUID.randomUUID().toString();/*** Granted roles for the default user name.*/private List<String> roles = new ArrayList<>();// ...}
}

这就是默认⽣成 user 以及 uuid 密码过程! 另外看明⽩源码之后,就知道只要在配置⽂

件中加⼊如下配置可以对内存中⽤户和密码进⾏覆盖。

spring.security.user.name=root
spring.security.user.password=root
spring.security.user.roles=admin,users

总体流程

 


http://www.ppmy.cn/news/48519.html

相关文章

软件测试之测试名词解释

1. 白盒测试&#xff0c;英文是white-box testing 是通过程序的源代码进行测试而不使用用户界面。这种类型的测试需要从代码句法发现内部代码在算法&#xff0c;溢出&#xff0c;路径&#xff0c;条件等等中的缺点或者错误&#xff0c;进而加以修正。 2. 黑盒测试&#xff0c;英…

19 标准模板库STL之set和multiset

基础知识 1、set是一个自动有序且不含重复元素的容器,内部使用红黑树的平衡二叉索引树的数据结构来实现。向set中插入新元素时,会自动调节二叉树的排列,将元素放到合适的位置。multiset与set不同的地方在于,set内相同数值的元素只能出现一次,multiset内相同数值的元素可以…

CANoe自带的诊断工程分析

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

nodejs+vue 智能餐厅菜品厨位分配管理系统

系统功能主要介绍以下几点&#xff1a; 本智能餐厅管理系统主要包括三大功能模块&#xff0c;即用户功能模块和管理员功能模块、厨房功能模块。 &#xff08;1&#xff09;管理员模块&#xff1a;系统中的核心用户是管理员&#xff0c;管理员登录后&#xff0c;通过管理员功能来…

分布式锁-Redisson

分布式锁 1、分布式锁1.1 本地锁的局限性1.1.1 测试代码1.1.2 使用ab工具测试(单节点)1.1.3 本地锁问题演示(集群情况) 1.2 分布式锁实现的解决方案1.3 使用Redis实现分布式锁(了解即可)1.3.1 编写代码1.3.2 压测 1.4 使用Redisson解决分布式锁1.4.1 实现代码1.4.1 压测1.4.2 可…

【力扣-JZ22】链表中倒数第k个结点

&#x1f58a;作者 : Djx_hmbb &#x1f4d8;专栏 : 数据结构 &#x1f606;今日分享 : "把手插进米堆的原因 " : 因为米堆类似于高密度的流体&#xff0c;会给人的手带来较大的压强&#xff0c;这种压强促进静脉血回流&#xff0c;会让人感到生理上的舒服。 文章目…

android aidl

本文只是记录个人学习aidl的实现&#xff0c;如需学习请参考下面两篇教程 官方文档介绍Android 接口定义语言 (AIDL) | Android 开发者 | Android Developers 本文参考文档Android进阶——AIDL详解_android aidl_Yawn__的博客-CSDN博客 AIDL定义&#xff1a;Android 接口…

简单的小型C++项目怎么用CMAKE进行管理

项目目录&#xff1a; 根目录下共有两个文件夹&#xff0c;分别为include、src&#xff0c;有两个文件&#xff0c;分别为CMakeLists.txt和main.cpp main函数 可以看出&#xff0c;include了func.h&#xff0c;且func.h的声明在include文件夹下&#xff0c;定义在src文件夹下的…