搭建环境
导入依赖
<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 实例是否存在,存在则不注入,没有就注入
流程分析
- 请求 /hello 接⼝,在引⼊ spring security 之后会先经过⼀些列过滤器
- 在请求到达 FilterSecurityInterceptor时,发现请求并未认证。请求拦截下来,并抛出 AccessDeniedException 异常
- 抛出 AccessDeniedException 的异常会被 ExceptionTranslationFilter 捕获,这个 Filter 中会调⽤ LoginUrlAuthenticationEntryPoint#commence⽅法给客户端返回 302,要求客户端进⾏重定向到 /login ⻚⾯。
- 客户端发送 /login 请求。
- /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
总体流程