Spring Security在前后端分离项目的使用(认证篇)

455 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

Spring Security在前后端分离项目的使用(认证篇)

\quad在Spring Security中,框架内部使用的默认的登录方式是表单登录,传递的参数传递格式是键值对形式,但是这种登陆的方式并不适用于前后端分离的项目中,因为我们需要的是以JSON格式来传递登录数据,前端发送请求,后端处理请求后进行响应数据,这就需要我们自定义登录过滤器,来提取用户信息来实现认证。

前后端分离.png

$\quad$在Spring Security中,默认的登录参数的提取是在UsernamePasswordAuthenticationFilter过滤器中完成的。如果我们要使用JSON格式登录,只需要重写UsernamePasswordAuthenticationFilter里的认证方法,然后再将自定义的过滤器替换UsermamePasswordAuthenticationFiter过滤器
注意:此处应确保过滤器来那种过滤器的顺序保持不变。

1.思路分析

\quad首先,我们需要自定义的过滤器,重写父类方法的认证方法,父类的认证方法主要分三步:

  • 1.校验登录请求的方式是否是post
  • 2.校验请求的参数类型是否是Json格式的数据
  • 3.提取用户名和密码进行认证

\quad然后,我们需要自定义用户数据,用户数据肯定是放在数据库里面的,此处为了简单就还是用内存的方式进行登录,只不过要对原来的UserDetailsService进行替换。
\quad其次,我们需要自定义登陆成功和失败的数据,这些数据当然也是Json格式的数据。
\quad最后,我们需要用自定义的过滤器去替换默认的过滤器。 \quad思路理清楚之后,我们嘴在手写代码。

2.代码

2.1 准备过滤器

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
            //校验登录请求的方式是否是post
            if (!request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported:" + request.getMethod());
            }
            //校验请求的参数类型是否是Json格式的数据
            if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
                Map<String, String> userInfo = new HashMap<>();
                try {
                //对Json格式进行处理,提取用户名密码
                    userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                    String username = userInfo.get(getUsernameParameter());
                    String password = userInfo.get(getPasswordParameter());
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                    setDetails(request, authRequest);
                    return this.getAuthenticationManager().authenticate(authRequest);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return super.attemptAuthentication(request, response);
    }
}

\quad从自定义的过滤器可以看出,与默认的UsermamePasswordAuthenticationFiter过滤器处理方式很像,只是替换了对Json的处理。

2.1 编写配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //自定义用户信息,当然,最好是放在数据库里,这里只是模拟一下
    //需要注入到容器里
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    //替换用户的数据源
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setUsernameParameter("username");
        loginFilter.setPasswordParameter("password");//用户名和密码的参数
        loginFilter.setAuthenticationManager(authenticationManagerBean());//认证器
        loginFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
            Map<String,Object> result = new HashMap<>();
            result.put("msg","登录成功");
            result.put("userInfo",authentication.getPrincipal());
            response.setStatus(HttpStatus.OK.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }));//登录成功的Json数据
        loginFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
            Map<String,Object> result = new HashMap<>();
            result.put("msg","登录失败"+exception.getMessage());
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }));//登录失败的Json数据
        return loginFilter;

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()  //开启表单认证
                .and()
                .csrf().disable();
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

\quad准备好过滤器后,我们需要对它进行配置,来解释一下这些代码:

  • userDetailsService():自定义用户信息,当然,最好是放在数据库里,这里只是覆盖了内存的用户信息,只是为了模拟一下。可以集成mybatis等持久层框架完成数据源的更改。
  • configure(AuthenticationManagerBuilder auth)以及authenticationManagerBean()分别是用来将数据源进行替换以及注入到容器里。
  • loginFilter():是对过滤器进行配置,此处配置的有用户名和密码的参数;数据源的具体实现方式以及接受认证器的实例;登录成功和失败的信息;
  • configure():拦截所有请求,但是/login请求不会拦截,开启表单认证;最后替换过滤器的是实现,addFilterAt()方法,就是在当前过滤器的位置进行替换。

启动项目,用PostMan进行请求

[psot] localhost:8080/login
{"username":"root","password":"123"}

登录成功或者失败就会返回对应的信息了,注意看状态码是200,表示请求成功,登录后再次请求系统资源就会带上右上角的Cookies信息,就不用再认证了。登录失败就不展示了,状态码会返回500,并提示"登录失败Bad credentials",其实就是凭证错误。

image.png