Spring Security 使用JSON格式参数登录的两种方式
创始人
2024-02-19 02:12:13
0

前言

Spring Security 中,默认的登陆方式是以表单形式进行提交参数的。可以参考前面的几篇文章,但是在前后端分离的项目,前后端都是以 JSON 形式交互的。一般不会使用表单形式提交参数。所以,在 Spring Security 中如果要使用 JSON 格式登录,需要自己来实现。那本文介绍两种方式使用 JSON 登录。

  • 方式一:重写 UsernamePasswordAuthenticationFilter 过滤器
  • 方式二:自定义登录接口

方式一

通过前面几篇文章的分析,我们已经知道了登录参数的提取在 UsernamePasswordAuthenticationFilter 过滤器中提取的,因此我们只需要模仿UsernamePasswordAuthenticationFilter过滤器重写一个过滤器,替代原有的UsernamePasswordAuthenticationFilter过滤器即可。

UsernamePasswordAuthenticationFilter 的源代码如下:

重写的逻辑如下:

public class LoginFilter extends UsernamePasswordAuthenticationFilter {@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {// 需要是 POST 请求if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}HttpSession session = request.getSession();// 获得 session 中的 验证码值String sessionVerifyCode = (String) session.getAttribute("verify_code");// 判断请求格式是否是 JSONif (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {Map loginData = new HashMap<>();try {loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);} catch (IOException e) {}finally {String code = loginData.get("code");checkVerifyCode(sessionVerifyCode, code);}String username = loginData.get(getUsernameParameter());String password = loginData.get(getPasswordParameter());if(StringUtils.isEmpty(username)){throw new AuthenticationServiceException("用户名不能为空");}if(StringUtils.isEmpty(password)){throw new AuthenticationServiceException("密码不能为空");}UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}else {checkVerifyCode(sessionVerifyCode, request.getParameter("code"));return super.attemptAuthentication(request, response);}}private void checkVerifyCode(String sessionVerifyCode, String code) {if (StringUtils.isEmpty(code)){throw new AuthenticationServiceException("验证码不能为空!");}if(StringUtils.isEmpty(sessionVerifyCode)){throw new AuthenticationServiceException("请重新申请验证码!");}if (!sessionVerifyCode.equalsIgnoreCase(code)) {throw new AuthenticationServiceException("验证码错误!");}}
}
复制代码

上述代码逻辑如下:

  • 1、当前登录请求是否是 POST 请求,如果不是,则抛出异常。
  • 2、判断请求格式是否是 JSON,如果是则走我们自定义的逻辑,如果不是则调用 super.attemptAuthentication 方法,进入父类原本的处理逻辑中;当然也可以抛出异常。
  • 3、如果是 JSON 请求格式的数据,通过 ObjectMapper 读取 request 中的 I/O 流,将 JSON 映射到Map 上。
  • 4、从 Map 中取出 code key的值,判断验证码是否正确,如果验证码有错,则直接抛出异常。如果对验证码相关逻辑感到疑惑,请前往:【Spring Security 在登录时如何添加图形验证码验证】
  • 5、根据用户名、密码构建 UsernamePasswordAuthenticationToken 对象,然后调用官方的方法进行验证,验证用户名、密码是否真实有效。

接下来就是将我们自定义的 LoginFilter 过滤器代替默认的 UsernamePasswordAuthenticationFilter

import cn.cxyxj.study05.filter.config.MyAuthenticationEntryPoint;
import cn.cxyxj.study05.filter.config.MyAuthenticationFailureHandler;
import cn.cxyxj.study05.filter.config.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@BeanPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}@Bean@Overrideprotected UserDetailsService userDetailsService() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());manager.createUser(User.withUsername("security").password("security").roles("user").build());return manager;}@Override@Beanpublic AuthenticationManager authenticationManagerBean()throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 用自定义的 LoginFilter 实例代替 UsernamePasswordAuthenticationFilterhttp.addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class);http.authorizeRequests()  //开启配置// 验证码、登录接口放行.antMatchers("/verify-code","/auth/login").permitAll().anyRequest() //其他请求.authenticated().and()//验证   表示其他请求需要登录才能访问.csrf().disable();  // 禁用 csrf 保护http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());}@BeanLoginFilter loginFilter() throws Exception {LoginFilter loginFilter = new LoginFilter();loginFilter.setFilterProcessesUrl("/auth/login");loginFilter.setUsernameParameter("account");loginFilter.setPasswordParameter("pwd");loginFilter.setAuthenticationManager(authenticationManagerBean());loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());return loginFilter;}}
复制代码

