SpringSecurity源码分析以及如何解决前后端分离出现的跨域问题

解决Security前后端分离出现的跨域问题

一. Security源码分析

首先在看源码之前我们先来看这张图 , 这张图展示了Security执行的全部流程

从上图可知Security执行的入口是UsernamePasswordAuthenticationFilter这个抽象类 , 那我们就先从该类进行分析

1. UsernamePasswordAuthenticationFilter

进入源码我们可以看到该类继承了一个叫做AbstractAuthenticationProcessingFilter的类 , 而相同的是二者的类名都包含Filter , 说明二者的顶级接口都包含Filter , 而Filter中包含一个非常重要的方法doFilter , 但是作为抽象重写该方法不是必须的 , 现在我们就从继承的层级关系寻找哪个类重写了该方法

AbstractAuthenticationProcessingFilter

该类继承了一个GenericFilterBean抽象类 , 我们继续往上找

GenericFilterBean

在这里我们终于找到了Filter接口 , 那么谁重写了doFilter呢? ( 这里的是否被重写需要从上层一层一层往下找 )

点开结构我们发现这里并没有发现doFilter , 那么就看它的下一级嘛 , 也就是AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter

在这里我们也是找到了doFilter方法 , 观察其源码该方法调用了一个叫做attemptAuthentication尝试认证的方法

而该方法是一个抽象方法 , 必然会被子类重写 , 那么绕来绕去又要回到一开始的UsernamePasswordAuthenticationFilter

2. attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        username = username != null ? username : "";
        username = username.trim();
        String password = this.obtainPassword(request);
        password = password != null ? password : "";
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

如下所示 , 前端传输的账号与密码是通过obtainUsername这个方法进行获取的 , 那么继续来看该方法做了什么

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

private String usernameParameter = "username";
    private String passwordParameter = "password";

看完下面的代码是不是很明确了 , 账号密码是通过request.getParameter获取的而该方法 , 而该方法是获取表单数据的 , 在前后端分离的架构中拥有跨域问题 , 所以传统的Security架构无法解决这些问题 , 就需要我们自己来实现

二. 如何重写attemptAuthentication方法 , 实现数据传输

重写attemptAuthentication我们只需要继承AbstractAuthenticationProcessingFilter类并参照UsernamePasswordAuthenticationFilter原有的方法进行重新, 进行适当的修改即可

package com.itheima.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;

/**
 * @program: security_demo
 * @description:
 * @author: jixu
 * @create: 2024-10-18 01:40
 **/

public class MyUserNamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    protected MyUserNamePasswordAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

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

    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        if (!httpServletRequest.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + httpServletRequest.getMethod());
        } else {
            // 在这里就是出现问题的地方只需要重新修改获取账号密码的方式就行了
            // 这里由于我使用的是Json格式的数据进行传参 , 所以只需要获取数据, 反序列化即可
            ServletInputStream inputStream = httpServletRequest.getInputStream();
            HashMap<String,String> info = new ObjectMapper().readValue(inputStream, HashMap.class);
            String username = info.get(SPRING_SECURITY_FORM_USERNAME_KEY);
            username = username != null ? username : "";
            username = username.trim();
            String password = info.get(SPRING_SECURITY_FORM_PASSWORD_KEY);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            //this.setDetails(httpServletRequest, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    /**
     *
     * @param request
     * @param response
     * @param chain
     * @param authResult: 用户认证信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        User principal = (User) authResult.getPrincipal();
        String username = principal.getUsername();
        Collection<GrantedAuthority> authorities = principal.getAuthorities();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        HashMap<String, String> info = new HashMap<>();
        info.put("code","1");
        info.put("msg","响应成功");
        // 将数据Map数据序列化成Json数据并发送
        response.getWriter().write(new ObjectMapper().writeValueAsString(info));


    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        super.unsuccessfulAuthentication(request, response, failed);
    }
}

在这里我们已经将自定义的配置信息完善了 , 那么接下来再来想一个问题 , 该配置信息如何被Security识别并使用?

在Security中先来的过滤器会覆盖后来的 , 也就是当识别到一个过滤器被使用了 , 那么它后面与之相同的过滤器就会失效

所以只需要提前声明我们自定义的过滤器就行了

package com.itheima.security.config;

import com.itheima.security.filter.MyUserNamePasswordAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @program: security_demo
 * @description:
 * @author: jixu
 * @create: 2024-10-17 21:01
 **/

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    /**
     * 定义用户认证和授权的信息
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().and().logout().permitAll().and().csrf().disable().authorizeRequests();
        http.addFilterBefore(myUserNamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public MyUserNamePasswordAuthenticationFilter myUserNamePasswordAuthenticationFilter() throws Exception {
        // 构造认证过滤器对象 , 传入默认登录路径
        MyUserNamePasswordAuthenticationFilter myUserNamePasswordAuthenticationFilter = new MyUserNamePasswordAuthenticationFilter("/mylogin");
        // 认证该过滤器Bean
        myUserNamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());

        return myUserNamePasswordAuthenticationFilter;
    }
}
相关推荐
秋说1 分钟前
【Java开发指南 | 第三十四篇】IDEA没有Java Enterprise——解决方法
java·开发语言·intellij-idea
Spring小子20 分钟前
蓝桥杯[每日两题] 真题:好数 神奇闹钟 (java版)
java·数据结构·算法·蓝桥杯
Ιτ-ωoгκεг24 分钟前
在 Java 中使用 Apache POI 为 Word 文档添加水印
java·word·apache·poi·水印
已是上好佳36 分钟前
介绍一下Qt中的事件过滤
java·服务器·数据库
Java中文社群1 小时前
面试官:你项目是如何保证高可用的?
java·后端·面试
不修×蝙蝠1 小时前
SpringBoot(一)--搭建架构5种方法
java·spring boot·架构·配置·搭建
FreemanGordon2 小时前
Java volatile 关键字
java
北京_宏哥2 小时前
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
java·前端·selenium
北京_宏哥2 小时前
《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
java·selenium·前端工程化
当归10242 小时前
微服务与消息队列RabbitMQ
java·微服务