写在前面

前面使用的登录表单是Spring Security自带的,在实际开发过程中用户都会自定义登录表单、样式文件、登录接口、登录参数名称等信息,因此本篇就来研究这些内容。

自定义信息

登录表单自定义

SpringSecurity默认提供的登陆表单样式如下所示:

如果开发者想自定义该登陆页面及样式这也是允许的,首先用户需要在resourses文件夹下新建static目录,并在该目录中新建login.html文件,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row clearfix">
<div class="col-md-4 column"></div>

<div class="col-md-4 column">
<h3 class="text-center">
用户登录
</h3>
<form role="form" action="/login.html" method="post">
<div class="form-group">
<label for="exampleInputName">用户名</label><input type="text" name="username" class="form-control" id="exampleInputName" />
</div>
<div class="form-group">
<label for="exampleInputPassword1">密码</label><input type="password" name="password" class="form-control" id="exampleInputPassword1" />
</div>
<div class="checkbox">
</div> <button type="submit" class="btn btn-default">登录</button>
</form>
</div>

<div class="col-md-4 column"></div>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>

请注意上面必须在form中设置action为/login.html,method为post方式,同时jquery.js文件必须在bootstrap.js文件之前引入这一点非常重要。然后回到之前的MyWebSecurityConfig类中,继续完善该类的功能,新增如下代码:

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html").permitAll()
.and()
.csrf().disable();
}

简单解释一下上述代码的含义:

  • 首先重写了configure(HttpSecurity http)方法,然后调用httpSecurity.authorizeRequests()方法开启HttpSecurity的配置。
  • .anyRequest().authenticated()表示用户访问所有URL都必须认证后才能访问(即登录后才能访问)。
  • .and().formLogin().loginPage("/login.html").permitAll()表示开启表单登录,也就是一开始看到的登录页面,同时配置了登录页面为/login.html,最后还配置了permitAll表示和登录相关的页面都不需要认证即可访问。
  • .and().csrf().disable();表示关闭csrf验证。

请注意此处之所以在static文件夹下新建login.html,是因为static文件夹下的资源可以直接通过url地址来进行访问(前提是没有配置相关的url拦截规则)。如果用户在templates文件夹下新建了login.html,那么它必须在controller中配置相应的视图来映射访问,这种相对来说较为普遍,但是此处只是为了演示,因此就不需要了。

之后启动项目,直接访问http://localhost:8080/hello链接时,页面会自动跳转到http://localhost:8080/login.html页面:

这样自定义登录页面就完成了。

本地样式文件

现在你可能要问了,前面的样式文件都是采用了cdn的方式,但是实际开发过程中绝大多数样式文件都是存放在本地的,因此需要修改上述代码。

首先在static文件夹下新建css和js文件,然后在css文件夹中新建bootstrap.min.css文件,在js文件夹中新建bootstrap.min.jsjquery.min.js文件,各个文件中的内容可访问对应的cdn链接来复制获取。

接着回到MyWebSecurityConfig类中,新增configure(WebSecurity web)方法:

1
2
3
4
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**");
}

很明显该方法的作用就是忽略一些url,一般来说静态资源都是需要过滤掉。

之后启动项目,按照前述操作同样实现了既定要求。

自定义登录接口

前面设置的是自定义登录页面,如下图所示。

以及当该页面点击确定按钮后,页面将跳转至/login.html

现在你可能有疑问了,此处action跳转为何是/login.html,而不是/login呢?讲到这里就不得不提登录页面和登录接口,其中登录页面就是/login.html,即上述看到的登陆界面,而登录接口则是action跳转链接/login.html。登录页面使用的是get方式,而登录接口使用的则是post方式。正如你所看到的,此处的登录页面和登录接口设置一致了,均为/login.html。其实SpringSecurity默认的登录页面和登录接口均为/login

在前面自定义登录页面的时候,笔者在configure(HttpSecurity http)方法中进行了如下配置:

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html").permitAll()
.and()
.csrf().disable();
}

可以看到此处设置的.formLogin()表示开启表单登录,.loginPage("/login.html")表示登录页面为/login.html,其实就是登录地址为/login.html,即登录访问链接为:http:localhost:8080/login.html。其实此处也隐含设置登录接口为/login.html,因此也证明前述所说登录页面和登录接口一致的情况。这样现在存在两个链接:

1
2
GET ---> http:localhost:8080/login.html
POST ---> http:localhost:8080/login.html

其中GET请求用于获取登录页面,POST请求用于处理提交的登录信息。

现在问题来了,登录页面和登录接口是否可以设置为不相同呢?答案是当然。只需修改configure(HttpSecurity http)方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/goLogin")
.permitAll()
.and()
.csrf().disable();
}

