Spring Security 使用 JSON 格式登陆
博客: www.lxiaocode.com ,获取更好的阅读体验。
在 Spring Security 中默认的登陆是通过表单的形式进行的。但是在前后端分离的项目中很少会使用表单的形式登陆。大多数情况是由前端调用登陆接口,登陆后则会返回 JSON 格式的响应告诉前端是否成功,根据返回进行跳转页面或其他操作就可以由前端来进行判断了。
那接下来就看一看在 Spring Secuirty 中如何使用 JSON 格式登陆吧!
本文配套的示例源码: https://github.com/lxiaocode/spring-security-examples
你将会学到什么
- Spring Security 的默认配置。
- Spring Security 是如何处理认证异常的?
- Spring Security 是如何获取用户输入参数的?
- 自定义身份验证过滤器,实现 JSON 格式的登陆。
- 将自定义的过滤器配置到 Spring Security。
- Spring Security 在登陆后会进行什么操作?
- 自定义登陆 成功/失败 处理器。
1. Spring Security 的默认配置
1.1 创建 Spring Security 项目
首先需要创建一个 Spring Security 的项目。你可以使用 Spring Initializr 进行创建,也可以使用 Maven 进行创建。因为以后可能还会继续写关于 Spring Security 相关的示例,所以本文配套的源码是使用 Maven 创建的一个多模块项目,以后的示例都会放到这个项目中。
创建项目之后添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 这是一个 Json 工具库,之后会用到 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
编写一个用于测试的接口:
@SpringBootApplication
@RestController
@RequestMapping("/")
public class Application {
// 程序启动方法
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
// 用于测试 Spring Security 的接口
@GetMapping("")
public String index(){
return "index.html";
}
}
1.2 Spring Security 默认配置
- 启动默认配置后,该配置会创建一个名为
springSecurityFilterChain
的 Servlet 过滤器 bean。这个 bean 负责应用程序中所有的安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。 - 使用用户名和随机生成的密码创建一个
UserDetailsService
bean,并记录到控制台。 - 向
springSecurityFilterChain
注册过滤器。
1.2.1 默认配置实现的功能
虽然 Spring Security 的默认配置不多,但却实现了很多功能:
- 需要通过身份验证才能与应用程序进行交互。
- 为你生成一个默认的登陆表单。
- 让用户使用
user
用户名和密码通过基于表单的身份验证(再前面的示例中,密码为8e557245-73e2-4286-969a-ff57fe326336
)。 - 使用 BCrypt 保护密码的储存。
- 允许用户注销。
- 预防 CSRF 攻击。
- Session Fixation 保护。
- Security Header 集成。
- Servlet API方法集成。
HttpServletRequest#getRemoteUser()
HttpServletRequest.html#getUserPrincipal()
HttpServletRequest.html#isUserInRole(java.lang.String)
HttpServletRequest.html#login(java.lang.String, java.lang.String)
HttpServletRequest.html#logout()
1.2.2 默认的 Spring Security 项目
如果你看不懂上面的内容,那么你就当作都是废话。
现在启动项目,你会发现在控制中打印了一串字符:
Using generated security password: cb102f8a-8286-496b-92c0-d36989e55987
很明显这是 Spring Security 为我们自动生成密码,每次启动项目这个密码都会重新生成。有了密码,那么用户名时什么呢?没错就是 “user”。
为什么默认的用户名是 “user” 呢?这个密码又是在哪里生成的呢?这些问题我都会在以后的文章中解释,所以请密切关注我的博客(www.lxiaocode.com)或者公众号(lxiao学习日记)。😄
然后访问我们刚刚编写的测试接口:http://localhost:8080/。你会发现会被重定向到 Spring Security 提供的默认登陆页面(/login)。默认的登陆页面和登陆接口的 URL 都是 “/login”,登陆页面为 GET 请求,登陆接口为 POST 请求。
在登陆表单中根据 Spring Security 提供的默认用户名和生成的密码就可以登陆了,登陆成功后会跳转到你之前访问的接口上。
以上就是 Spring Security 默认配置为我们提供的功能,就是一个典型的基于表单的登陆功能。
2. 认证异常处理
在默认的 Spring Security 登陆流程中,如果你在未登陆的情况下会被重定向到登陆页面。但在前后端分离的项目中,后端是没有登陆页面的,更不可能重定向到登陆页面。通常的做法是返回一串 JSON 格式的信息提示前端该用户没有登陆,由前端为用户跳转到登陆页面。
2.1 默认的认证异常处理
Spring Security 默认的认证异常处理在 LoginUrlAuthenticationEntryPoint
中执行,该类实现了 AuthenticationEntryPoint
接口。这个接口就是用于处理认证异常的:
public interface AuthenticationEntryPoint {
// 当用户未认证时,会进入这个方法进行处理
void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException;
}
实现类中的 commence()
方法:
public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint,
InitializingBean {
// 省略其他方法和字段...
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
// useForward 默认为 false
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
}
从上面的实现方法中可以看出,请求会被重定向到登陆页面。
所以我们主要的思路就是提供一个自定义的 AuthenticationEntryPoint
接口实现类,然后替换掉默认的 LoginUrlAuthenticationEntryPoint
。
2.2 自定义认证处理异常
我们知道认证异常处理是由 AuthenticationEntryPoint
接口提供的,所以我们只需实现它即可:
public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
// JSON 信息
Map<String, Object> map = new HashMap<String, Object>(3);
map.put("code", 401);
map.put("message", "尚未登陆");
map.put("data", authException.getMessage());
JSONObject json = new JSONObject(map);
// 将 JSON 信息写入响应
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = response.getWriter();
out.append(json.toString());
out.flush();
}catch (Exception e){
}finally {
if (out != null){
out.close();
}
}
}
}
2.3 配置认证异常处理
实现认证异常处理完成之后,我们要将它覆盖掉默认的认证异常处理。这时我们需要一个 Spring Security 配置类:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有请求都需要身份验证,关闭 CSRF
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
// 配置认证异常处理
// 因为 AuthenticationEntryPoint 是函数式接口(只有一个方法的接口),
// 所以我们可以使用 Lambda 表达式进行实现,之前的类可以删除了。
// 如果不使用 Lambda 表达式,就直接传入一个实现类的实例既可。
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
// JSON 信息
Map<String, Object> map = new HashMap<String, Object>(3);
map.put("code", 401);
map.put("message", "尚未登陆");
map.put("data", authException.getMessage());
JSONObject json = new JSONObject(map);
// 将 JSON 信息写入响应
response.setCharacterEncoding("UTF-8");
response.setContentType