SpringSecurity6从入门到上天系列第九篇:SpringSecurity当中的默认用户的生成、存储、认证过程的源码级别分析
😉😉 欢迎加入我们的学习交流群呀!
??1:这是孙哥suns给大家的福利!
??2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring等等很多应用和源码级别的高质量视频和笔记资料,你想学的我们这里都有!
🥭🥭3:QQ群:583783824 ? 📚📚 ?工作VX:BigTreeJava 拉你进VX群,免费领取!
🍎🍎4:本文章内容出自上述:SpringSecurity应用课程!💞💞
💞💞5:以上内容,进群免费领取呦~ 💞💞💞💞
?
1:控制台的默认用户名和密码是怎么生成的?
? ? ? ? 我们已经讲过在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 List<String> roles = 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<String> getRoles() {
return this.roles;
}
public void setRoles(List<String> roles) {
this.roles = new ArrayList(roles);
}
public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
public static class Filter {
private int order = -100;
private Set<DispatcherType> 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<DispatcherType> getDispatcherTypes() {
return this.dispatcherTypes;
}
public void setDispatcherTypes(Set<DispatcherType> 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 List<String> roles = 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<String> getRoles() {
return this.roles;
}
public void setRoles(List<String> roles) {
this.roles = new ArrayList(roles);
}
public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
? ? ? ? 到这里,就解释了控制台中的账号和密码是怎么生成的了。
private String name = "user";
private String password = UUID.randomUUID().toString();
2:从表单认证流程角度再来追踪源代码
? ? ? ? ?现在我们回到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 FormLoginConfigurer<HttpSecurity> formLogin() 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 Map<String, MutableUserDetails> users = 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类具体进行实现,也就是基于内存的实现。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!