日积月累,水滴石穿 😄
前言
本文内容跟着上文继续,Spring Security 的入门案例。
到目前为止我们的SecurityConfig
只包含了关于如何验证用户的信息。那 Security
怎么知道我们需要对所有的接口进行验证?Security
又是怎么知道我们需要支持基于表单的验证?原因是WebSecurityConfigurerAdapter
在configure(HttpSecurity http)
方法中提供了一个默认的配置,代码如下:
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
}
- anyRequest().authenticated():请求都需要被认证
- formLogin:基于表单认证
- httpBasic:可以配置basic登录
自定义表单登录
默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml
生成。在 Spring Security 中,如果我们不做任何配置,默认的登录页面和登录接口的地址都是 /login
,也就是说,默认会存在如下两个请求:
如果发起的是 GET 请求表示你想访问登录页面,如果发起的是 POST 请求,表示你想提交登录数据。而且默认的请求参数为 username、password。那这些是在哪里规定的呢?当进行登录时会执行 UsernamePasswordAuthenticationFilter
过滤器。
- usernamePasrameter :账户参数名
- passwordParameter :密码参数名
- postOnly=true :默认情况下只允许POST请求
但是大多数应用程序都有自己的登录页面,登录接口,接口请求参数名称。 比如不想使用 Security 提供的登录页面,不想调用 login 接口,想改名为 /auth/login,表单参数不想使用 username、password,而是想使用 account、pwd,要做到这一点,只需要配置如下代码:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启配置
.anyRequest() //其他请求
.authenticated()//验证 表示其他请求需要登录才能访问
.and()
// 增加如下
.formLogin()
.loginPage("/login.html") //登录页面
.loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
.usernameParameter("account") //用户名字段
.passwordParameter("pwd") //密码字段
.defaultSuccessUrl("/hello") //登录成功的回调
.permitAll() // 上述 login.html 页面、/auth/login接口放行
.and()
.csrf().disable(); // 禁用 csrf 保护
}
html页面内容如下,并将其配置在 resources/static 目录下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<div id="login">
<h1>Login</h1>
<form method="post" action="/auth/login">
<input type="text" required="required" placeholder="用户名" name="account"></input>
<input type="password" required="required" placeholder="密码" name="pwd"></input>
<button class="but" type="submit">登录</button>
</form>
</div>
</body>
</html>
配置完成之后,重启项目。请求 /hello 接口,跳转到指定的页面,如下:
登录回调
登录回调分为两种情况:
- 前后端不分离的登录回调
- 前后端分离的登录回调
两种情况的处理方式不一样。我们先来看第一种前后端不分的登录回调。
前后端不分离的登录回调
登录成功重定向
登录成功重定向 URL 相关的方法有两个:
- successForwardUrl:表示不管你在浏览器输入什么地址,登录后一律跳转到
successForwardUrl
指定的地址。 - defaultSuccessUrl:指定登录成功的跳转页面或者接口。比如指定为
/hello
,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到/hello
,如果你是在浏览器中输入了其他地址,例如http://localhost:8080/cxyxj/hello
,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到/hello
,而是来到/cxyxj/hello
页面。如果不配置defaultSuccessUrl
,直接在浏览器中输入登录\接口地址,登录成功后,就直接跳转到/
。defaultSuccessUrl
还有一个重载方法,第二个参数默认为 false,与defaultSuccessUrl
方法一致,如果设置为 true,效果与successForwardUrl
方法一致。
登录失败重定向
- failureForwardUrl
- failureUrl
failureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,发生重定向。
在实际开发中(虽然上述方式用的少),不论是登录成功回调还是登录失败回调
只需要选择一种方式配置即可,否则会发生冲突。
如下配置:
http.authorizeRequests() //开启配置
.antMatchers("/hello2","/hello4").permitAll()
.anyRequest() //其他请求
.authenticated()//验证 表示其他请求需要登录才能访问
.and()
// 增加如下
.formLogin()
.loginPage("/login.html") //登录页面
.loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
.usernameParameter("account") //用户名字段
.passwordParameter("pwd") //密码字段
.defaultSuccessUrl("/hello") //登录成功的回调
.successForwardUrl("/hello3")
.failureUrl("/hello2")
.failureForwardUrl("/hello4")
.permitAll() // 上述 login.html 页面、/auth/login接口放行
.and()
.csrf().disable(); // 禁用 csrf 保护
前后端分离的登录回调
登录成功
需要使用到的方法是successHandler
,用来处理登录成功的回调,可以自定义前后端交互的数据。该方法的入参为 AuthenticationSuccessHandler
接口,重写其 onAuthenticationSuccess
方法可以自定义登录的逻辑,onAuthenticationSuccess
方法有三个入参:
void onAuthenticationSuccess(HttpServletRequest var1,
HttpServletResponse var2, Authentication var3)
前面两个参数就不介绍了。
- Authentication:保存了登录后的用户信息
登录成功之后,返回 json 数据给前端,可以进行如下配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启配置
// /hello2、/hello4接口可以直接访问,不需要登录
.antMatchers("/hello2", "/hello4").permitAll().anyRequest() //其他请求
.authenticated() //验证 表示其他请求需要登录才能访问
.and().formLogin().loginPage("/login.html") //登录页面
.loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
.usernameParameter("account") //用户名字段
.passwordParameter("pwd") //密码字段
// 增加如下
.successHandler((request, response, authentication) - > {
Object principal = authentication.getPrincipal();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(principal));
out.flush();
out.close();
}).permitAll() // 上述 login.html 页面、/auth/login接口放行
.and().csrf().disable(); // 禁用 csrf 保护
}
配置完成后,项目重新启动,这里使用请求工具进行请求,给各位小伙伴看的更为直观。可以看到登录成功的用户信息通过 JSON 响应,如下:
返回的密码被置为了空,角色带上了前缀 ROLE_。
如果你对代码有洁癖,觉得上述写法有难看,也也可以实现AuthenticationSuccessHandler
接口,自己重写 onAuthenticationSuccess
方法,
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(principal));
out.flush();
out.close();
}
}
然后将自定义的类配置进去:successHandler(new MyAuthenticationSuccessHandler())
。
登录失败
与之对应的,登录失败也有类似的回调,方法为;failureHandler
,入参为 AuthenticationFailureHandler
接口。重写其 onAuthenticationFailure
方法可以自定义登录的逻辑,onAuthenticationFailure
方法有三个入参:
void onAuthenticationFailure(HttpServletRequest var1,
HttpServletResponse var2, AuthenticationException var3)
前面两个参数就不介绍了。
- AuthenticationException:登录失败的原因,可以根据不同的异常类型,响应用户更加明确的提示信息。
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
String msg = "";
if (e instanceof LockedException) {
msg = "账户被锁定,请联系管理员!";
}
else if (e instanceof BadCredentialsException) {
msg = "用户名或者密码输入错误,请重新输入!";
}
out.write(msg);
out.flush();
out.close();
}
}
添加自定义配置:.failureHandler(new MyAuthenticationFailureHandler())
。重新启动项目,进行测试,故意输错密码。
常见的异常类型
- LockedException:账户被锁定
- CredentialsExpiredException:密码过期
- AccountExpiredException:账户过期
- DisabledException:账户被禁用
- BadCredentialsException:用户名或者密码错误
未认证处理
如果没有登录就访问需要认证的数据,security 默认情况下,会重定向到登录页面。
但是在实际的项目中,有时候不需要这样。我们希望给用户一个尚未登录的提示,然后前端收到提示之后,再决定干什么。要达到这个效果,我们可以使用AuthenticationEntryPoint
这个接口进行自定义,取消默认的重定向行为。
该接口中有一个方法 commence
,方法有三个参数:
void commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3)
- AuthenticationException:未认证的exception。
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("您未登录,请先登录!");
out.flush();
out.close();
}
}
在配置csrf().disable()
之后添加 .exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());
配置完成之后,请求 hello
接口,响应结果如下:
注销登录
有登录功能,那肯定还有注销功能。在 Security
中默认的接口就是 logout
,是一个 GET 请求。
成功退出后跳转到 /login?logout
。也是可以进行配置的。
前后端不分离
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启配置
.antMatchers("/cxyxj/**").hasRole("admin") //访问/cxyxj/**下的路径,必须具备admin身份
.antMatchers("/security/**").hasRole("user") //访问/security/**下的路径,必须具备user身份
.antMatchers("/permitAll").permitAll() // 访问/permitAll路径,不需要登录
.anyRequest() //其他请求
.authenticated()//验证 表示其他请求只需要登录就能访问
.and()
.formLogin()
.loginPage("/login.html") //登录页面
.loginProcessingUrl("/auth/login") //登录接口
.usernameParameter("account") //用户名字段
.passwordParameter("pwd") //密码字段
.defaultSuccessUrl("/hello") //登录成功的回调
.permitAll() // 上述 formLogin() 的页面、接口放行
.and()
// 增加如下
.logout() //开启注销登陆
.logoutUrl("/auth/logout") //注销请求url
.logoutSuccessUrl("/login.html") //注销成功后要跳转的页面
.deleteCookies() // 清除 cookie
.clearAuthentication(true) //清除认证信息
.invalidateHttpSession(true) //使session失效
.permitAll()
.and()
.csrf().disable(); // 禁用 csrf 保护
}
- logoutUr:注销登录的URL。
- logoutSuccessUrl:表示注销成功后要跳转的页面。
- deleteCookies:清除 cookie。
- clearAuthentication:清除认证信息,默认为 true。
- invalidateHttpSession:使 HttpSession 失效,默认为 true。
前后端分离
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("注销成功");
out.flush();
out.close();
}
}
配置如下:
.and()
.logout()
.logoutUrl("/auth/logout") //注销请求url
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.permitAll()
- 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。