在 Spring Security 中默认的登陆是通过表单的形式进行的。但是在前后端分离的项目中很少会使用表单的形式登陆。大多数情况是由前端调用登陆接口,登陆后则会返回 JSON 格式的响应告诉前端是否成功,根据返回进行跳转页面或其他操作就可以由前端来进行判断了。

那接下来就看一看在 Spring Security 中如何使用 JSON 格式登陆吧!

本文配套的示例源码: https://github.com/lxiaocode/spring-security-examples

你将会学到什么

  1. Spring Security 的默认配置。
  2. Spring Security 是如何处理认证异常的?
  3. Spring Security 是如何获取用户输入参数的?
  4. 自定义身份验证过滤器,实现 JSON 格式的登陆。
  5. 将自定义的过滤器配置到 Spring Security。
  6. Spring Security 在登陆后会进行什么操作?
  7. 自定义登陆 成功/失败 处理器。

1. Spring Security 的默认配置

1.1 创建 Spring Security 项目

首先需要创建一个 Spring Security 的项目。你可以使用 Spring Initializr 进行创建,也可以使用 Maven 进行创建。因为以后可能还会继续写关于 Spring Security 相关的示例,所以本文配套的源码是使用 Maven 创建的一个多模块项目,以后的示例都会放到这个项目中。

创建项目之后添加以下依赖:

spring-boot-starter-web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
fastjson
<!-- 这是一个 Json 工具库,之后会用到 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

编写一个用于测试的接口:

Application.java
@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 接口。这个接口就是用于处理认证异常的:

AuthenticationEntryPoint.java
public interface AuthenticationEntryPoint {

// 当用户未认证时,会进入这个方法进行处理
void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException;
}

实现类中的 commence() 方法:

LoginUrlAuthenticationEntryPoint.java
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 接口提供的,所以我们只需实现它即可:

JsonAuthenticationEntryPoint.java
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 配置类:

SecurityConfig.java
@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("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.4 测试

现在启动项目,在没有登陆的情况下访问接口会得到以下信息:

{
"message": "尚未登陆",
"data": "Full authentication is required to access this resource",
"code": 401
}

3. JSON 格式登陆

在默认的 Spring Security 登陆流程中,登陆的方式是通过表单进行的。同样的,在前后端分离的项目依然是通过 JSON 格式进行登陆而不是表单。下面就看一看 Spring Security 默认的登陆逻辑以及如何实现 JSON 格式登陆吧!

3.1 默认的表单登陆方式

Spring Security 是基于过滤器链来实现的,当请求进入 Spring Seucirty 后会经过一个个职责不同的过滤器。而其中负责处理登陆的过滤器就是 UsernamePasswordAuthenticationFilter

而在 UsernamePasswordAuthenticationFilter 过滤器中核心的方法就是 attemptAuthentication()

UsernamePasswordAuthenticationFilter.java
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {

// 省略其他方法和字段...

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {

if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

// 从请求参数中获取用户的输入
String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}
if (password == null) {
password = "";
}

username = username.trim();

// 创建 Authentication 用于身份验证
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
// 设置请求详细信息
setDetails(request, authRequest);
// 调用身份验证方法
return this.getAuthenticationManager().authenticate(authRequest);
}

@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}

@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
}

从上面的方法可以看出,Spring Security 默认会在请求的参数中获取用户的输入。而我们所希望的是获取请求 Body 中的 JSON 信息。

同样实现方式很简单,提供一个自定义的 UsernamePasswordAuthenticationFilter,然后替换掉默认的过滤器。

3.2 自定义身份验证过滤器

我们只需要继承默认的 UsernamePasswordAuthenticationFilter 过滤器,然后重写 attemptAuthentication() 方法即可:

JsonAuthenticationFilter.java
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {

if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

// 判断 ContentType 类型
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){

// 获取请求内容
Map<String, String> loginData = new HashMap<>(2);

try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
e.printStackTrace();
}
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());

// 创建 Authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authentication);

// 执行身份验证
return this.getAuthenticationManager().authenticate(authentication);
}else {
// 兼容表单登陆
return super.attemptAuthentication(request, response);
}
}
}

3.3 配置身份验证过滤器

回到我们的 SecurityConfig 配置类中,在这对自定义身份验证过滤器进行配置:

SecurityConfig.java
@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("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();
}
}
});

// 创建 AuthenticationFilter 实例
UsernamePasswordAuthenticationFilter authenticationFilter =
new JsonAuthenticationFilter();
// 配置 AuthenticationManager
authenticationFilter.setAuthenticationManager(authenticationManagerBean());
// 替换过滤器
http.addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}

