一文上手SpringSecurity【四】

上一篇当中我们自定义了认证的登录页面,定了一个基本的表格,自定义了登录的密码,但是咱们都知道不能让所有用户都使用一个密码啊...

一、自定义认证登录页面细节分析

1.1 默认登录页面html页面详情

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" integrity="sha384-oOE/3m0LUMPub4kaC09mrdEhIc+e3exm4xOGxAmuFXhBNF4hcg/6MiAXAf5p0P56" crossorigin="anonymous"/>
  </head>
  <body>
     <div class="container">
      <form class="form-signin" method="post" action="/login">
        <h2 class="form-signin-heading">Please sign in</h2>
        <p>
          <label for="username" class="sr-only">Username</label>
          <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
        </p>
        <p>
          <label for="password" class="sr-only">Password</label>
          <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
        </p>
<input name="_csrf" type="hidden" value="WUrEFQvTxRrns-H-eiOdlycETF2qYMfVJ4ww3X5ccJWZmU97Py72JGrl9S_Kh9ScHg6poRA0YWWbA_f4FrlT7xo_E6P8oCxJ" />
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      </form>
</div>
</body></html>

以上是生成的默认的登录页面html源码,我们只看核心部分

html 复制代码
     <form class="form-signin" method="post" action="/login">
       <h2 class="form-signin-heading">Please sign in</h2>
       <p>
         <label for="username" class="sr-only">Username</label>
         <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
       </p>
       <p>
         <label for="password" class="sr-only">Password</label>
         <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
       </p>
<input name="_csrf" type="hidden" value="WUrEFQvTxRrns-H-eiOdlycETF2qYMfVJ4ww3X5ccJWZmU97Py72JGrl9S_Kh9ScHg6poRA0YWWbA_f4FrlT7xo_E6P8oCxJ" />
       <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
     </form>

这里需要注意的是:

  • 请求方式必须是post
  • 用户名称的参数默认必须是: username
  • 密码的参数默认必须是: password

我们知道,所有的认证的时候,会执行UsernamePasswordAuthenticationFilter过滤器, 其中在UsernamePasswordAuthenticationFilter#attemptAuthentication方法当中

java 复制代码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
		throws AuthenticationException {
		// 判断一下当前的认证请求是不是post,不是就抛出异常
	if (this.postOnly && !request.getMethod().equals("POST")) {
		throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
	}
	// 从请求参数当中取出用户名称
	String username = obtainUsername(request);
	username = (username != null) ? username.trim() : "";
	// 从请求参数当中取出密码
	String password = obtainPassword(request);
	password = (password != null) ? password : "";
	UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
			password);
	// Allow subclasses to set the "details" property
	setDetails(request, authRequest);
	return this.getAuthenticationManager().authenticate(authRequest);
}

String username = obtainUsername(request);

String password = obtainPassword(request);

java 复制代码
@Nullable
protected String obtainUsername(HttpServletRequest request) {
	return request.getParameter(this.usernameParameter);
}

@Nullable
protected String obtainPassword(HttpServletRequest request) {
	return request.getParameter(this.passwordParameter);
}

找到this.usernameParameter和this.passwordParameter

java 复制代码
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
java 复制代码
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

可以发现,这两上参数默认就是: username和passwork, 如果想要修改,则可以在配置类当中进行修改.

默认的UsernamePasswordAuthenticationFilter处理请求就是/login,在类当中定义了变量

java 复制代码
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");

调用父类AbstractAuthenticationProcessingFilter构造方法的时候, 将其传递给父类了

java 复制代码
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
		Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
		// 给父类的成员变量赋值
		this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
	}

判断当前登录的请求是不是/login

java 复制代码
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
	if (this.requiresAuthenticationRequestMatcher.matches(request)) {
		return true;
	}
	if (this.logger.isTraceEnabled()) {
		this.logger
			.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
	}
	return false;
}

该方法,在doFilter里进行调用,对登录请求进行拦截,如果不是/login,则直接跳过当前的过滤器

java 复制代码
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
		throws IOException, ServletException {
		// 判断一下是不是当前过滤器能处理的请求,如果不是,则跳过当前过滤器的执行,继续下一个过滤器的执行
	if (!requiresAuthentication(request, response)) {
		chain.doFilter(request, response);
		return;
	}
	// 略

如果要修改请求的uri,则可以在配置类当中进行配置

java 复制代码
 @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")
                                 // 自定义参数名称为yonghuming和mima
                                 .usernameParameter("yonghuming")
                                 .passwordParameter("mima")
                                 // 自定义登录的url
                                 .loginProcessingUrl("/dologin"); // ①. 指定处理登录的url【必须的】
                     }).csrf(AbstractHttpConfigurer::disable)
                     .build();
 }

认证成功

1.2 FormLoginConfigurer

在自定义配置类当中,FormLoginConfigurer是一个用于配置表单登录功能的类.在配置类当中,主要使用的就是它

java 复制代码
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
	formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
	return HttpSecurity.this;
}
java 复制代码
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
	AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
}

