😉😉 欢迎加入我们的学习交流群呀!
✅✅1:这是孙哥suns给大家的福利!
✨✨2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring等等很多应用和源码级别的高质量视频和笔记资料,你想学的我们这里都有!
🥭🥭3:QQ群:583783824 📚📚 工作VX:BigTreeJava 拉你进VX群,免费领取!
🍎🍎4:本文章内容出自上述:SpringSecurity应用课程!💞💞
💞💞5:以上内容,进群免费领取呦~ 💞💞💞💞
我们已经讲过在SpringSecurity这个依赖一旦被SpringBoot引入之后呢,这个jar包中的核心来会被加载,此时这个web服务当中所有的接口都必须要进行认证才能够被请求!我们进行认证时候需要去填写一个默认的user用户名和密码才能够认证通过,那么这个默认的user和控制台密码是如何生成,并保存在了哪里呢?
将这个事呀,我们又得从SpringBoot的自动装配是说起。SpringBoot启动的时候会自动加载一个spring-boot-autoconfigure-3.0.12.jar这么一个jar包。然后加载这个jar包-META-INFO下spring-org.springframework.boot.autoconfigure.AutoConfiguration.imports这样的文件。当我们打开这个文件
看到这么多一坨东西之后,我们就知道,所有在这里边定义的内容,如果Maven中引入了相应jar包之后,都会在SpringBoot启动的时候,自动去进行装配加载。
我们看到里边有一个和SpringSecurity关系十分紧密的这么一个东西,第102行:
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
这就是SpringSecurity的核心配置类。上边有一个十分重要的注解:
@EnableConfigurationProperties({SecurityProperties.class})(加载某个配置类并且让配置类生效)
@Configuration @ConditionalOnClass({DefaultAuthenticationEventPublisher.class}) @EnableConfigurationProperties({SecurityProperties.class}) @Import({SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class}) public class SecurityAutoConfiguration { public SecurityAutoConfiguration() { } @Bean @ConditionalOnMissingBean({AuthenticationEventPublisher.class}) public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) { return new DefaultAuthenticationEventPublisher(publisher); } }
这个注解的作用就是为了让这个注解中的配置类被加载且生效,将这个类SecurityProperties加载到Spring容器当中,我们看下这个类:
@ConfigurationProperties( prefix = "spring.security" ) public class SecurityProperties implements SecurityPrerequisite { public static final int BASIC_AUTH_ORDER = 2147483642; public static final int IGNORED_ORDER = Integer.MIN_VALUE; public static final int DEFAULT_FILTER_ORDER = -100; private final Filter filter = new Filter(); private User user = new User(); public SecurityProperties() { } public User getUser() { return this.user; } public Filter getFilter() { return this.filter; } public static class User { private String name = "user"; private String password = UUID.randomUUID().toString(); private Listroles = new ArrayList(); private boolean passwordGenerated = true; public User() { } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public String getPassword() { return this.password; } public void setPassword(String password) { if (StringUtils.hasLength(password)) { this.passwordGenerated = false; this.password = password; } } public List getRoles() { return this.roles; } public void setRoles(List roles) { this.roles = new ArrayList(roles); } public boolean isPasswordGenerated() { return this.passwordGenerated; } } public static class Filter { private int order = -100; private Set dispatcherTypes; public Filter() { this.dispatcherTypes = new HashSet(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST)); } public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } public Set getDispatcherTypes() { return this.dispatcherTypes; } public void setDispatcherTypes(Set dispatcherTypes) { this.dispatcherTypes = dispatcherTypes; } } }
@ConfigurationProperties这个注解作用就是将SpringBoot配置文件的内容和当前的属性做绑定,并且在绑定的时候,会去SpringBoot的配置文件中,会找到这个前缀为spring.security这样的配置,很明显在我们当前的springboot程序中,我们并没有这这些事。像这种情况下,这里边的配置属性都会使用默认值。这个SecurityProperties类中有一个User属性,这个就对应了一个默认的User对象。User是SecurityProperties的一个静态内部类。
在我们在SpringBoot项目中的配置文件没有对SpringSecurity做任何配置情况下,这个User的name就是“user”,password就是一个uuid。
public static class User { private String name = "user"; private String password = UUID.randomUUID().toString(); private Listroles = new ArrayList(); private boolean passwordGenerated = true; public User() { } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public String getPassword() { return this.password; } public void setPassword(String password) { if (StringUtils.hasLength(password)) { this.passwordGenerated = false; this.password = password; } } public List getRoles() { return this.roles; } public void setRoles(List roles) { this.roles = new ArrayList(roles); } public boolean isPasswordGenerated() { return this.passwordGenerated; } }
到这里,就解释了控制台中的账号和密码是怎么生成的了。
private String name = "user"; private String password = UUID.randomUUID().toString();
现在我们回到SecurityAutoConfiguration这个类,这个是引入jar包之后,Springboot就会自动加载的那个SpringSecurity的核心配置文件。这个类的上边还有一个注解:
@Import({SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class})
@Configuration @ConditionalOnClass({DefaultAuthenticationEventPublisher.class}) @EnableConfigurationProperties({SecurityProperties.class}) @Import({SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class}) public class SecurityAutoConfiguration { public SecurityAutoConfiguration() { } @Bean @ConditionalOnMissingBean({AuthenticationEventPublisher.class}) public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) { return new DefaultAuthenticationEventPublisher(publisher); } }
这个import注解会自动把上边这俩类加载进来。在第一个配置类当中,里边配置了SpringSecurity的默认认证方式。
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) class SpringBootWebSecurityConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnDefaultWebSecurity static class SecurityFilterChainConfiguration { @Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests().anyRequest().authenticated(); http.formLogin(); http.httpBasic(); return http.build(); } } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) @ConditionalOnClass(EnableWebSecurity.class) @EnableWebSecurity static class WebSecurityEnablerConfiguration { } }
@ConditionalOnWebApplication(type = Type.SERVLET)这个告诉我们,生效于Servlet类型的应用。
@Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests().anyRequest().authenticated(); http.formLogin(); http.httpBasic(); return http.build(); }
这个定义了所有的http请求,都会进行认证。
这个定义了所有的http请求,都支持表单认证。
这个定义了所有的http请求,都会支持basic认证。
我们现在说的这个事是表单认证,所以我们现在进来formLogin这种表单认证方式。
public FormLoginConfigurerformLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); }
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password"); }
UsernamePasswordAuthenticationFilter这个类作用就是根据用户名和密码做认证,这一个默认加载的过滤器。
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST"); public UsernamePasswordAuthenticationFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
这个时候就意味着这个过滤器被加载了,然后过滤器发挥作用的时候用的都是doFilter方法,然而这个Filter并没有doFilter方法,答案就是在他的父类当中,父类是一个抽象类,我们找一下:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); }
在这个公共的doFilter方法中,调用了私有的方法:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //不需要认证,直接结束即可 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); } 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); } }
在哪里做的认证呢?显然是在这个方法里边:
Authentication authenticationResult = attemptAuthentication(request, response);
我们查看一下认证过程:
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //如果认证方式只支持post,又不是post。 if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } //接下来一定是post提交的表单模式。 String username = obtainUsername(request); //获取username和password username = (username != null) ? username.trim() : ""; String password = obtainPassword(request); password = (password != null) ? password : ""; //把获取到的账号密码封装成一个Token对象。 UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); //拿着这个Token对象在这里边做认证。 return this.getAuthenticationManager().authenticate(authRequest); }
我们查看一下详细认证过程:
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)", provider.getClass().getSimpleName(), ++currentPosition, size)); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { prepareException(ex, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw ex; } catch (AuthenticationException ex) { lastException = ex; } } if (result == null && this.parent != null) { // Allow the parent to try. try { parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException ex) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException ex) { parentException = ex; lastException = ex; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } // If the parent AuthenticationManager was attempted and successful then it // will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent // AuthenticationManager already published it if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } // If the parent AuthenticationManager was attempted and failed then it will // publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the // parent AuthenticationManager already published it if (parentException == null) { prepareException(lastException, authentication); } throw lastException; }
这个拿着一个Authentication 对象去做认证,如果认证通过的话返回的还是一个Authentication 对象,这时候Authentication 对象就是认证后的完成对象,就不仅仅是账号密码了,包含角色等等信息。
我们继续追踪如何认证的。AuthenticationProvider是真正进行提供认证的工具,ProviderManager管理着多个AuthenticationProvider,所以进行遍历从中挑选认证工具。然后进行认证:
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); String username = determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { throw ex; } // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
拿到账号之后,去往一个用户cache里边去获取用户详情对象信息。第一次访问的时候,缓存里边是空的,这个时候
@Override protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
org.springframework.security.provisioning.InMemoryUserDetailsManager#loadUserByUsername private final Mapusers = new HashMap<>(); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetails user = this.users.get(username.toLowerCase()); if (user == null) { throw new UsernameNotFoundException(username); } return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities()); }
所以,你看默认用户信息创建好之后是放到这个一个默认的Map里边的。也是从这里边获取信息进行认证和返回。
由内存支持的UserDetailsManager接口的非持久实现类主要用于测试和演示目的,不需要完成的持久性系统支持。
总结:
1.默认用户名user和控制台的密码,是在SpringSecurity 提供的 User类中定义生成的
2.在表单认证时,基于InMemoryUserDetailsManager类具体进行实现,也就是基于内存的实现。
上一篇:sql字段类型和时间格式转换