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

  • 前后端分离的情况下自定义认证流程
相关推荐
一只叫煤球的猫2 分钟前
MySQL 8.0 SQL优化黑科技,面试官都不一定知道!
后端·sql·mysql
SoFlu软件机器人4 分钟前
智能生成完整 Java 后端架构,告别手动编写 ControllerServiceDao
java·开发语言·架构
写bug写bug1 小时前
如何正确地对接口进行防御式编程
java·后端·代码规范
Cyanto1 小时前
Java并发编程面试题
java·开发语言·面试
不超限1 小时前
Asp.net core 使用EntityFrame Work
后端·asp.net
在未来等你1 小时前
互联网大厂Java求职面试:AI大模型与云原生技术的深度融合
java·云原生·kubernetes·生成式ai·向量数据库·ai大模型·面试场景
豌豆花下猫1 小时前
Python 潮流周刊#105:Dify突破10万星、2025全栈开发的最佳实践
后端·python·ai
sss191s1 小时前
Java 集合面试题从数据结构到 HashMap 源码剖析详解及常见考点梳理
java·开发语言·数据结构
LI JS@你猜啊2 小时前
window安装docker
java·spring cloud·eureka