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;
    }
}
相关推荐
兩尛2 小时前
项目概述、开发环境搭建(day01)
java·spring boot·web
我想学LINUX3 小时前
【2024年华为OD机试】(C卷,100分)- 攀登者1 (Java & JS & Python&C/C++)
java·c语言·javascript·c++·python·游戏·华为od
日暮温柔5 小时前
实现nacos配置修改后无需重启服务--使用@RefreshScope注解
java
武昌库里写JAVA6 小时前
React方向:react中5种Dom的操作方式
java·开发语言·spring boot·学习·课程设计
ThetaarSofVenice6 小时前
一个个顺序挨着来 - 责任链模式(Chain of Responsibility Pattern)
java·设计模式·责任链模式
数据小小爬虫7 小时前
利用Java爬虫获取义乌购店铺所有商品列表:技术探索与实践
java·开发语言·爬虫
Dong雨7 小时前
Java的Stream流和Option类
java·新特性
!!!5257 小时前
maven的生命周期
java·数据库·maven
Hello Dam8 小时前
基于 FastExcel 与消息队列高效生成及导入机构用户数据
java·数据库·spring boot·excel·easyexcel·fastexcel
ShyTan8 小时前
java项目启动时,执行某方法
java·开发语言