自定义表单认证和配置
#1. 自定义表单
-
配置『自定义表单认证』核心代码段
Copied!http.formLogin() .loginPage("...") .loginProcessingUrl("...") ...;
-
准备自定义登录页面(可以是一个纯 html 页面)
Copied!<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>登录页</title> </head> <body> <form action="/login.do" method="post"> <p><input name="username" value="tommy" placeholder="username"></p> <p><input name="password" value="123" placeholder="password"></p> <p><button type="submit">登录</button></p> </form> </body> </html>
-
SpringSecurityConfig 类中的配置代码
Copied!@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/sign-in.html") .loginProcessingUrl("/login.do") .permitAll(); // 1 这句配置很重要,新手容易忘记。放开 sign-in.html 的访问权 http.authorizeRequests() .anyRequest() .authenticated(); // 2 http.csrf().disable();// 3 }
自定义表单认证的配置和 Spring Security 自带表单认证配置很像。不同点就在于明确指明了登陆页面,以及登陆页面上登陆表单的 action
。
不过,这里仍有几处小细节需要明确说明:
-
在 Spring Security 自带的表单验证中,我们不需要指定 .permitAll,但是在自定义的表单验证中则需要。表示该页面和登陆请求是匿名可访问的(否则,逻辑上说不通)。
-
登陆页面的名字不强求必须是 sign-in.html,可以自定义。
-
.loginPage 方法的参数除了可以是一个静态的页面(例如,
/sign-in.html
)之外,可以是一个非静态页面的通用的 URI(例如,/login-page.do
)。Spring Security 会重定向到这个 URI,触发 Controller 的执行,由 Controller 的返回值再来决定显示哪个登录页面。
-
登陆页面上的表单提交方式『必须』是 post 方式。
-
.loginProcessingUrl 方法的参数值具体是什么无所谓,但是要和登陆页面(sign-in.html)上的
<form action="...">
值一致。 -
如果登陆页面的 form 表单的 action 属性值是
/login
,那么.loginProcessingUrl()
可以省略,因为它的默认值就是/login
。如果,你的 @RequestMapping 的值是
/login
,由于在这种情况下,Spring MVC 默认会忽略掉后缀,因此,form 表单的action="/login.do"
、action="/login.action"
、action="/login.xxx"
和action="/login"
等价。
代码配置的链式调用的连写:
http.formLogin()
.loginPage("/sign-in.html")
.loginProcessingUrl("/login.do")
.permitAll();
http.authorizeRequests()
.anyRequest().authenticated();
http.csrf()
.disable();
Copied!
#2. 过滤器链中的 UsernamePasswordAuthenticationFilter
在 Spring Security 中 form 表单方式的登录处理是由过滤器链中的 UsernamePasswordAuthenticationFilter 处理的,即,你的登录请求在 Spring Security 的 Filter 链中『走到』UsernamePasswordAuthenticationFilter 时,会被它识别、处理。
UsernamePasswordAuthenticationFilter 会从请求中获取到用户名和密码,再和 UserDetailsService 所提供的标准答案匹对,最终给出『通过认证』或『无法通过认证』的答案。
在这个 UsernamePasswordAuthenticationFilter 中
-
默认的登录请求 url 是 /login
-
默认的两个请求参数分别是 username 和 password
-
默认的请求方式是 post
要么你自定义的登录页面必须满足以上默认的条件,要么进行配置,手动指定。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/sign-in.html")
.loginProcessingUrl("/login"); // 相对于 context-path 的路径,即,不包含 context-path
// .usernameParameter("username")
// .passwordParameter("password")
http.authorizeRequests()
.antMatchers("/sign-in.html").permitAll()
.anyRequest().authenticated();
http.csrf()
.disable(); // 关闭 csrf 功能
}
Copied!
默认,Spring Security 开起了 CSRF Token 功能(跨站请求伪造攻击防护),因此,此时需要通过配置先关闭掉。
TIP
但是有时候(其实就是 RESTful 风格的 API)中,你需要的并不是页面跳转,而是服务端返回 JSON 格式的数据,其中包含登录成功或失败信息。这种情况下,需要使用别的方案处理(见后续内容)。
#3. 认证成功之后
#3.1 默认行为
登录成功后的跳转页面、跳转路径有 2 种:
-
如果用户是直接请求登录页面,那么登录成功后,默认会跳转至当前应用的根路径(
/
)。 -
如果用户时访问某个受限页面/请求,被转到登录页面,那么登录成功后,默认会跳转至原本受限制的页面/请求。
当然,上述是『默认情况』,你可以通过配置,强行指定无论如何,在登录成功后,都跳转至 xxx 页面。
// 登录页面配置
http.formLogin()
.defaultSuccessUrl("/success.jsp");
// .defaultSuccessUrl("/success.jsp", true);
Copied!
通过 .defaultSuccessUrl()
可以指定上述第 1 种情况下的成功跳转页面。如果多加一个参数 true,那么第 2 种情况下,登录成功后也会被强制跳转至这个特定页面。
类似的,通过 .failureForwardUrl()
可以指定登录失败时跳转的错误页面。
#3.2 登录成功返回 JSON
在某些前后端完全分离,仅靠 JSON 完成所有交互的系统中,一般会在登陆成功时返回一段 JSON 数据,告知前端,登陆成功与否。
在这里,可以通过 .successHandler 方法和 .failureHandler 方法指定『认证通过』之后和『认证未通过』之后的处理逻辑。
http.formLogin()
.loginPage("/sign-in.html")
.loginProcessingUrl("/login.do")
.successHandler(new SimpleAuthenticationSuccessHandler())
.failureHandler(new SimpleAuthenticationFailureHandler())
.permitAll();
Copied!
上面的 SimpleAuthenticationSuccessHandler 和 SimpleAuthenticationFailureHandler 类分别是 AuthenticationSuccessHandler 和 AuthenticationFailureHandler 接口的实现类,它们负责实现具体的回复逻辑:
class SimpleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(
HttpServletRequest req,
HttpServletResponse resp,
Authentication authentication) throws IOException, ServletException {
// authentication 对象携带了当前登陆用户名等相关信息
// User user = (User) authentication.getPrincipal();
resp.setContentType("application/json;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.write("JSON 格式字符串");
}
}
class SimpleAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest req,
HttpServletResponse resp,
AuthenticationException e) throws IOException, ServletException {
// e 对象携带了认证的错误原因
resp.setContentType("application/json;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.write("JSON 格式字符串");
}
}
Copied!
在 Spring Security 和 JWT 整合时,我们会用到上面的 AuthenticationSuccessHandler 机制。