当我们替换了 UsernamePasswordAuthenticationFilter 之后,原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效,那些失效的属性,都可以在配置 LoginFilter 实例的时候配置;还需要记得配置AuthenticationManager,否则启动时会报错。

  • MyAuthenticationFailureHandler
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*** 登录失败回调*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic 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(e.getMessage());out.flush();out.close();}
}
复制代码
  • MyAuthenticationSuccessHandler
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;/*** 登录成功回调*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic 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();}}
复制代码
  • MyAuthenticationEntryPoint
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*** 未登录但访问需要登录的接口异常回调*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic 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();}
}
复制代码

测试

提供一个业务接口,该接口需要登录才能访问

@GetMapping("/hello")
public String hello(){return "登录成功访问业务接口";
}
复制代码

OK,启动项目,先访问一下 hello 接口。

接下来先调用验证码接口,然后再访问登录接口,如下:

再次访问业务接口!

方式二

@PostMapping("/doLogin")
public Object login(@RequestBody LoginReq req) {String account = req.getAccount();String pwd = req.getPwd();String code = req.getCode();UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(account, pwd);Authentication authentication = authenticationManager.authenticate(authenticationToken);SecurityContextHolder.getContext().setAuthentication(authentication);return authentication.getPrincipal();
}public class LoginReq {private String account;private String pwd;private String code;
}
复制代码

方式二就是在我们自己的 Controller 层中,编写一个登录接口,接收用户名、密码、验证码参数。根据用户名、密码构建 UsernamePasswordAuthenticationToken 对象,然后调用官方的方法进行验证,验证用户名、密码是否真实有效;最后将认证对象放入到 Security 的上下文中。就三行代码就实现了简单的登录功能。

import cn.cxyxj.study05.custom.config.MyAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@BeanPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}@Bean@Overrideprotected UserDetailsService userDetailsService() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());manager.createUser(User.withUsername("security").password("security").roles("user").build());return manager;}@Override@Beanpublic AuthenticationManager authenticationManagerBean()throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()  //开启配置// 验证码、登录接口放行.antMatchers("/verify-code","/doLogin").permitAll().anyRequest() //其他请求.authenticated().and()//验证   表示其他请求需要登录才能访问.csrf().disable();  // 禁用 csrf 保护http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());}
}
复制代码

简简单单的配置一下内存用户,接口放行。

  • MyAuthenticationEntryPoint
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*** 未登录但访问需要登录的接口异常回调*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic 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();}
}
复制代码

测试

还是先来访问一下业务接口,如下:

再访问登录接口,如下:

登录成功之后,访问业务接口,如下:


  • 自定义官方过滤器方式,要重写各种接口,比如失败回调、登录成功回调,因为官方已经将这些逻辑单独抽离出来了。需要对认证流程有一定的了解,不然你都不知道为什么需要实现这个接口。
  • 自定义接口方式,只要写好那几行代码,你就可以在后面自定义自己的逻辑,比如:密码输入错误次数限制,这种方式代码编写起来更流畅一点,不需要这个类写一点代码,那个类写一点代码。

两者之间没有哪种方式更好,看公司、个人的开发习惯吧!但自定义接口方法应该用的会比较多一点,笔者公司用的就是该方式。

相关内容

热门资讯

MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
操作系统面试题(史上最全、持续... 尼恩面试宝典专题40:操作系统面试题(史上最全、持续更新)...
Android---Banne... 轮播图是一种很常见的UI。Banner框架能够帮助我们快速开发,完成首页轮播图效果的需...
python -- PyQt5... 控件2 本章我们继续介绍PyQt5控件。这次的有 QPixmap , QLineEdi...
Mysql SQL优化跟踪来看... 背景 使用索引字段进行筛选数据时,explain查询语句发现MySQL居然没有使用索...
UG 6.0软件安装教程 UG 6.0软件安装教程 软件简介: UG 6.0是目前网络最好用、使用最为广泛的大型...
HTML静态网页作业——关于我... 家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法,如盒子的嵌套、浮动、ma...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
NoSQL数据库之Redis2 Redis 事务 事务的基础概念 关于事务最常见的例子就是银行转账,A 账户给 B 账...
Spring Security... 前言 在 Spring Security 中,默认的登陆方式是以表单形式进行提交参数的...