持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
Spring Security登录相关的表单配置
在之前的文章树梳理了入门案例,接下来我们再来看一下登录表单的详细配置。通常在开发中我们不会使用Security自带的登录页面,都会自定义自己的登录页面,假如现在有一个登录页面,名为 login.htnml ,需要替换掉默认的登录页,那么我们该怎么做呢?
1.快速入门
在登陆页面里需要注意几个地方:
(1)form 的 action需要提交到 /login 接口上。/login 这个接口(也可以是/doLogin)是 Spring Security 自带的,我们写的都是资源接口是登录后访问的,并没有写执行登录操作的接口。
(2)用户名输入框的 name 属性值为 uname,当然这个值是可以自定义的,这里采用了 uname。
(3)密码输入框的 name 属性值为 passwd,passwd 也是可以自定义的。
然后,再写两个用来测试的接口,这个就随便写了,表示受保护的资源。
@RestController
@RequestMapping("/test")
public class HelloSecurity {
@RequestMapping("/index")
public String index() {
return "login success";
}
@RequestMapping("/hello")
public String hello() {
return "hello spring security";
}
}
其次,需要提供一个Spring Security 的配置类:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/test/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
在 Spring Security 中,如果我们需要自定义配置,基本上都是继承自 WebSecurityConfigurer Adapter 来实现的,当然 WebSecurityConfigurerAdapter 本身的配置还是比较复杂,同时也是比较丰富的。下面解释一下每一部分的作用:
- 首先 configure 方法中是一个链式配置,当然也可以不用链式配置,每一个属性配置完毕后再从http.重新开始写起。
- authorizeRequests()方法表示开启权限配置,后面的.anyRequest().authenticated()则表示所有的请求都要认证之后才能访问,不管访问的资源在不在都会对请求进行认证。
- and()方法会返回 HttpSecurityBuilder 对象的一个 子类(实际上就是 HttpSecurity),所以 and()方法相当于又回到 HttpSecurity 实例,重新开启新一轮的配置。其实也可以不用and()方法, 在.anyRequest().authenticated()配置完成后直接用分号(;)结束,然后通过 http.formLogin()继续配置表单登录,这样写就比较麻烦。
- formLogin() 表示开启表单登录配置,针对表单的配置
loginPage 用来配置登录页面地址;
loginProcessingUrl 用来配置登录接口地址;
defaultSuccessUrl 表示登录成功后的跳转地址
failureUrl 表示登录失败后的跳转地址;
usernameParameter 表示登录用户名的参数名称;
passwordParameter 表示登录密码的参数名称;
permitAll 表示跟登录相关的页面和接口不做拦截,直接通过。\
需要注意的是,loginProcessingUrl、usernameParameter、passwordParameter 需要和login.html 中登录表单的配置一致。
5. 最后的 csrf().disable()表示禁用 CSRF 防御功能,Spring Security 自带了 CSRF 防御机制,但是我们这里为了测试方便,先将 CSRF 防御机制关闭。
经过上面的配置,我们已经成功自定义了一个登录页面出来,用户在登录成功之后,就可以访问受保护的资源了。
2. 配置细节
在前面的配置中,我们用 defaultSuccessUrl 表示用户登录成功后的跳转地址,用 failureUrl 表示用户登录失败后的跳转地址。关于登录成功和登录失败,除了这两个方法可以配置之外, 还有另外两个方法也可以配置。
2.1 登录成功
当用户登录成功之后,除了 defaultSuccessUrl 方法可以实现登录成功后的跳转之外, successForwardUrl 也可以实现登录成功后的跳转,两者的区别如下:
- defaultSuccessUrl 表示当用户登录成功之后,会自动重定向到登录之前的地址上, 如果用户本身就是直接访问的登录页面,则登录成功后就会重定向到 defaultSuccessUrl 指定的 页面中。例如,用户在未认证的情况下,访问了/hello 页面,此时会自动重定向到登录页面, 当用户登录成功后,就会自动重定向到/hello 页面;而用户如果一开始就访问登录页面,则登录成功后就会自动重定向到 defaultSuccessUrl 所指定的页面中。
- successForwardUrl 则不会考虑用户之前的访问地址,只要用户登录成功,就会通过服务器端跳转到 successForwardUrl 所指定的页面。
- defaultSuccessUrl 有一个重载方法,如果重载方法的第二个参数传入 true,则 defaultSuccessUrl 的效果与 successForwardUrl 类似,即不考虑用户之前的访问地址,只要登录 成功,就重定向到 defaultSuccessUrl 所指定的页面。不同之处在于,defaultSuccessUrl 是通过重定向实现的跳转(客户端跳转),而 successForwardUrl 则是通过服务器端跳转实现的。
Spring Security 中专门提供了 AuthenticationSuccessHandler 接口用来处理登录成功事项:
public interface AuthenticationSuccessHandler {
//处理特定的认证请求
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
//则用来处理登录成功的具体事项
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException;
}
AuthenticationSuccessHandler 接口共有三个实现类,如图所示
当通过 defaultSuccessUrl 来设置登录成功后重定向的地址时,实际上对应的实现类就是 SavedRequestAwareAuthenticationSuccessHandler,代码如下:
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,
- 首先从 requestCache 中获取缓存下来的请求,如果没有获取到缓存请求,就说明用户在访问登录页面之前并没有访问其他页面,此时直接调用父类的 onAuthenticationSuccess 方法来处理,最终会重定向到 defaultSuccessUrl 指定的地址。
- 接下来会获取一个 targetUrlParameter,这个是用户显式指定的、希望登录成功后重定向的地址,例如用户发送的登录请求是 http://localhost:8080/doLogin?target=/hello,这就表示 当用户登录成功之后,希望自动重定向到 /hello 这个接口。getTargetUrlParameter 就是要获取重定向地址参数的 key,也就是上面的 target,拿到 target 之后,就可以获取到重定向地址了。 其次又分为几种情况:
1.如果 targetUrlParameter 存在,或者用户设置了 alwaysUseDefaultTargetUrl 为 true, 这个时候缓存下来的请求就没有意义了。此时会直接调用父类的 onAuthenticationSuccess 方法完成重定向。
2.targetUrlParameter 存在,则直接重定向到 targetUrlParameter 指定的地址;
3.alwaysUseDefaultTargetUrl 为 true,则直接重定向到 defaultSuccessUrl 指定的地址;
4.如果 targetUrlParameter 存在并且 alwaysUseDefaultTargetUrl 为 true,则重定向到 defaultSuccessUrl 指定的地址。 - 如果前面的条件都不满足,那么最终会从缓存请求 savedRequest 中获取重定向地址, 然后进行重定向操作。
了解了这些之后,我在 SecurityConfig 配置类里面新增方法如下,然后将SecurityConfig里的.defaultSuccessUrl("/test/index")替换为.successHandler(successHandler())也可以跳转到/hello 接口。
SavedRequestAwareAuthenticationSuccessHandler successHandler() {
SavedRequestAwareAuthenticationSuccessHandler handler =
new SavedRequestAwareAuthenticationSuccessHandler();
handler.setDefaultTargetUrl("/index");
handler.setTargetUrlParameter("target");
return handler;
}
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");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 200);
resp.put("msg", "登录成功!");
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
}
配置完成后,重启项目。此时,当用户成功登录之后,就不会进行页面跳转了,而是返回一段 JSON 字符串。
2.2 登录失败和注销登录
登录失败和注销登录的配置与登录大同小异,有默认的配置,也可以自定义登录失败跳转的页面,注销后跳转的页面,以及重写相应的handler去实现更加个性化的功能。