一、认证流程分析
上篇文章当中,我们一步一步查阅源码方式对认证流程有了一些认证,本章节梳理一下整个流程,最后形成一张图,以更直观的方式来理解认证的整个流程.
1.1 认证当中步及的接口和类
1.1.1 【抽象类】AbstractAuthenticationProcessingFilter
- 实现了GenericFilterBean抽象类,GenericFilterBean实现了接口Filter
- 此类当中重写了doFilter()方法,当请求到达的时候,要经过该过滤器,并且执行doFitler方法
- 定义模板方法Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
1.1.2 【实现类】UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
- 核心功能实现父类定义的模板方法
1.1.3 【接口】AuthenticationManager
- 认证管理器接口
- 提供了核心认证方法authenticate
1.1.4 【实现类】ProviderManager implements AuthenticationManager
- 实现了接口当中的定义的抽象方法authenticate
1.1.5 【接口】AuthenticationProvider
- 认证的提供方法, 可以处理特定 Authentication 实现
- 提供了认证方法authenticate
1.1.6 【抽象类】AbstractUserDetailsAuthenticationProvider implement AuthenticationProvider
- 实现了接口AuthenticationProvider定义的authenticate()方法
- 定义模板方法UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication), 用于检索用户
1.1.7 【实现类】DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
- 实现父类定义的模板方法, 完成用户的检索
1.1.8 【接口】UserDetailsService
- 定义抽象方法:UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
- 该方法作用: 根据用户名称在数据源当中里查询出具体用户信息, 并且封装为UserDetails对象
1.1.9 【实现类】InMemoryUserDetailsManager implements UserDetailsManager
- 基于内存的数据存储,实现接口当中定义的方法loadUserByUsername
1.1.10 【接口】 UserDetails
- 提供用户的核心信息封装
- 包括用户名称、用户密码、权限列表集合、用户状态信息
1.1.11 【实现类】User implements UserDetails
- 实现接口相关方法
- 对认证的用户信息进行封装
1.1.12 【接口】Authentication
- 定义了认证用户的主体信息
- 权限信息、认证主体信息、用户凭证
1.1.13 【实现类】UsernamePasswordAuthenticationToken
- 间接实现了接口Authentication
- 用来将用户传递的用户名称、密码封装成Authentication对象
- 存储用户名称的时候也是传入的此对象
1.1.14 【图】 认证图解
以上就是认证和授权当中所使用到的接口、抽象类和实现类.结合上篇文章的认证流程的源码分析,可以得出如下的认证时序图
二、默认密码处理流程
2.1 UserDetailsServiceAutoConfiguration自动装配
在上篇文章的认证流程的源码分析当中,从数据源当中获取UserDetails对象的时候,使用的是如下的代码
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
this.getUserDetailsService()返回UserDetailsService对象,问题题为什么是返回的InMemoryUserDetailsManager的对象呢?为什么不是其它对象呢?
其实在应用程序加载的时候,已经默认将InMemoryUserDetailsManager初始化好了,然后将其放到容器当中了.查看UserDetailsServiceAutoConfiguration源码.
@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@Conditional(MissingAlternativeOrUserPropertiesConfigured.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder")
public class UserDetailsServiceAutoConfiguration {// ....
}
其中核心方法如下所示:
// 将InMemoryUserDetailsManager对象放到容器当中
// InMemoryUserDetailsManager是将数据存储到内存当中.这也是spring security默认的存储策略
@Bean
public 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());
}
2.2 默认用户名称生成策略
在UserDetailsServiceAutoConfiguration#inMemoryUserDetailsManager自动装配方法当中实例化InMemoryUserDetailsManager的时候, 设置的默认的用户名称和密码.
return new InMemoryUserDetailsManager(User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build());
其中User.withUsername(user.getName())表示设置用户名称
进入User.withUsername方法, 该方法表示设置用户名称
public static UserBuilder withUsername(String username) {return builder().username(username);
}
通过构建者模式,设置用用户名称,进入方法查看
public UserBuilder username(String username) {Assert.notNull(username, "username cannot be null");this.username = username;return this;
}
this.name=username,这个赋值语句, 用于将传入的用户名称存储到内存当中.
User.withUsername(user.getName()), user.getName()表示获取用户名称,查看该方法
public void setName(String name) {this.name = name;
}
直接返回this.name的值, 找到趸同变量name.
private String name = "user";
所以默认的用户名称为: user.
2.3 默认密码生成策略
在UserDetailsServiceAutoConfiguration#inMemoryUserDetailsManager自动装配方法当中实例化InMemoryUserDetailsManager的时候, 设置的默认的用户名称和密码.
return new InMemoryUserDetailsManager(User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build());
.password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))将密码存储到从内存当中,调用了UserDetailsServiceAutoConfiguration#getOrDeducePassword方法获取密码
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {String password = user.getPassword();if (user.isPasswordGenerated()) {user.getPassword()));}if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {return password;}return NOOP_PASSWORD_PREFIX + password;
}
user.getPassword()表示获取密码
public String getPassword() {return this.password;
}
private String password = UUID.randomUUID().toString();
默认采用的是uuid的方式生成的密码,最后将生成的用户名称和密码存储到内存当中.
三、自定义认证页面、用户名称和密码
3.1 自定义认证页面
在项目当中的resources/static目录下创建一个html文件,起名字: login.html即可.
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body><form method="post" action="/login">用户名称: <input type="text" name="username"><br>用户密码: <input type="password" name="password"><br><input type="submit" value="登录"></input></br></input></br></input></form>
</body>
</html>
特别注意: 这里只是替换了认证的登录页面,具体的认证流程处理还是由spring security默认的流程去处理即可.
怎么才能让spring security使用我们自己定义的认证页面呢? 在上篇文章的认证流程的源码分析当中,提到了spring security的自动装配, 如果想让自动装配失效,则必须要破坏自动装配的生效条件.
class DefaultWebSecurityCondition extends AllNestedConditions {DefaultWebSecurityCondition() {super(ConfigurationPhase.REGISTER_BEAN);}@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })static class Classes {}@ConditionalOnMissingBean({ SecurityFilterChain.class })static class Beans {}
}
@ConditionalOnMissingBean({ SecurityFilterChain.class }), 如果在容器当中,没有 SecurityFilterChain.class 这个bean对象,则默认配置生效,相反的说,如果我们自己定义了 SecurityFilterChain.class 对象,将它添加到容器当中,则默认配置失效,会执行我们自己的认证配置.
3.2 编写配置文件
@Configuration
public class SpringSecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {returnhttp.authorizeHttpRequests(authorize -> {// 如果请求的是/login.html则放行authorize.requestMatchers("/login.html").permitAll().anyRequest().authenticated(); // 其它的请求都需要认证}).formLogin(form -> { // 配置登录相关的信息// 用来指定默认的登录页面, 注意: 一旦自定义登录页面以后必须配置一下登录的url【必须】form.loginPage("/login.html").loginProcessingUrl("/login"); // ①. 指定处理登录的url【必须的】}).csrf(AbstractHttpConfigurer::disable).build();}
}
重点内容都添加到注释当中了,注意查看.
配置完成之后,启动项目,请求接口发现换成我们自己定义的登录页面了.
输入默认的用户名称及密码,成功访问接口
3.3 自定义密码
在spring security自动装配当中
@AutoConfiguration(before = UserDetailsServiceAutoConfiguration.class)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {@Bean@ConditionalOnMissingBean(AuthenticationEventPublisher.class)public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {return new DefaultAuthenticationEventPublisher(publisher);}
}
@EnableConfigurationProperties(SecurityProperties.class), 关于spring security的所有属性配置被引入了,我们查看一下SecurityProperties类当中,可以看到
在application.yml文件当中配置密码:
spring:security:user:name: byejackpassword: byejack
再次启动项目,访问接口
观察启动的控制台日志输出,可以发现,如果我们自己定义了密码,则spring security不再生成密码.
清除浏览器缓存之后,再次访问/hello接口
访问成功.
这里需要注意的是, 当认证成功一次,默认的spring security会将用户凭证保存到session当中,此时清除一下浏览器缓存,或者直接在浏览器当中,删除凭证,再次测试即可. 或者直接在配置文件当中关闭也可.
三、总结
3.1 内容总结
- 认证、授权当中所用到的类或者接口
- 认证流程图解
- 默认的密码生成策略
- 自定义认证登录页面、密码
3.2 下篇内容
- 自定义认证页面细节
- 自定义存储用户信息的数据源