登录表单配置
在上一篇文章中,我们介绍了,基本认证以及默认用户名和密码以及页面SpringSecurity是怎样帮我们生成的,这里我们就来看一下登录表单的详细配置。
项目准备
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
编写登录页面(login.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>登录</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"rel="stylesheet"
id="bootstrap-css"
/>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js">
</script>
</head>
<style>
</style>
<body>
<div id="login">
<div class="container">
<div id="login-row" class="row justify-content-center align-item-center">
<div id="login-colum" class="col-md-6">
<div id="login-box" class="col-md-12">
<form id="login-form" class="form" action="/doLogin" method="post">
<h3 class="text-center text-info">登录</h3>
<div class="form-group">
<label for="username" class="text-info">用户名:</label><br>
<input type="text" name="uname" id="username" class="form-control">
</div>
<div class="form-group">
<label for="password" class="text-info">密码:</label><br>
<input type="text" name="passwd" id="password" class="form-control">
</div>
<div class="form-group">
<input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
提供两个测试接口
@GetMapping("/hello")
public String hello(){
return "hello springboot security";
}
@GetMapping("/index")
public String index(){
return "login success";
}
自定义用户名密码
spring.security.user.name=test
spring.security.user.password=123456
spring.security.user.roles=admin,user
提供SpringSecurity配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
}
在SpringSecurity中,如果我们需要自定义配置,基本上都是继承WebSecurityConfigurerAdapter来实现的,当然WebSecurityConfigurerAdapter本身的配置还是比较复杂,同时也是比较丰富的,这里不细说,后续会详细介绍。
-
首先configure方法中是一个链式配置,当然也可以不用链式配置,每个属性配置完毕后再从http重新写起
-
authorizeRequests()方法表示开启权限配置
- anyRequest().authenticated()表示所有的请求都要认证之后才能访问
-
and()方法,该方法会返回一个HttpSecurityBuilder对象的一个子类(实际上就算HttpSecurity),所以and()方法相当于又回到HttpSecurity实例,重新开启新一轮的配置。
-
formLogin()表示开启表单登录配置:
- loginPage:用来配置登录页面地址
- loginProcessingUrl:用来配置接口登录接口地址
- defaultSuccessUrl:表示登录成功后跳转地址
- failureUrl:表示登录失败后跳转的地址
- usernameParameter:表示登录用户名的参数名称
- passwordParameter:表示密码的参数名称
- permitAll:可以理解成两个and()之间的所有方法地址不需要认证拦截(白名单)。
需要注意的是loginProcessingUrl、usernameParameterpasswordParameter需要和login.html中登录表单配置一致
最后csrf().disable()表示禁用CSRF防御功能,SpringSecurity自带了CSRF防御机制,但是我们这里为了测试方便,先将CSRF防御机制关闭。
启动项目访问http://localhost:8080/index
输入配置的用户名/密码:test/123456,然后就能访问到/index接口了
配置细节
上面我们说到defaultSuccessUrl和failureUrl表示用户登录失败后的跳转地址。关于登录成功和登录失败,除了这两个方法之外,还有另外两个方法可以配置
登录成功
- successForwardUrl
- defaultSuccessUrl
defaultSuccessUrl前者表示当用户登录成功之后,会自动重定向到登录之前的地址上,如果用户本身就是直接访问的登录页面,则登录成功之后就会重定向到defaultSuccessUrl指定的页面中。例如用户在未认证的情况下,访问了/hello页面,此时会重定向到登录页面,登录成功后,就会自动重定向到/hello页面;而用户如果一开始就是访问到登录页面,则登录成功后就会自动重定向到defaultSuccessUrl指定的页面
successForwardUrl则不会考虑用户之前所访问地址,只要用户登录成功,就会通过服务器端跳转到successForwardUrl所指定的页面。
defaultSuccessUrl有一个重载方法,如果重载方法的第二个参数传入true,则它和successForwardUrl效果类似,即不考虑用户之前的访问地址,只要登录成功就重定向到指定页面。不同之处在于defaultSuccessUrl是通过重定向实现的跳转(客户端跳转),而successForwardUrl是通过服务器端跳转实现的。
无论是successForwardUrl还是defaultSuccessUrl,最终所有配置的都是AuthenticationSuccessHandler接口的实例。
SpringSecurity中专门提供了AuthenticationSuccessHandler接口用来处理登录成功事项
public interface AuthenticationSuccessHandler {
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
this.onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
由上述代码可以看到AuthenticationSuccessHandler接口中一共定义了两个方法,一个是default方法,此方法是SpringSecurity5.2开始加入的,在处理特定的认证请求Authentication Filter中会用到;另外一个非default方法,则用来处理登录成功的具体事项,其中authentication参数保存了登录成功的用户信息。
AuthenticationSuccessHandler接口一共有三个实现类:
(1)、SimpleUrlAuthenticationSuccessHandler继承自AbstractAuthenticationTargetUrlRequestHandler,通过AbstractAuthenticationTargetUrlRequestHandler中的handle方法实现请求重定向
(2)、SavedRequestAwareAuthenticationSuccessHandler在SimpleUrlAuthenticationSuccessHandler的基础之上增加了请求缓存的功能,可以记录之前请求的地址,进而在登录成功之后重定向到一开始访问的地址。
(3)、ForwardAuthenticationSuccessHandler的实现就比较容易,就是一个服务端跳转。
我们来重点看一下SavedRequestAwareAuthenticationSuccessHandler和ForwardAuthenticationSuccessHandler
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
这里的核心方法就是 onAuthenticationSuccess
(1)、首先从requestCache中获取缓存下来的请求,如果没有获取到缓存请求,就说明用户在登录页面之前并没有访问其他页面,此时调用父类的方法来处理,最终会重定向到defaultSuccessUrl指定的地址
(2)、如果缓存请求不为空,则会获取一个targetUrlParameter,这个是用户显示指定的,希望登录成功重定向的地址,例如用户发送的登录请求是http://localhost:8080/doLogin?target=/hello,这就表示当用户登录成功之后。希望自动重定向到/hello这个接口,getTargetUrlParameter就是要获取重定向地址参数的key,也就是上面的target,拿到target之后,就可以获取到重定向地址了。
(3)、如果targetUrlParameter存在,或者用户设置了alwaysUseDefaultTargetUrl为true,这个时候缓存下来的请求就没有意义了。此时会直接调用父类的onAuthenticationSuccess方法完成重定向。targetUrlParameter存在,则直接重定向到targetUrlParameter指定的地址。alwaysUseDefaultTargetUrl为true,则直接重定向到defaultSuccessUrl指定的地址。如果alwaysUseDefaultTargetUrl和targetUrlParameter同时满足,则重定向到defaultSuccessUrl指定的地址。
(4)、如果前面的条件都不满足,那么最终会从缓存请求saveRequest中获取重定向地址,然后进行重定向操作。
这就是SavedRequestAwareAuthenticationSuccessHandler的实现逻辑,开发者也可以配置自己的SavedRequestAwareAuthenticationSuccessHandler,代码如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.successHandler(successHandler())
// .defaultSuccessUrl("/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
SavedRequestAwareAuthenticationSuccessHandler successHandler(){
SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
handler.setDefaultTargetUrl("/hello");
handler.setTargetUrlParameter("target");
return handler;
}
}
然后在上篇文章中的表单中,修改一下action的参数
action="/doLogin?target=http://www.baidu.com"
这样当我们登录成功之后就可以跳转到百度了,如果不指定action的target就会跳转到我们上面默认的/hello接口
当我们通过successForwardUrl来设置登录成功后重定向地址时,实际上对应的实现类就是ForwardAuthenticationSuccessHandler,它的源码特别简单,就是一个服务端转发,如下:
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String forwardUrl;
/**
* @param forwardUrl
*/
public ForwardAuthenticationSuccessHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> "'" + forwardUrl + "' is not a valid forward URL");
this.forwardUrl = forwardUrl;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
上述代码可以看到,主要共嗯那个就是调用getRequestDispatcher方法进行服务端转发,AuthenticationSuccessHandler默认的三个实现类,无论是哪一个,都是用来处理页面跳转的。有时候页面跳转并不能满足我们的需求,特别是现在的前后的分离开发中,用户登录成功之后,就不需要跳转页面了,只需要给前端返回一个JSON数据即可,告诉前端登录成功还是失败,前端收到消息后自行处理,像这样的需求,我们可以通过自定义AuthenticationSuccessHandler的实现类来完成,如下:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
HashMap<String,Object> resp = new HashMap<>();
resp.put("status",200);
resp.put("msg","登录成功");
ObjectMapper om = new ObjectMapper();
final String writeValueAsString = om.writeValueAsString(resp);
response.getWriter().write(writeValueAsString);
}
}
然后在WebSecurity中配置
http.successHandler(new MyAuthenticationSuccessHandler());
配置完成之后,此时登录成功,就不会进行页面添砖了,而是返回一段JSON字符串。
关于登录失败的,会在下一篇中详细讲解。