• Spring Security 使用JSON格式参数登录的两种方式


    前言

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

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

    方式一

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

    UsernamePasswordAuthenticationFilter 的源代码如下:

    重写的逻辑如下:

    1. public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    2. @Override
    3. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    4. // 需要是 POST 请求
    5. if (!request.getMethod().equals("POST")) {
    6. throw new AuthenticationServiceException(
    7. "Authentication method not supported: " + request.getMethod());
    8. }
    9. HttpSession session = request.getSession();
    10. // 获得 session 中的 验证码值
    11. String sessionVerifyCode = (String) session.getAttribute("verify_code");
    12. // 判断请求格式是否是 JSON
    13. if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
    14. Map loginData = new HashMap<>();
    15. try {
    16. loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
    17. } catch (IOException e) {
    18. }finally {
    19. String code = loginData.get("code");
    20. checkVerifyCode(sessionVerifyCode, code);
    21. }
    22. String username = loginData.get(getUsernameParameter());
    23. String password = loginData.get(getPasswordParameter());
    24. if(StringUtils.isEmpty(username)){
    25. throw new AuthenticationServiceException("用户名不能为空");
    26. }
    27. if(StringUtils.isEmpty(password)){
    28. throw new AuthenticationServiceException("密码不能为空");
    29. }
    30. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    31. username, password);
    32. setDetails(request, authRequest);
    33. return this.getAuthenticationManager().authenticate(authRequest);
    34. }else {
    35. checkVerifyCode(sessionVerifyCode, request.getParameter("code"));
    36. return super.attemptAuthentication(request, response);
    37. }
    38. }
    39. private void checkVerifyCode(String sessionVerifyCode, String code) {
    40. if (StringUtils.isEmpty(code)){
    41. throw new AuthenticationServiceException("验证码不能为空!");
    42. }
    43. if(StringUtils.isEmpty(sessionVerifyCode)){
    44. throw new AuthenticationServiceException("请重新申请验证码!");
    45. }
    46. if (!sessionVerifyCode.equalsIgnoreCase(code)) {
    47. throw new AuthenticationServiceException("验证码错误!");
    48. }
    49. }
    50. }
    51. 复制代码

    上述代码逻辑如下:

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

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

    1. import cn.cxyxj.study05.filter.config.MyAuthenticationEntryPoint;
    2. import cn.cxyxj.study05.filter.config.MyAuthenticationFailureHandler;
    3. import cn.cxyxj.study05.filter.config.MyAuthenticationSuccessHandler;
    4. import org.springframework.context.annotation.Bean;
    5. import org.springframework.context.annotation.Configuration;
    6. import org.springframework.security.authentication.AuthenticationManager;
    7. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    8. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    9. import org.springframework.security.core.userdetails.User;
    10. import org.springframework.security.core.userdetails.UserDetailsService;
    11. import org.springframework.security.crypto.password.NoOpPasswordEncoder;
    12. import org.springframework.security.crypto.password.PasswordEncoder;
    13. import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    14. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    15. @Configuration
    16. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    17. @Bean
    18. PasswordEncoder passwordEncoder() {
    19. return NoOpPasswordEncoder.getInstance();
    20. }
    21. @Bean
    22. @Override
    23. protected UserDetailsService userDetailsService() {
    24. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    25. manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
    26. manager.createUser(User.withUsername("security").password("security").roles("user").build());
    27. return manager;
    28. }
    29. @Override
    30. @Bean
    31. public AuthenticationManager authenticationManagerBean()
    32. throws Exception {
    33. return super.authenticationManagerBean();
    34. }
    35. @Override
    36. protected void configure(HttpSecurity http) throws Exception {
    37. // 用自定义的 LoginFilter 实例代替 UsernamePasswordAuthenticationFilter
    38. http.addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    39. http.authorizeRequests() //开启配置
    40. // 验证码、登录接口放行
    41. .antMatchers("/verify-code","/auth/login").permitAll()
    42. .anyRequest() //其他请求
    43. .authenticated().and()//验证 表示其他请求需要登录才能访问
    44. .csrf().disable(); // 禁用 csrf 保护
    45. http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());
    46. }
    47. @Bean
    48. LoginFilter loginFilter() throws Exception {
    49. LoginFilter loginFilter = new LoginFilter();
    50. loginFilter.setFilterProcessesUrl("/auth/login");
    51. loginFilter.setUsernameParameter("account");
    52. loginFilter.setPasswordParameter("pwd");
    53. loginFilter.setAuthenticationManager(authenticationManagerBean());
    54. loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
    55. loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
    56. return loginFilter;
    57. }
    58. }
    59. 复制代码

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

    • MyAuthenticationFailureHandler
    1. import org.springframework.security.authentication.BadCredentialsException;
    2. import org.springframework.security.authentication.LockedException;
    3. import org.springframework.security.core.AuthenticationException;
    4. import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    5. import javax.servlet.ServletException;
    6. import javax.servlet.http.HttpServletRequest;
    7. import javax.servlet.http.HttpServletResponse;
    8. import java.io.IOException;
    9. import java.io.PrintWriter;
    10. /**
    11. * 登录失败回调
    12. */
    13. public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    14. @Override
    15. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    16. response.setContentType("application/json;charset=utf-8");
    17. PrintWriter out = response.getWriter();
    18. String msg = "";
    19. if (e instanceof LockedException) {
    20. msg = "账户被锁定,请联系管理员!";
    21. }
    22. else if (e instanceof BadCredentialsException) {
    23. msg = "用户名或者密码输入错误,请重新输入!";
    24. }
    25. out.write(e.getMessage());
    26. out.flush();
    27. out.close();
    28. }
    29. }
    30. 复制代码
    • MyAuthenticationSuccessHandler
    1. import com.fasterxml.jackson.databind.ObjectMapper;
    2. import org.springframework.security.core.Authentication;
    3. import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    4. import javax.servlet.ServletException;
    5. import javax.servlet.http.HttpServletRequest;
    6. import javax.servlet.http.HttpServletResponse;
    7. import java.io.IOException;
    8. import java.io.PrintWriter;
    9. /**
    10. * 登录成功回调
    11. */
    12. public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    13. @Override
    14. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    15. Object principal = authentication.getPrincipal();
    16. response.setContentType("application/json;charset=utf-8");
    17. PrintWriter out = response.getWriter();
    18. out.write(new ObjectMapper().writeValueAsString(principal));
    19. out.flush();
    20. out.close();
    21. }
    22. }
    23. 复制代码
    • MyAuthenticationEntryPoint
    1. import org.springframework.security.core.AuthenticationException;
    2. import org.springframework.security.web.AuthenticationEntryPoint;
    3. import javax.servlet.ServletException;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletResponse;
    6. import java.io.IOException;
    7. import java.io.PrintWriter;
    8. /**
    9. * 未登录但访问需要登录的接口异常回调
    10. */
    11. public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    12. @Override
    13. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    14. response.setContentType("application/json;charset=utf-8");
    15. PrintWriter out = response.getWriter();
    16. out.write("您未登录,请先登录!");
    17. out.flush();
    18. out.close();
    19. }
    20. }
    21. 复制代码

    测试

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

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

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

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

    再次访问业务接口!

    方式二

    1. @PostMapping("/doLogin")
    2. public Object login(@RequestBody LoginReq req) {
    3. String account = req.getAccount();
    4. String pwd = req.getPwd();
    5. String code = req.getCode();
    6. UsernamePasswordAuthenticationToken authenticationToken =
    7. new UsernamePasswordAuthenticationToken(account, pwd);
    8. Authentication authentication = authenticationManager.authenticate(authenticationToken);
    9. SecurityContextHolder.getContext().setAuthentication(authentication);
    10. return authentication.getPrincipal();
    11. }
    12. public class LoginReq {
    13. private String account;
    14. private String pwd;
    15. private String code;
    16. }
    17. 复制代码

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

    1. import cn.cxyxj.study05.custom.config.MyAuthenticationEntryPoint;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.security.authentication.AuthenticationManager;
    5. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    6. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    7. import org.springframework.security.core.userdetails.User;
    8. import org.springframework.security.core.userdetails.UserDetailsService;
    9. import org.springframework.security.crypto.password.NoOpPasswordEncoder;
    10. import org.springframework.security.crypto.password.PasswordEncoder;
    11. import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    12. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    13. @Configuration
    14. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    15. @Bean
    16. PasswordEncoder passwordEncoder() {
    17. return NoOpPasswordEncoder.getInstance();
    18. }
    19. @Bean
    20. @Override
    21. protected UserDetailsService userDetailsService() {
    22. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    23. manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
    24. manager.createUser(User.withUsername("security").password("security").roles("user").build());
    25. return manager;
    26. }
    27. @Override
    28. @Bean
    29. public AuthenticationManager authenticationManagerBean()
    30. throws Exception {
    31. return super.authenticationManagerBean();
    32. }
    33. @Override
    34. protected void configure(HttpSecurity http) throws Exception {
    35. http.authorizeRequests() //开启配置
    36. // 验证码、登录接口放行
    37. .antMatchers("/verify-code","/doLogin").permitAll()
    38. .anyRequest() //其他请求
    39. .authenticated().and()//验证 表示其他请求需要登录才能访问
    40. .csrf().disable(); // 禁用 csrf 保护
    41. http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());
    42. }
    43. }
    44. 复制代码

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

    • MyAuthenticationEntryPoint
    1. import org.springframework.security.core.AuthenticationException;
    2. import org.springframework.security.web.AuthenticationEntryPoint;
    3. import javax.servlet.ServletException;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletResponse;
    6. import java.io.IOException;
    7. import java.io.PrintWriter;
    8. /**
    9. * 未登录但访问需要登录的接口异常回调
    10. */
    11. public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    12. @Override
    13. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    14. response.setContentType("application/json;charset=utf-8");
    15. PrintWriter out = response.getWriter();
    16. out.write("您未登录,请先登录!");
    17. out.flush();
    18. out.close();
    19. }
    20. }
    21. 复制代码

    测试

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

    再访问登录接口,如下:

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


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

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

  • 相关阅读:
    五、DRF 模型序列化器ModelSerializer
    m1 android emulator 无法共享复制剪切板
    【总结】kubernates 插件工具总结
    dubbo:从零理解及搭建dubbo微服务框架(一)【附带源码】
    基于开源IM即时通讯框架MobileIMSDK:RainbowChat v11.5版已发布
    基于Python完成的配音软件之适用于有声主播
    python自动化测试(十一):写入、读取、修改Excel表格的数据
    工业智能网关BL110应用之六十: 实现西门子S7-200SMART PLC接入Modbus TCP Server云平台
    hivehook 表血缘与字段血缘的解析
    小米将推出中端手机,高通骁龙7系列再添一员,能否吸引消费者?
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/128061326