其核心方法如下所示

  • loginPage(String loginPage):
    用于设置自定义的登录页面 URL。如果不设置,Spring Security 会使用默认的登录页面。
  • loginProcessingUrl(String loginProcessingUrl):
    指定登录处理的 URL。当用户提交登录表单时,请求会被发送到这个 URL 进行处理。
  • usernameParameter(String usernameParameter)和passwordParameter(String passwordParameter):
    分别用于设置用户名和密码在登录表单中的参数名称。默认情况下,用户名字段为 "username",密码字段为 "password"。
  • defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse):
    设置登录成功后的默认重定向 URL。alwaysUse参数决定是否总是使用这个 URL 进行重定向,即使之前有保存的请求(如在登录前试图访问一个受保护的资源)。
  • failureUrl(String failureUrl):
    指定登录失败后的重定向 URL。
  • permitAll():
    配置登录页面和登录处理 URL 可以被所有用户访问,即使没有经过身份验证。
  • and():
    用于连接配置,返回父类HttpSecurity对象以便继续进行其他安全配置。

以上可以根据自己的需求,进行选择性的进行配置.

二、自定义数据源

2.1 自定义数据源分析

当前我们将用户名称和密码配置在了application.yml文件当中了,但是不能所有用户都使用一个账号和密码啊啊啊啊.再者,先前所有的账号密码都存储在了内存当中,应用重新就啥也没有了,以我们的经验来说,用户账号信息等数据肯定需要持久化存储的.

根据上一篇当中认证流程分析的流程图解

回顾一下这里的核心逻辑:

java 复制代码
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

this.getUserDetailsService()返回的是UserDetailsService接口,实际上这里返回的是InMemoryUserDetailsManager对象,因为在程序启动的时候, 已经将InMemoryUserDetailsManager放到容器里了.

在UserDetailsServiceAutoConfiguration配置类当中, 可以看到该生效的条件当中

java 复制代码
@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 {

}

其中我们看

java 复制代码
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
		AuthenticationManagerResolver.class }

@ConditionalOnMissingBean表示如果在容器当中, 没有AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,AuthenticationManagerResolver.class几个对象,则配置类生效, 反之,如果我们要自己定义的数据源, 不再采用默认的基于内存存储,则可以直接将它们其中的任意一个对象,放到容器里去即可.这里我们根据业务选择【自己去实现UserDetailsService即可,重写抽象方法】.

2.2 参考示例

在项目当中,直接定义UserDetailsService接口的实现类即可,这里我们先模拟一下操作即可

java 复制代码
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 模拟数据库中查询到的用户名称和密码
        String name = "rj";
        String password = "rj";

        // 判断一下用户名称和密码是否正确
        if(!name.equals(username)){
            throw new UsernameNotFoundException("用户名或密码错误");
        }

        // 如果用户名称正确,则去数据源当中,根据用户名称取出相应的用户数据.
        // 这里模拟一下操作,即可.后续再添加上数据源,我们这里暂定使用MySQL

        // 将取出的用户数据,封装成 UserDetails 对象返回即可
        // 结合之前的分析,User是 UserDetails 的子类,所以直接返回即可
        // 参数说明
        // 1. 用户名
        // 2. 密码
        // 3. 角色、权限列表
        return new User(username, password, Collections.emptyList());
    }
}

自定义数据源就是这么简单.

  • 注意事项
    • 示例代码仅仅是模拟操作
    • User不要导错包,它org.springframework.security.core.userdetails包下,切记.
    • User的构造方法,最后一个表示角色、权限列表目前我们没有步及,这里传入了一个空的集合

2.3 验证

启动服务, 访问/hello接口,输入我默认的用户名称和密码,点击提交

到此,自定义数据源完成,总体来说,理解了整个认证流程,理解起来不是很难.

三、总结

3.1 重点总结

  • 对于认证登录细节分析
  • 自定义数据源,即UserDetailsService, 其它的【核心业务】依然是由SpringSecurity帮助我们完成的, 如下图所示,我们只是开了个头,然后提供了一下认证数据而已
  • 以上的自定义认证的登录页面、数据源只适用于前后端不分离的情况下使用

3.2 下篇内容

  • 前后端分离的情况下自定义认证流程
相关推荐
Code_流苏1 分钟前
VSCode搭建Java开发环境 2024保姆级安装教程(Java环境搭建+VSCode安装+运行测试+背景图设置)
java·ide·vscode·搭建·java开发环境
miss writer2 分钟前
Redis分布式锁释放锁是否必须用lua脚本?
redis·分布式·lua
良许Linux3 分钟前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
m0_748254888 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
蜜獾云13 分钟前
docker 安装雷池WAF防火墙 守护Web服务器
linux·运维·服务器·网络·网络安全·docker·容器
小屁不止是运维15 分钟前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构
求知若饥15 分钟前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
Hacker_Oldv19 分钟前
WPS 认证机制
运维·服务器·wps
bitcsljl28 分钟前
Linux 命令行快捷键
linux·运维·服务器
禁默44 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程