解决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;
}
}