当前位置: 首页 > article >正文

一文上手SpringSecurity【三】

一、认证流程分析

上篇文章当中,我们一步一步查阅源码方式对认证流程有了一些认证,本章节梳理一下整个流程,最后形成一张图,以更直观的方式来理解认证的整个流程.

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 【图】 认证图解

以上就是认证和授权当中所使用到的接口、抽象类和实现类.结合上篇文章的认证流程的源码分析,可以得出如下的认证时序图
1

二、默认密码处理流程

2.1 UserDetailsServiceAutoConfiguration自动装配

在上篇文章的认证流程的源码分析当中,从数据源当中获取UserDetails对象的时候,使用的是如下的代码

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

this.getUserDetailsService()返回UserDetailsService对象,问题题为什么是返回的InMemoryUserDetailsManager的对象呢?为什么不是其它对象呢?
2

其实在应用程序加载的时候,已经默认将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);
}

通过构建者模式,设置用用户名称,进入方法查看
3

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()表示获取用户名称,查看该方法
4

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()表示获取密码
6

public String getPassword() {
	return this.password;
}
private String password = UUID.randomUUID().toString();

默认采用的是uuid的方式生成的密码,最后将生成的用户名称和密码存储到内存当中.

三、自定义认证页面、用户名称和密码

3.1 自定义认证页面

在项目当中的resources/static目录下创建一个html文件,起名字: login.html即可.
5

<!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 {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return
                http.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();
    }
}

重点内容都添加到注释当中了,注意查看.
配置完成之后,启动项目,请求接口发现换成我们自己定义的登录页面了.
7
输入默认的用户名称及密码,成功访问接口
6

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类当中,可以看到
9
在application.yml文件当中配置密码:

spring:
  security:
    user:
      name: byejack
      password: byejack

再次启动项目,访问接口
观察启动的控制台日志输出,可以发现,如果我们自己定义了密码,则spring security不再生成密码.
清除浏览器缓存之后,再次访问/hello接口
7
访问成功.
8

这里需要注意的是, 当认证成功一次,默认的spring security会将用户凭证保存到session当中,此时清除一下浏览器缓存,或者直接在浏览器当中,删除凭证,再次测试即可. 或者直接在配置文件当中关闭也可.
10

三、总结

3.1 内容总结

  • 认证、授权当中所用到的类或者接口
  • 认证流程图解
  • 默认的密码生成策略
  • 自定义认证登录页面、密码

3.2 下篇内容

  • 自定义认证页面细节
  • 自定义存储用户信息的数据源

http://www.kler.cn/news/330268.html

相关文章:

  • 大语言模型入门(二)——提示词
  • 5分钟学会SPI
  • MySQL基础练习题49-低质量的问题
  • 【动态规划】完全背包问题
  • 媒介坊:软文自助发布平台,开启营销新篇章
  • 什么是大语言模型的大海捞针指标
  • 【数据库差异研究】update与delete使用表别名的研究
  • Swift并发笔记
  • 「4.3」维护序列
  • 多系统萎缩患者必看!这些维生素助你对抗病魔
  • Docker的入门详解
  • 第十四讲-输入控件QPlainTextEdit
  • redis数据库学习一
  • [Cocoa]_[初级]_[使用NSNotificationCenter作为目标观察者实现时需要注意的事项]
  • STM32F103C8----3-1 LED闪烁(跟着江科大学STM32)
  • 【TypeScript】异步编程
  • Django Nginx+uwsgi 安装配置
  • Python开发环境配置(mac M2)
  • Qt 教程全集目录公布(方便查阅)
  • 基于单片机人体反应速度测试仪系统