一文上手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 下篇内容

  • 前后端分离的情况下自定义认证流程
相关推荐
bobz965几秒前
ovs patch port 对比 veth pair
后端
Asthenia041210 分钟前
Java受检异常与非受检异常分析
后端
uhakadotcom24 分钟前
快速开始使用 n8n
后端·面试·github
JavaGuide31 分钟前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz96541 分钟前
qemu 网络使用基础
后端
Asthenia04121 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04121 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua2 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫
致心2 小时前
记一次debian安装mariadb(带有迁移数据)
后端