导入依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
编写 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 的强⼤之处,只需要引⼊⼀个依赖,所有的接⼝就会⾃动保护起来!思考 ?
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 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)
@ConditionalOnDefaultWebSecurity
static 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)
@EnableWebSecurity
static 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 {
}
}
通过上⾯对⾃动配置分析,我们也能看出默认⽣效条件为:
补充说明:
@ConditionalOnClass:当项目中存在他条件中的某个类时才会使标有该注解的类或方法生效;
@ConditionalOnMissingBean:判断 Spring 容器中该 bean 实例是否存在,存在则不注入,没有就注入
1.查看 SecurityFilterChainConfiguration.defaultSecurityFilterChain() ⽅法表单登录
2.处理登录为 FormLoginConfigurer 类中 调⽤ UsernamePasswordAuthenticationFilter 这个类实例
3.查看类中 UsernamePasswordAuthenticationFilter.attempAuthentication() ⽅法得知实际调⽤ AuthenticationManager 中 authenticate ⽅法
4.调⽤ ProviderManager 类中⽅法 authenticate
5.调⽤了 ProviderManager 实现类中 AbstractUserDetailsAuthenticationProvider 类中⽅法
6.最终调⽤实现类 DaoAuthenticationProvider 类中⽅法⽐较
看到这⾥就知道默认实现是基于 InMemoryUserDetailsManager 这个类,也就是内存的实现!
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 UserDetails
* 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 null
)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
@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
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List 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;
}
}
结论
默认情况下都会满⾜,此时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 roles = new ArrayList<>();
// ...
}
}
这就是默认⽣成 user 以及 uuid 密码过程! 另外看明⽩源码之后,就知道只要在配置⽂
件中加⼊如下配置可以对内存中⽤户和密码进⾏覆盖。
spring.security.user.name=root
spring.security.user.password=root
spring.security.user.roles=admin,users