SpringSecurity——前后端分离登录认证
SpringSecurity——前后端分离登录认证的整个过程
前端:
使用Axios向后端发送请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<form>
用户名:<input type="text" name="username" id="username"><br>
密码:<input type="password" name="password" id="password"><br>
<input type="button" value="登录" onclick="login()">
</form>
</body>
<script>
function login() {
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
let formData = new FormData(); // 创建formData对象
formData.append("username", username);
formData.append("password", password);
axios.post("http://localhost:8080/login", formData)
.then(function (response) {
console.log(response);
if (response.data.code === 200) {
alert("登录成功");
window.location.href = "welcome.html";
} else {
alert("登录失败");
}
})
.catch(function (error){
console.log(error);
});
}
</script>
</html>
后端:
Spring Security 配置指定该 URL 作为登录处理入口。
@Configuration
@EnableMethodSecurity
// 配置spring的容器
public class SecurityConfig {
@Bean// 安全过滤器链Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity ,CorsConfigurationSource configurationSource ) throws Exception { //httpSecurity方法参数注入Bean
return httpSecurity
// 配置自己的登录页面
.formLogin( (formLogin) ->{
formLogin.loginProcessingUrl("/login") // 登录账户密码往哪个地址提交
})
.build();
}
}
由于现在是前后端分离的,所以拿不到CSRF,因此我们需要先禁用CSRF:
禁用 CSRF:
在基于 Token 的认证中,由于不依赖 Session,因此通常会关闭 CSRF 保护。可以在 HttpSecurity 中调用 .csrf().disable()
来实现这一点。
@Configuration
@EnableMethodSecurity
// 配置spring的容器
public class SecurityConfig {
@Bean// 安全过滤器链Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity ,CorsConfigurationSource configurationSource ) throws Exception { //httpSecurity方法参数注入Bean
return httpSecurity
// 配置自己的登录页面
.formLogin( (formLogin) ->{
formLogin.loginProcessingUrl("/login") // 登录账户密码往哪个地址提交
})
.csrf((csrf)->{
// 禁止csrf跨站请求,禁用之后,肯定就不安全了,有csrf网络攻击的风险,后续加入jwt是可以防御的
csrf.disable();
})
.build();
}
}
配置 formLogin.loginProcessingUrl("/login")
实际上告诉 Spring Security:
- 当收到指向
/login
的 POST 请求时,Spring Security 内部的过滤器(例如UsernamePasswordAuthenticationFilter
)会拦截这个请求,并自动处理用户的认证逻辑。 - 你不需要在自己的 Controller 中实现这个
/login
接口,因为 Spring Security 会接管并执行用户名、密码的验证,以及后续的成功或失败处理(如果你配置了相应的successHandler
和failureHandler
)。
存在跨域问题:
协议不同会跨域 https://localhost:8080 http://localhost:8080
- 端口不同会跨域:http://localhost:10492 http://localhost:8080
- 域名不同会跨域:http://bjpowernode.com http://baidu.com
三个里面有任何一个不同,都是跨域,跨域是浏览器不允许的,浏览器是为了安全,不允许你跨域访问
跨域资源共享(CORS)配置
- 允许跨域访问:
前后端分离架构中,前端通常与后端不在同一个域名下,因此必须在后端配置 CORS 策略。可以通过在 HttpSecurity 配置中调用.cors()
方法通常需要自己定义一个 CorsConfigurationSource Bean。在自己定义 CorsConfigurationSource Bean当中我们需要返回CorsConfigurationSource(接口)的实现类——通常情况下是UrlBasedCorsConfigurationSource
@Configuration
@EnableMethodSecurity
// 配置spring的容器
public class SecurityConfig {
/**
* 配置跨域
* @return
*/
@Bean
public CorsConfigurationSource configurationSource() {
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
// 跨域配置
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("*")); // 允许的请求来源
corsConfiguration.setAllowedMethods(Arrays.asList("*")); // 允许的请求方法
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));// 允许的请求头
// 注册配置
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration);
return urlBasedCorsConfigurationSource;
}
@Bean// 安全过滤器链Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity ,CorsConfigurationSource configurationSource ) throws Exception { //httpSecurity方法参数注入Bean
return httpSecurity
// 配置自己的登录页面
.formLogin( (formLogin) ->{
formLogin.loginProcessingUrl("/login") // 登录账户密码往哪个地址提交
})
.csrf((csrf)->{
// 禁止csrf跨站请求,禁用之后,肯定就不安全了,有csrf网络攻击的风险,后续加入jwt是可以防御的
csrf.disable();
})
.cors((cors)->{ // 允许前端跨域访问
cors.configurationSource( configurationSource);
})
.build();
}
}
凭证接收和验证: Spring Security框架使用 UsernamePasswordAuthenticationFilter
拦截请求,获取用户提交的账号和密码。
用户信息查询:
UserServiceImpl重写loadUserByUsername方法
从数据库中加载用户信息,返回一个包含用户状态和权限信息的 UserDetails
(在本例中为 TUser 对象)。
重写loadUserByUsername方法需要
service接口,需要继承springsecurity框架的UserDetailsService接口
// 我们的处理登录的service接口,需要继承springsecurity框架的UserDetailsService接口
public interface UserService extends UserDetailsService {
}
service实现类
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名查询数据库
TUser user = userMapper.selectByLoginAct(username);
if (user == null){
throw new UsernameNotFoundException("用户不存在");
}
return user; // 实现了UserDetails接口,包含所有字段
}
状态检查和密码比对: 框架会检查用户对象的状态(例如账户是否有效)以及比对密码是否匹配。
登录成功或失败的处理(处理器):
根据认证结果决定登录成功后的跳转(默认跳转到上一次请求的地址或项目根路径)或失败后的处理(重定向到 /login?error
),但在前后端分离场景中,需要通过自定义 Handler 返回 JSON 格式的响应。
@Configuration
@EnableMethodSecurity
// 配置spring的容器
public class SecurityConfig {
@Bean// 安全过滤器链Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity ,CorsConfigurationSource configurationSource ) throws Exception { //httpSecurity方法参数注入Bean
return httpSecurity
// 配置自己的登录页面
.formLogin( (formLogin) ->{
formLogin.loginProcessingUrl("/login") // 登录账户密码往哪个地址提交
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHandler); // 登录失败的回调
})
.build();
}
}
MyAuthenticationSuccessHandle(登录成功的处理器):
@Component
public class MyAuthenticationSuccessHandle implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
R result = R.builder().code(200).msg("登录成功").info(authentication.getPrincipal()).build();
response.setContentType("application/json;charset=utf-8");
String json = JSONUtil.toJsonStr(result);
response.getWriter().write(json);
}
}
MyAuthenticationFailHandle(登录失败的处理器):
@Component
public class MyAuthenticationFailHandle implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
R result = R.builder().code(500).msg("登录失败").info(exception.getMessage()).build();
response.setContentType("application/json;charset=utf-8");
String json = JSONUtil.toJsonStr(result);
response.getWriter().write(json);
}
}
退出成功的处理器
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
R result = R.builder().code(200).msg("退出成功").info(authentication.getPrincipal()).build();
response.setContentType("application/json;charset=utf-8");
String json = JSONUtil.toJsonStr(result);
response.getWriter().write(json);
}
}
在 Spring Security 中,退出操作(logout)的设计逻辑与登录有所不同。登录过程涉及用户凭证验证、状态检查和密码匹配等环节,这些都有可能失败,所以需要提供失败处理器(如登录失败处理器)。而退出操作本质上只是清理会话、清空 SecurityContext 等动作,通常不会出现“失败”的情况。因此,框架只提供了退出成功处理器(LogoutSuccessHandler),而没有专门的退出失败处理器。
无状态认证的考虑:
由于不再使用传统 Session 记录用户状态,后续访问其他需要认证的接口时会提示未登录,此时通常会引入 JWT 等机制来维持用户认证状态。
没有session、前端cookie中也不会存储sessionid;那么这样的话,用户状态怎么保持呢?
需要使用我们下面介绍的jwt解决该问题;
不分离的:Tomcat 【thymeleaf <----> (controller、sucurity)】
前后端分离:Nginx 【Vue】 <----jwt----> Tomcat 【 (controller、sucurity) 】