文章目录
1. 前言
在弄懂HelloWorld案例后,我们将自定义登陆页面并弄自定义一个登陆表单,然后覆盖Spring Security默认的登陆页面。首先在引入如下依赖:
<!--引入thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--Spring boot Web容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入SpringSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在配置文件中禁用引入thymeleaf 缓存如下所示:
server:
port: 8001
spring:
application:
name: gsr-auth
security:
user:
name: admin
password: admin123
## thymeleaf配置
thymeleaf:
cache: false
2. 自定义认证
2.1 自定义登录页面
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>login</title>
<link href="favicon.ico" rel="shortcut icon" />
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body style=" background: url(https://img.zcool.cn/community/019e835c611d92a801203d22c2cb34.jpg@1280w_1l_2o_100sh.jpg) no-repeat center center fixed; background-size: 100%;">
<div class="modal-dialog" style="margin-top: 10%;">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title text-center" id="myModalLabel">自定义登录页面</h4>
</div>
<form id="loginForm" class="form" th:action="@{/doLogin}" method="post">
<div class="modal-body" id = "model-body">
<div class="form-group">
<input type="text" name="username" class="form-control"placeholder="请输入用户名" autocomplete="off">
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="请输入密码" autocomplete="off">
</div>
</div>
<div class="modal-footer">
<div class="form-group">
<input type="submit" class="form-control btn btn-info btn-md" name="submit" value="登陆" autocomplete="off">
</div>
</div>
</form>
</div>
</div>
</body>
</html>
2.2 后端认证逻辑
表单定义好以后,接下来定义三个个如下方法:
@Controller
public class HelloWorldController {
@GetMapping("/helloWorld")
public ResponseEntity<String> helloWorld(){
return ResponseEntity.ok("helloWorld");
}
/**
* spring Security 自定义登陆成功跳转的URL必须为post,否则会报错405的问题()
* @return ResponseEntity<String>
*/
@PostMapping("/index")
public ResponseEntity<String> index() {
return ResponseEntity.ok("login Success!");
}
@RequestMapping("/login.html")
public String login() {
return "login";
}
}
最后在配置一个Spring Security的配置类:
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().antMatchers("/index","/login.html").permitAll().anyRequest().authenticated()
.and().formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").successForwardUrl("/index")
.usernameParameter("username").passwordParameter("password")
.and().csrf().disable();
}
Spring Security 提供了一个WebSecurityConfigurerAdapter
抽象类,此类提供了丰富的配置,后面我们在深入研究此类,此处略过。上面配置类的代码简单分析一下:
- configure 方法可以使用链式配置。
- authorizeHttpRequests 表示开启权限认证。
- antMatchers 表示url路径匹配,此方法参数是一个可变参数列表。
- permitAll 结合antMatchers 表示匹配到的url路径可以直接访问(无需任何权限)
- anyRequest().authenticated() 表示所有的请求都需要认证才能访问。
- formLogin() 表示开启表单登陆配置,loginPage 指定登陆页面地址。loginProcessingUrl 用来配置登陆访问的接口地址,successForwardUrl 登陆成功后转发地址,usernameParameter 指定表单登陆的用户名属性,passwordParameter 指定表单中登陆的密码属性。需要注意的是loginProcessingUrl、usernameParameter、passwordParameter这三个属性的值需要与自定义登陆页表单相关属性一致。
- csrf().disable()表示禁用csrf 安全防御机制。
配置完成后,浏览器中直接输入:http://127.0.0.1:8001/ 会自动跳转到登陆页面:
输入用户名和密码,登陆成功跳转到/index 如下所示:
3. 自定义登陆成功处理
在前面配置中,我们使用successForwardUrl 作为用户登陆成功后的跳转地址,除了这个方法以外,defaultSuccessUrl也可以作为用户跳转地址。两者的区别如下所示:
- defaultSuccessUrl 表示用户登陆成功后会自动重定向到登陆之前的地址上,如果在浏览器直接访问登陆页面(login.html)登陆成功后就会重定向到defaultSuccessUrl指定的页面中,但是如果在未认证的情况下直接访问/helloWorld,此时会自动重定向到登陆页面,当认证成功后就会自动重定向到/helloWorld。
- successForwardUrl 表示只要认证成功就会访问指定的地址而不会考虑之前访问的地址。
- defaultSuccessUrl 还有一个重载的方法,第二个参数传入true则作用与successForwardUrl一样,不会考虑认证前访问的地址,只要认证成功就会访问指定的地址。
- successForwardUrl是通过转发实现页面跳转,而defaultSuccessUrl是通过重定向实现页面跳转。
3.1 登陆成功原理
需要说明的是:无论是successForwardUrl还是defaultSuccessUrl都是通过配置AuthenticationSuccessHandler接口实现的。此接口定义如下:
我们可以看到AuthenticationSuccessHandler
接口提供了一个默认方法,此方法从5.2后加入此接口中,这个方法专门为Authentication Filter 使用。另一个方法也是下面我们需要理解并需要使用的方法,此方法中封装了request与response,还有一个保存了登录成功的用户信息,AuthenticationSuccessHandler 一共有三个实现类如下所示:
其中 defaultSuccessUrl
功能实现是由SavedRequestAwareAuthenticationSuccessHandler
完成的,此类定义如下:
可以看到onAuthenticationSuccess
方法 最后是通过重定向来实现页面跳转。
successForwardUrl 功能实现是通过ForwardAuthenticationSuccessHandler
类完成的,此类接口定义如下:
这个类的onAuthenticationSuccess
最后是通过转发实现页面跳转。通过上面两个类的源码分析,我们也可以自定义登陆成功的逻辑,需要注意的是:上述两个类页面跳转并不满足目前流行的前后端分离架构场景中即,后端只需返回登陆成功的JSON数据给前端,然后由前端负责后续的逻辑处理。
3.2 自定义登陆成功响应处理
我们可以自定义类实现AuthenticationSuccessHandler接口并实现其onAuthenticationSuccess
方法如下:
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter implements AuthenticationSuccessHandler {
private final static ObjectMapper objectMapper = new ObjectMapper();
private final static String CONTENT_TYPE_UTF8 = "application/json;charset=utf-8";
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().antMatchers("/index", "/login.html").permitAll().anyRequest().authenticated()
.and().formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").defaultSuccessUrl("/index")
.usernameParameter("username").passwordParameter("password").successHandler(this)
.and().csrf().disable();
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType(CONTENT_TYPE_UTF8);
Map<String, Object> resp = new HashMap<>();
resp.put("code", 200);
resp.put("msg", "登陆成功");
resp.put("authentication", authentication);
// 将登陆成功消息返回给前端
response.getWriter().write(objectMapper.writeValueAsString(resp));
}
}
当重启应用时,并不会自动跳转指定的页面,而是返回如下JSON字符串:
4. 自定义登陆失败处理
默认情况下:登陆失败则直接重定向到登陆页面,而没有任何提示错误消息提醒,我们可以将登陆失败的异常信息输出到登陆页面上,首先在登陆页面添加如下内容:
<div class="modal-header">
<h4 class="modal-title text-center" id="errorMsh" th:text="{SPRING_SECURITY_LAST_EXCEPTION}"></h4>
</div>
后端配置如下:
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter implements AuthenticationSuccessHandler {
private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final static String CONTENT_TYPE_UTF8 = "application/json;charset=utf-8";
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().antMatchers("/index", "/login.html").permitAll().anyRequest().authenticated()
.and().formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").defaultSuccessUrl("/index")
.usernameParameter("username").passwordParameter("password").successHandler(this).failureForwardUrl("/login.html")
.and().csrf().disable();
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType(CONTENT_TYPE_UTF8);
Map<String, Object> resp = new HashMap<>();
resp.put("code", 200);
resp.put("msg", "登陆成功");
resp.put("authentication", authentication);
// 将登陆成功消息返回给前端
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(resp));
}
重启应用后当登陆失败时会输出如下错误:
除了failureForwardUrl,还可以使用failureUrl,修改代码逻辑如下:
同时修改静态页面取值:
重新尝试错误登陆如下:
4.1 登陆失败原理
无论是 failureForwardUrl,还是 failureUrl,都是通过配置AuthenticationFailureHandler
接口实现登陆失败逻辑处理。此接口定义如下:
Spring Security为此接口提供了5个实现类,如下图所示:
- SimpleUrlAuthenticationFailureHandler 默认处理的逻辑就是通过重定向到登陆页面,failureUrl 方法底层就是通过此类进行实现。
通过上面源码可以看到也可以设置
forwardToDestination
为true属性实现转发,其默认为false则是重定向。 ForwardAuthenticationFailureHandler
类非常简单,其处理逻辑即为failureForwardUrl
实现效果。
那么我们登陆错误展示的错误信息来源哪里呢?首先来到SimpleUrlAuthenticationFailureHandler#onAuthenticationFailure,这个方法中有如下代码片段:
首先说明的是:forwardToDestination
默认为false,我们可以看到当session不为空,或者allowSessionCreation(此属性默认为true),所以当认证失败的时候默认在session域对象里设置了一个属性,即:**WebAttributes.AUTHENTICATION_EXCEPTION **的值为:SPRING_SECURITY_LAST_EXCEPTION
这个值也就是我们最开始在静态页面取的表达式设置的值:
让我们再来分析ForwardAuthenticationFailureHandler
类的onAuthenticationFailure
方法如下所示:
这次我们看到了其是在request域对象存储的WebAttributes.AUTHENTICATION_EXCEPTION的值。
4.2 自定义登陆失败响应处理
一般在前后端分离系统中,当认证失败时候只需后端发送认证失败结果为给前端,然后由前端实现认证失败的后续处理逻辑,这个时候我们可以自定义AuthenticationFailureHandler接口实现,我们继续改造AuthSecurityConfig类如下:
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
private final static ObjectMapper objectMapper = new ObjectMapper();
private final static String CONTENT_TYPE_UTF8 = "application/json;charset=utf-8";
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().antMatchers("/index", "/login.html").permitAll().anyRequest().authenticated()
.and().formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").defaultSuccessUrl("/index")
.usernameParameter("username").passwordParameter("password").successHandler(this).failureHandler(this)
.and().csrf().disable();
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType(CONTENT_TYPE_UTF8);
Map<String, Object> resp = new HashMap<>();
resp.put("code", 200);
resp.put("msg", "登陆成功");
resp.put("authentication", authentication);
// 将登陆成功消息返回给前端
response.getWriter().write(objectMapper.writeValueAsString(resp));
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType(CONTENT_TYPE_UTF8);
Map<String, Object> resp = new HashMap<>();
resp.put("code", 500);
resp.put("msg", "登陆失败:"+exception.getMessage());
// 将登陆成功消息返回给前端
response.getWriter().write(objectMapper.writeValueAsString(resp));
}
}
重启应用当再次认证失败的时候只会输出一个JSON如下所示:
5. 注销用户处理
5.1 注销原理
Spring Security 提供了LogoutSuccessHandler接口专门处理注销用户,此接口只有一个onLogoutSuccess
方法,接口定义如下:
spring Security一共提供了4个类实现此接口如下所示:
上文中我们配置的登出逻辑是由SimpleUrlLogoutSuccessHandler类实现的,这个类onLogoutSuccess
调用了抽象父类的方法:
父类相应方法如下所示,当登出成功后就重定向到指定的url。
Spring Security 中提供了默认的注销登录页面如下:
当点击logOut按钮后就会再次跳转到登陆页面如下:
这里我们使用自己开发的静态登陆页面,在配置类添加如下代码:
- logout() 方法开启了登出的配置,logoutUrl指定注销url
- invalidateHttpSession表示登出并使session失效,其属性默认为true。
- clearAuthentication 表示清除认证信息,其属性默认为true。
- logoutSuccessUrl 表示注销登出成功跳转的地址。
当重启项目后登陆认证成功后,然后在浏览器输入:http://127.0.0.1:8001/logout ,注销成功后会自动跳转到login.html。
我们还可以在配置多个登出的url,同时并制定请求url的请求类型为get或者是post,我们修改配置类如下:
此时myLogout1与myLogout2 都可以完成注销,上述注销触发页面跳转都是在后台进行控制的,并不满足前后端分离的场景中,我们可以自定义登出成功的json给前端,然后由前端去处理后续逻辑。整体配置类代码如下:
/**
* 自定义Spring Security 配置类
*
* @author GalenGao
* @version Id: AuthSecurityConfig.java, v 0.1 2022/5/17 19:09 GalenGao Exp $$
*/
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter implements
AuthenticationSuccessHandler, AuthenticationFailureHandler , LogoutSuccessHandler {
private final static ObjectMapper objectMapper = new ObjectMapper();
private final static String CONTENT_TYPE_UTF8 = "application/json;charset=utf-8";
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.antMatchers("/index", "/login.html")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(this)
.failureHandler(this)
.and()
.logout()
// .logoutUrl("/logout")
.logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/myLogout1", HttpMethod.GET.name()),
new AntPathRequestMatcher("/myLogout2", HttpMethod.POST.name())))
.logoutSuccessHandler(this)
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/login.html")
.and()
.csrf().disable();
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType(CONTENT_TYPE_UTF8);
Map<String, Object> resp = new HashMap<>();
resp.put("code", 200);
resp.put("msg", "登陆成功");
resp.put("authentication", authentication);
// 将登陆成功消息返回给前端
response.getWriter().write(objectMapper.writeValueAsString(resp));
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType(CONTENT_TYPE_UTF8);
Map<String, Object> resp = new HashMap<>();
resp.put("code", 500);
resp.put("msg", "登陆失败:" + exception.getMessage());
// 将登陆成功消息返回给前端
response.getWriter().write(objectMapper.writeValueAsString(resp));
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType(CONTENT_TYPE_UTF8);
Map<String, Object> resp = new HashMap<>();
resp.put("code", 200);
resp.put("msg", "注销成功");
resp.put("authentication", authentication);
// 将登出成功消息返回给前端
response.getWriter().write(objectMapper.writeValueAsString(resp));
}
}
重启项目后 再次尝试登出成功后就返回了如下JSON字符串:
总结
这里我们将自定义表单登录的环节已经做了一次实战,下面我们将深入学习Spring Security 是如何进行认证,尽请期待。