注意其中的.loginPage("/login.html")设置的是登录页面,而.loginProcessingUrl("/goLogin")则是设置登录接口,这样就将两者分开了。

既然两者不同,那么就需要修改login.html页面的action指向:

1
<form role="form" action="/goLogin" method="post">

之后重启项目,重新登录试试,可以发现没有任何问题。那么现在问题来了SpringSecurity为什么默认将登录页面和登录接口都设置同一个链接呢?这里就不得不查阅源码了,我们知道在SpringSecurity中和form表单相关的配置均在FormLoginConfigurer类中:

1
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> { }

可以看到该类继承自AbstractAuthenticationFilterConfigurer类,查阅一下AbstractAuthenticationFilterConfigurer类的源码:

可以看到该类无参的构造方法中设置了setLoginPage("/login"),别忘了这个无参的构造方法可是会在之类实例化未指定父类构造方法时自动执行,也就是说默认为loginPage("/login")

回到FormLoginConfigurer类中,可以发现该类的init方法调用了父类的init方法,而在父类的init方法中又调用了updateAuthenticationDefaults方法:

1
2
3
4
5
public void init(B http) throws Exception {
this.updateAuthenticationDefaults();
this.updateAccessDefaults(http);
this.registerDefaultAuthenticationEntryPoint(http);
}

查看一下这个updateAuthenticationDefaults方法的源码,可以发现当loginProcessingUrl为null的时候,就将loginPage设置为loginProcessingUrl

需要注意的是,只要loginProcessingUrl为null,那么就将loginPage设置为loginProcessingUrl的值,这一点和是否设置loginPage是没有任何关系的,所以通过阅读源码就知道了SpringSecurity默认将登录页面和登录接口设为一致的原因,还知道了登录接口为什么采用Post方式进行访问:

自定义登录参数名称

仔细阅读FormLoginConfigurer类源码的童鞋可能已经发现了FormLoginConfigurer无参的构造方法中有这么一段代码:

1
2
3
4
5
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), (String)null);
this.usernameParameter("username");
this.passwordParameter("password");
}

这就是一开始笔者要求默认的登录控件name为username,密码控件name为password的原因,可以看到该构造方法一开始调用了父类的构造方法,传入一个UsernamePasswordAuthenticationFilter对象实例,然后将该实例变量赋值给父类的authFilter属性,第二个参数是null,表示使用默认loginProcessingUrl

接下来阅读usernameParameter方法的源码:

1
2
3
4
5
 public FormLoginConfigurer<H> usernameParameter(String usernameParameter) {
((UsernamePasswordAuthenticationFilter)this.getAuthenticationFilter())
.setUsernameParameter(usernameParameter);
return this;
}

可以看到它先将子类强转为父类UsernamePasswordAuthenticationFilter,然后调用它的getAuthenticationFilter方法,而这个getAuthenticationFilter方法返回的则是authFilter属性,也就是说该方法得到的其实是一个UsernamePasswordAuthenticationFilter对象。

1
2
3
protected final F getAuthenticationFilter() {
return this.authFilter;
}

然后调用该对象的setUsernameParameter方法去设置登录名参数,即默认的username

1
2
3
4
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}

设置密码名参数也是类似,因此就不重复介绍了。现在问题来了,这里的设置有什么用呢?我们知道此处需要获取用户输入的账户和密码信息,很显然是需要从HttpServletRequest对象中获取,但是怎么获取呢?在Servlet中一般是通过request.getParameter("username")等这种方式,其实此处也是采取这种方式。在UsernamePasswordAuthenticationFilter类中有以下两个方法用于获取用户提交的账户和密码信息:

1
2
3
4
5
6
7
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}

protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}

看到没有,它是通过passwordParameter和usernameParameter参数来获取的,因此想要自定义登录参数名称只需在MyWebSecurityConfig类中新增如下配置:

1
2
.usernameParameter("name")
.passwordParameter("word")

注意放置的位置:

可以看到IDEA中也智能提示了这些方法的返回值类型。光这样是不够的,因为request.getParameter("username")这种方式是根据表单控件的name属性来获取值的,因此需要修改login.html文件中form表单控件信息:

1
2
3
4
5
6
<div class="form-group">
<label for="exampleInputName">用户名</label><input type="text" name="name" class="form-control" id="exampleInputName" />
</div>
<div class="form-group">
<label for="exampleInputPassword1">密码</label><input type="password" name="word" class="form-control" id="exampleInputPassword1" />
</div>

之后重启项目,重新登录试试,可以发现没有任何问题。

那么本篇关于如何自定义表单信息、样式文件、登录接口、登录参数名称的学习就到此为止,后续开始学习登录回调相关的知识。