3.4 测试

由于我们替换了认证异常处理,现在不会自动跳转到登陆页面了,所以需要我们自己调用 POST /login 接口进行登陆(这个接口是 Spring Security 默认为我们提供的不需要我们创建)。

在 Postman 中进行登陆测试,登陆成功后则会跳转到首页(默认为 “/“):

JSON格式登陆测试

4. 配置登陆 成功/失败 处理器

在 Spring Security 的默认配置中,当用户登陆成功后会自动跳转到之前访问的接口或首页。在前后端分离的项目中,登陆成功的跳转往往是有前端控制,后端只需告诉前端用户是否登陆成功就可以了。

4.1 默认的登陆 成功/失败 处理

Spring Security 默认的登陆 成功/失败 处理在 UsernamePasswordAuthenticationFilter 的父类的 doFilter() 方法中:

AbstractAuthenticationProcessingFilter.java
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {

// 省略其他方法和字段...

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);

return;
}

if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}

Authentication authResult;

try {
// 这里是调用之前自定义的 attemptAuthentication() 方法进行身份验证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
// 当身份验证发生异常时,会调用登陆失败的处理方法
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// 当身份验证发生异常时,会调用登陆失败的处理方法
unsuccessfulAuthentication(request, response, failed);
return;
}

if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 当成功通过身份验证后,会调用登陆成功的处理方法
successfulAuthentication(request, response, chain, authResult);
}

// 登陆成功
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {

if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}

SecurityContextHolder.getContext().setAuthentication(authResult);

rememberMeServices.loginSuccess(request, response, authResult);

if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
// 这里会调用登陆成功处理器,进行真正的处理
// 我们会自定义处理器,将这个默认的处理器替换掉
successHandler.onAuthenticationSuccess(request, response, authResult);
}

// 登陆失败
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();

if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}

rememberMeServices.loginFail(request, response);
// 这里会调用登陆失败处理器,进行真正的处理
// 我们会自定义处理器,将这个默认的处理器替换掉
failureHandler.onAuthenticationFailure(request, response, failed);
}
}

根据上面的源码解析,我们知道了 Spring Security 登陆成功/失败 处理器是哪里调用的了。我们只需要替换掉默认处理器即可。

4.2 自定义登陆 成功/失败 处理器

4.2.1 处理器接口

Spring Security 处理登陆成功和失败是由两个不同的接口实现的。我们需要分别实现这两个接口,然后将默认的处理替换掉即可:

AuthenticationSuccessHandler.java
// 登陆成功处理器
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;

}
AuthenticationFailureHandler.java
// 登陆失败处理器
public interface AuthenticationFailureHandler {

// 我们需要实现的方法
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException;
}
4.2.2 登陆成功处理器实现

逻辑非常简单,与认证异常处理的逻辑差不多:

LoginSuccessHandler
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {

// JSON 信息
Map<String, Object> map = new HashMap<String, Object>(3);
map.put("code", 200);
map.put("msg", "登陆成功");
map.put("data", authentication.getPrincipal().toString());

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){
e.printStackTrace();
}finally {
if (out != null){
out.close();
}
}
}
}
4.2.3 登陆失败处理器实现

与登陆成功处理器很像,但是要处理一些登陆失败的异常:

LoginFailureHandler
public class LoginFailureHandler implements AuthenticationFailureHandler {
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {

// JSON 信息
Map<String, Object> map = new HashMap<String, Object>(3);

map.put("code", 401);
if (exception instanceof LockedException){
map.put("msg", "账户被锁定");
}else if (exception instanceof CredentialsExpiredException){
map.put("msg", "密码过期");
}else if (exception instanceof AccountExpiredException){
map.put("msg", "账户过期");
}else if (exception instanceof DisabledException){
map.put("msg", "账户被禁用");
}else if (exception instanceof BadCredentialsException){
map.put("msg", "用户名或者密码输入错误");
}
map.put("data", exception.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){
e.printStackTrace();
}finally {
if (out != null){
out.close();
}
}
}
}

4.3 配置处理器

因为这两个处理器是包含在身份验证过滤器中的,所以我们需要在身份验证过滤器中添加这两个过滤器。我在来到添加身份验证过滤器的 SecurityConfig 配置类中:

SecurityConfig
@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("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();
}
}
});

// 创建 AuthenticationFilter 实例
UsernamePasswordAuthenticationFilter authenticationFilter =
new JsonAuthenticationFilter();

