Skip to content

前后端分离

在如今项目中基本上都是前后端分离,也就是前端请求用户名和密码到后端后,后端返回不再是具体成功或者失败页面而是json数据,由前端自行觉得跳转某个页面。要进行改动后端认证流程,那就需要提前了解具体的用户认证流程

1. 用户认证流程

Alt text 其中校验用户名密码的过滤器是UsernamePasswordAuthenticationFilter,步骤如下:

  1. 将用户的登录请求信息封装成UsernamePasswordAuthenticationToken,里面包含了用户名和密码信息
  2. 将UsernamePasswordAuthenticationToken交给AuthenticationManager进行用户名密码验证处理,处理结果无非是成功和失败
  3. 成功的话,经过一系列处理,最后调用AuthenticationSuccessHandler,AuthenticationSuccessHandler用于返回处理
  4. 同样在失败的话,最后要返回之前要调用AuthenticationFailureHandler,AuthenticationFailureHandler用于失败情形的返回处理

2. 认证成功的响应

编写ResponseJsonAuthenticationSuccessHandler类:

java
public class ResponseJsonAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private HttpMessageConverter<Object> converter;

    public ResponseJsonAuthenticationSuccessHandler(HttpMessageConverter<Object> converter) {
        this.converter = converter;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //获取用户身份信息
        Object principal = authentication.getPrincipal();
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "登录成功");
        result.put("data", principal);
        this.converter.write(result, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
    }
}

3. 认证失败响应

编写ResponseJsonAuthenticationFailureHandler类:

java
public class ResponseJsonAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private HttpMessageConverter<Object> converter;

    public ResponseJsonAuthenticationFailureHandler(HttpMessageConverter<Object> converter) {
        this.converter = converter;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //获取错误信息
        String localizedMessage = exception.getLocalizedMessage();

        HashMap<String, Object> result = new HashMap<>();
        result.put("code", -1);
        result.put("message", localizedMessage);
        this.converter.write(result, MediaType.APPLICATION_JSON_UTF8, new ServletServerHttpResponse(response));
    }
}

4. 注销响应

同样的,注销如果要返回Json, 需要实现LogoutSuccessHandler接口,编写ResponseJsonLogoutSuccessHandler类:

java
public class ResponseJsonLogoutSuccessHandler implements LogoutSuccessHandler {

    private HttpMessageConverter<Object> converter;

    public ResponseJsonLogoutSuccessHandler(HttpMessageConverter<Object> converter) {
        this.converter = converter;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //获取用户身份信息
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "注销成功");
        this.converter.write(result, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
    }
}

5. SecurityFilterChain配置

java
@Configuration
public class WebSecurityConfig {

     @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter();

        http
                .authorizeHttpRequests(
                        (authorize) ->
                                authorize.anyRequest()
                                .authenticated())
        // 放开登录页面的访问权限
        .formLogin( form -> {
            form
                    .loginPage("/login").permitAll() //登录页面无需授权即可访问
                    .failureUrl("/login?error=true") //登录失败后跳转的页面
                    .successHandler(new ResponseJsonAuthenticationSuccessHandler(converter))
                    .failureHandler(new ResponseJsonAuthenticationFailureHandler(converter))
            ;
        }); //使用表单授权方式
        // 配置退出
        http.logout(logout -> {
            logout
                    .logoutSuccessHandler(new ResponseJsonLogoutSuccessHandler(converter));
        });
        
        // api接口测试时关闭csrf校验
        http.csrf((csrf) -> csrf.disable());
        return http.build();
    }
}

需要注意的是,配置前后端分离也就意味着前端通过api接口访问,需要关闭csrf。 运行测试:
Alt text

6. 请求未认证的接口

6.1 实现AuthenticationEntryPoint接口

当访问一个需要认证之后才能访问的接口的时候,Spring Security会使用AuthenticationEntryPoint将用户请求跳转到登录页面,要求用户提供登录凭证。
这里我们也希望系统返回json结果,因此我们定义类实现AuthenticationEntryPoint接口

java
public class ResponseJsonAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private HttpMessageConverter<Object> converter;

    public ResponseJsonAuthenticationEntryPoint(HttpMessageConverter<Object> converter){
        this.converter = converter;
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //获取错误信息
        // String localizedMessage = authException.getLocalizedMessage();

        HashMap<String, Object> result = new HashMap<>();
        result.put("code", -1);
        result.put("message", "请先登录!");
        this.converter.write(result, MediaType.APPLICATION_JSON_UTF8, new ServletServerHttpResponse(response));
    }
}

6.2 SecurityFilterChain配置

java
http.exceptionHandling(exception -> {
    exception
            .authenticationEntryPoint(new ResponseJsonAuthenticationEntryPoint(converter));
});

运行测试:
Alt text

7. 跨域

跨域全称是跨域资源共享(Cross-Origin Resources Sharing, CORS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。
在SpringSecurity中解决跨域很简单,在配置文件中添加如下配置即可:

java
// 跨域
http.cors(withDefaults());