// 添加登陆 成功/失败 处理器
// 因为这两个处理器也是函数式接口,所以这里同样使用 Lambda 表达式
// 如果不使用 Lambda 表达式,就直接传入一个实现类的实例既可。
authenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
Map<String, Object> map = new HashMap<String, Object>(3);
map.put("code", 200);
map.put("msg", "登陆成功");
map.put("data", authentication.getPrincipal().toString());

// 将 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();
}
}
});

authenticationFilter.setAuthenticationFailureHandler((request, response, exception) -> {
Map<String, Object> map = new HashMap<String, Object>(3);

map.put("code", 401);
if (exception instanceof LockedException){
map.put("msg", "账户被锁定");
}else if (exception instanceof CredentialsExpiredException){
map.put("msg", "密码过期");
}else if (exception instanceof AccountExpiredException){
map.put("msg", "账户过期");
}else if (exception instanceof DisabledException){
map.put("msg", "账户被禁用");
}else if (exception instanceof BadCredentialsException){
map.put("msg", "用户名或者密码输入错误");
}
map.put("data", exception.getMessage());

// 将 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();
}
}
});


// 配置 AuthenticationManager
authenticationFilter.setAuthenticationManager(authenticationManagerBean());
// 替换过滤器
http.addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}

4.4 测试

现在已经配置完成处理器了,我们可以来分别对登陆成功和失败进行测试。

4.4.1 登陆失败

登陆失败测试

4.4.2 登陆成功

5. 总结

以上就是在 Spring Security 使用 JSON 格式登陆的全部内容。

  • AuthenticationEntryPoint 接口实现在未认证的情况下返回 JSON 信息。
  • 继承 UsernamePasswordAuthenticationFilter 过滤器,并重写 attemptAuthentication() 方法,实现从请求 Body 中获取 JSON 格式的登陆信息。
  • AuthenticationSuccessHandlerAuthenticationFailureHandler 处理器实现在登陆成功或失败时返回 JSON 信息。
  • 继承 WebSecurityConfigurerAdapter 对 Spring Security 进行配置。

5.1 一点小优化

5.1.1 封装重复的代码

在返回 JSON 格式信息时,我们都需要将 JSON 信息写入响应中:

// 将 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){
e.printStackTrace();
}finally {
if (out != null){
out.close();
}
}

我们可以将它封装成一个方法,有利于代码的复用避免出现重复的代码:

ResponseUtil.java
public class ResponseUtil {

public static void send(HttpServletResponse response, Map data){

JSONObject json = new JSONObject(data);

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){
e.printStackTrace();
}finally {
if (out != null){
out.close();
}
}
}
}
5.1.2 修改 Spring Security 配置文件

利用我们封装出来的方法,可以优化一下我们的配置文件:

SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有请求都需要身份验证,关闭 CSRF
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();

// 配置 AuthenticationFilter
UsernamePasswordAuthenticationFilter authenticationFilter =
new JsonAuthenticationFilter();
authenticationFilter.setAuthenticationManager(authenticationManagerBean());

authenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
Map<String, Object> map = new HashMap<String, Object>(3);
map.put("code", 200);
map.put("msg", "登陆成功");
map.put("data", authentication.getPrincipal().toString());

// 将 JSON 信息写入响应
ResponseUtil.send(response, map);
});

authenticationFilter.setAuthenticationFailureHandler((request, response, exception) -> {
Map<String, Object> map = new HashMap<String, Object>(3);

map.put("code", 401);
if (exception instanceof LockedException){
map.put("msg", "账户被锁定");
}else if (exception instanceof CredentialsExpiredException){
map.put("msg", "密码过期");
}else if (exception instanceof AccountExpiredException){
map.put("msg", "账户过期");
}else if (exception instanceof DisabledException){
map.put("msg", "账户被禁用");
}else if (exception instanceof BadCredentialsException){
map.put("msg", "用户名或者密码输入错误");
}
map.put("data", exception.getMessage());

// 将 JSON 信息写入响应
ResponseUtil.send(response, map);
});

// 添加过滤器
http.addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

// 配置认证异常处理
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
Map<String, Object> map = new HashMap<String, Object>(3);
map.put("code", 401);
map.put("message", "尚未登陆");
map.put("data", authException.getMessage());

// 将 JSON 信息写入响应
ResponseUtil.send(response, map);
});
}
}

本文配套的示例源码: https://github.com/lxiaocode/spring-security-examples

评论