文章目录
一、介绍
1、简介
Spring Security 是一个强大的安全框架,旨在保护基于 Spring 的应用程序。它提供了一整套全面的安全功能,包括认证、授权、以及防护常见安全攻击的机制。
2、核心概念
-
认证(Authentication)
认证是验证用户身份的过程。在 Spring Security 中,用户在登录时提交凭证(如用户名和密码)。这些凭证会通过一系列过滤器进行处理和验证。如果凭证正确,系统会确认用户的身份并授予相应的权限。
-
授权(Authorization)
授权是确定已认证用户是否有权限执行特定操作或访问特定资源的过程。Spring Security 通过配置访问控制规则来实现授权。授权通常基于角色(Role)和权限(Authority)。例如,只有具有管理员角色的用户才能访问管理页面。
-
过滤器(Filter)
Spring Security 使用一系列过滤器来拦截 HTTP 请求,并在请求到达应用程序之前执行安全逻辑。每个过滤器都有特定的职责,例如处理认证、授权和会话管理。这些过滤器由 FilterChainProxy 管理,并按顺序执行,确保所有安全检查在请求处理前完成。
-
安全上下文(Security Context)
安全上下文包含当前用户的安全信息,包括用户的认证信息和权限。Spring Security 使用 SecurityContextHolder 来存储和访问安全上下文。每次请求到达服务器时,安全上下文会被加载并在整个请求期间保持有效,以确保所有的安全检查都基于最新的用户信息。
3、主要功能
-
表单登录(Form-based Authentication)
提供自定义登录页面和登录处理机制。
处理用户提交的登录表单,并进行身份验证。 -
HTTP 基础认证(HTTP Basic Authentication)
使用 HTTP 基础认证头(Authorization)来进行身份验证。
常用于简单的 RESTful API 安全。 -
LDAP 支持
集成 LDAP(轻量目录访问协议)进行用户认证和授权。
-
方法级别安全(Method Level Security)
使用注解(如 @PreAuthorize, @Secured)在方法级别进行权限控制。
允许对服务层方法进行细粒度的安全控制。 -
CSRF 防护(Cross-Site Request Forgery Protection)
防护跨站请求伪造攻击。
默认启用,可以根据需要进行配置。 -
会话管理(Session Management)
控制用户会话,包括会话超时和并发会话控制。
提供防止会话固定攻击的机制。 -
安全事件和监听器(Security Events and Listeners)
处理并记录安全相关的事件,如登录成功、登录失败等。
提供监听器机制,允许开发者自定义处理这些事件。
4、处理流程
Spring Security 的处理流程主要包括以下几个步骤:
-
(1)请求进入过滤器链
用户发起一个 HTTP 请求,首先被 Spring Security 的过滤器链拦截。
-
(2)安全上下文初始化
SecurityContextPersistenceFilter 过滤器加载或创建一个新的安全上下文,确保请求期间的安全信息可用。
-
(3)认证过程
例如,UsernamePasswordAuthenticationFilter 处理登录请求,提取用户名和密码,并将其传递给 AuthenticationManager 进行验证。
AuthenticationManager 会使用一个或多个 AuthenticationProvider 验证用户凭证。如果认证成功,用户信息会存储在安全上下文中。 -
(4)授权过程
请求经过 FilterSecurityInterceptor,该过滤器会检查用户是否有权限访问请求的资源。如果没有权限,系统会拒绝访问并返回相应的错误信息。
-
(5)会话管理
SessionManagementFilter 负责管理用户会话,防止会话固定攻击和控制并发会话数。
-
(6)请求处理
如果认证和授权都通过,过滤器链将请求传递给应用程序的控制器进行正常的业务处理。
-
(7)响应处理
在响应返回之前,过滤器链会执行一些清理工作(如保存安全上下文),确保会话和安全上下文的一致性。
通过以上流程,Spring Security 确保每个 HTTP 请求都经过严格的安全检查,从而保护应用程序的安全。
二、Spring Security实现权限
Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。
如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。
这里面我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter
负责登录认证,FilterSecurityInterceptor
负责权限授权。
1、添加依赖
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、执行顺序和代码执行流程
(1)用户登录
SpringSecurityConf
在 SpringSecurityConf
配置中,添加了自定义的过滤器 PostJsonAuthenticationFilter
和 TokenAuthorizationFilter
。
/**
* @description: TODO 定义了一系列安全策略,包括禁用 CSRF 保护、配置会话管理策略、配置白名单、添加自定义过滤器等
*
* @author: LLong
* @date: 2024/03/29 17:54:41
* @param http
* @return: void
**/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置了异常处理,指定了身份验证失败时的处理方式,即当用户未经身份验证访问受保护的资源时,会调用 authenticationEntryPoint 来处理
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
// .accessDeniedHandler(accessDeniedHandler);
//禁用了 CSRF 保护,并启用了 HTTP Basic 认证
http.csrf().disable().httpBasic()
//设置会话管理策略为无状态,即不创建会话
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置一组 URL 的白名单,这些 URL(AUTH_WHITELIST) 不需要身份验证即可访问
.and().authorizeRequests().antMatchers(AUTH_WHITELIST).permitAll()
//获取标有注解 AnonymousAccess 的访问路径,不需要身份验证即可访问
.and().authorizeRequests().antMatchers(getAnonymousUrls()).permitAll()
.anyRequest().authenticated();
//添加登陆过滤器,用来处理基于令牌的身份验证
http.addFilterBefore(createTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//添加权限鉴定过滤器 authorizationFilter,用于进行授权验证。
http.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
/**
* @description: TODO 登陆过滤器
* @author: LLong
* @date: 2024/03/29 18:00:31
* @param
* @return: PostJsonAuthenticationFilter
**/
private PostJsonAuthenticationFilter createTokenAuthenticationFilter() throws Exception {
// 创建PostJsonAuthenticationFilter实例,传入AuthenticationManager和ObjectMapper
PostJsonAuthenticationFilter postJsonAuthenticationFilter =
new PostJsonAuthenticationFilter(authenticationManagerBean(), objectMapper);
// 设置身份验证成功处理器
postJsonAuthenticationFilter.setAuthenticationSuccessHandler(postJsonAuthenticationSuccessHandler);
// 设置身份验证失败处理器
postJsonAuthenticationFilter.setAuthenticationFailureHandler(postJsonAuthenticationFailureHandler);
return postJsonAuthenticationFilter;
}
/**
* @description: TODO 配置身份验证管理器,用于验证用户的身份
* @author: LLong
* @date: 2024/03/29 18:01:38
* @param auth
* @return: void
**/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
// 设置使用自己实现的userDetailsService(loadUserByusername)
.userDetailsService(userDetailsService)
// 设置密码加密方式(自定义)
.passwordEncoder(ssha512PasswordEncoder());
}
PostJsonAuthenticationFilter
PostJsonAuthenticationFilter
负责处理登录请求。其主要方法 attemptAuthentication
会被调用:
/**
* @description: TODO 处理用户身份验证
* @author: LLong
* @date: 2024/03/29 18:16:35
* @param request
* @param response
* @return: Authentication
**/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 从请求体中读取JSON数据并映射到UserEntity对象
AuthLoginVO authLoginVO = objectMapper.readValue(request.getInputStream(), AuthLoginVO.class);
// 创建一个UsernamePasswordAuthenticationToken用于进行身份验证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(authLoginVO.getUsername(), authLoginVO.getPassword());
// 进行身份验证
return authenticationManager.authenticate(authenticationToken);
}
}
}
UserDetailsServiceImpl
在 attemptAuthentication
方法中,AuthenticationManager
调用 UserDetailsServiceImpl
的 loadUserByUsername
方法来加载用户的详细信息:(该方法是自定义的校验逻辑)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 创建查询条件,用于根据用户名查询用户信息
LambdaQueryWrapper<MailUserEntity> lqw = new LambdaQueryWrapper<>();
lqw.eq(MailUserEntity::getUsername, username);
// 使用查询条件从数据库中查询用户信息
MailUserEntity user = mailUserMapper.selectOne(lqw);
if (user == null) {
// 如果用户不存在,抛出 UsernameNotFoundException 异常
throw new UsernameNotFoundException("用户不存在");
}
// 获取用户的权限集合
Collection<GrantedAuthority> authList = getAuthorities();
// 创建自定义的 AuthLoginVO 实例,包含用户名、密码、姓名和权限集合
AuthLoginVO authLoginVO = new AuthLoginVO(user.getUsername(), user.getPassword(), user.getName(), authList);
// 返回包含用户信息和权限的 AuthLoginVO 对象
return authLoginVO;
}
PostJsonAuthenticationSuccessHandler
如果登录成功,PostJsonAuthenticationFilter
将调用 PostJsonAuthenticationSuccessHandler
的 onAuthenticationSuccess
方法:
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
// 从 authentication 对象中获取已认证的用户信息
Object principal = authentication.getPrincipal();
// 检查 principal 是否为 AuthLoginVO 实例
if (principal instanceof AuthLoginVO) {
AuthLoginVO user = (AuthLoginVO) authentication.getPrincipal(); // 将 principal 强制转换为 AuthLoginVO 类型
// 从 Redis 中获取与用户名关联的 uuid
String uuid = redisTemplate.opsForValue().get(USER_USERNAME_PRE + user.getUsername());
// 如果 uuid 不存在,则生成一个新的 uuid
if (StringUtils.isEmpty(uuid)) {
uuid = UUID.fastUUID().toString(true);
}
// 将用户名和 uuid 存储到 Redis 中,并设置过期时间为 12 小时
redisTemplate.opsForValue().set(USER_USERNAME_PRE + user.getUsername(), uuid, EXPIRE_TIME_12_HOURS, TimeUnit.SECONDS);
// 将 uuid 和用户信息存储到 Redis 中,并设置过期时间为 12 小时
redisTemplate.opsForValue().set(USER_TOKEN_PRE + uuid, objectMapper.writeValueAsString(user), EXPIRE_TIME_12_HOURS, TimeUnit.SECONDS);
// 设置 HTTP 响应状态为 200
response.setStatus(200);
// 设置响应内容类型为 JSON,字符集为 UTF-8
response.setContentType("application/json;charset=UTF-8");
// 创建一个新的 AuthLoginVO 对象,用于响应
AuthLoginVO authLoginVO = new AuthLoginVO();
authLoginVO.setId(user.getId()); // 设置用户 ID
authLoginVO.setUsername(user.getUsername()); // 设置用户名
authLoginVO.setToken(uuid); // 设置用户 token
// 将用户权限信息转换为 Set 并设置到 authLoginVO 中(此代码被注释掉)
// authLoginVO.setPermissions(AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
// 将 authLoginVO 对象转换为 JSON 并写入响应
response.getWriter().print(objectMapper.writeValueAsString(new Result<>(authLoginVO)));
// 刷新缓冲区,确保所有数据被写入
response.flushBuffer();
}
}
PostJsonAuthenticationFailureHandler
如果登录失败,PostJsonAuthenticationFilter
将调用 PostJsonAuthenticationFailureHandler
的 onAuthenticationFailure
方法:
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
// 设置 HTTP 响应状态码为 200(OK)
response.setStatus(200);
// 设置响应内容类型为 JSON,字符集为 UTF-8
response.setContentType("application/json;charset=UTF-8");
// 将登录失败的结果转换为 JSON 并写入响应
response.getWriter().print(JSONUtil.toJsonStr(new Result<>(LOGIN_FAIL)));
// 刷新缓冲区,确保所有数据被写入
response.flushBuffer();
}
(2)访问受保护资源
TokenAuthorizationFilter
在请求访问受保护资源时,TokenAuthorizationFilter
会被调用:
/**
* @description: TODO 过滤器 基于令牌设置用户的认证信息
* 自定义的过滤器(Filter),用于在请求中提取令牌(token),然后基于令牌设置用户的认证信息
* @author: LLong
* @date: 2024/03/29 18:06:38
* @param request
* @param response
* @param chain
* @return: void
**/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 从请求头中获取名为 "token" 的令牌
String token = request.getHeader("token");
if (!StringUtils.isEmpty(token)) {
// 从redis中获取用户名
String user = redisTemplate.opsForValue().get(USER_TOKEN_PRE + token);
//从 Redis 中获取到的用户信息 JSON 字符串反序列化为 AuthLoginVO 对象
AuthLoginVO authUser = objectMapper.readValue(user, AuthLoginVO.class);
if (SecurityContextHolder.getContext().getAuthentication() == null && Objects.nonNull(authUser)) {
// 创建一个 UsernamePasswordAuthenticationToken 对象,用于表示用户的认证信息。
// 其中 authUser 是从 Redis 中获取到的用户信息对象,authUser.getAuthorities() 返回用户的权限集合
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将创建的认证信息对象设置到当前的安全上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
//刷新token
redisTemplate.expire(USER_USERNAME_PRE + authUser.getUsername(), EXPIRE_TIME_12_HOURS,
TimeUnit.SECONDS);
redisTemplate.expire(USER_TOKEN_PRE + token, EXPIRE_TIME_12_HOURS, TimeUnit.SECONDS);
}
}
//调用 chain.doFilter(request, response) 继续执行过滤器链中的下一个过滤器
chain.doFilter(request, response);
}
TokenAuthenticationEntryPoint
如果请求没有进行身份验证或令牌无效,TokenAuthenticationEntryPoint
会被调用,返回未授权的响应:
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 设置 HTTP 响应状态码为 200(OK)
response.setStatus(200);
// 设置响应内容类型为 JSON,字符集为 UTF-8
response.setContentType("application/json;charset=UTF-8");
// 将未授权的结果转换为 JSON 并写入响应
response.getWriter().print(JSONUtil.toJsonStr(new Result<>(UNAUTHORIZED)));
// 刷新缓冲区,确保所有数据被写入
response.flushBuffer();
}
总结
1、用户登录
- 用户提交登录表单,
PostJsonAuthenticationFilter
处理请求,提取用户名和密码,进行身份验证。 UserDetailsServiceImpl
加载用户详细信息,并验证用户的合法性。- 登录成功后,
PostJsonAuthenticationSuccessHandler
生成令牌,存储到 Redis 中,并返回用户信息和令牌。 - 登录失败后,
PostJsonAuthenticationFailureHandler
返回失败消息。
2、访问受保护资源
- 用户请求受保护资源,
TokenAuthorizationFilter
验证请求中的令牌,确保用户身份有效。 - 如果令牌无效或未提供,
TokenAuthenticationEntryPoint
返回未授权消息。
完整源码
1、SpringSecurityConf
package com.cx.cxBasic.common.config.authorization;
import com.cx.cxBasic.common.config.authorization.handler.PostJsonAuthenticationFailureHandler;
import com.cx.cxBasic.common.config.authorization.handler.PostJsonAuthenticationSuccessHandler;
import com.cx.cxBasic.common.config.authorization.handler.TokenAuthenticationEntryPoint;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @Author: LLong
* @CreateTime: 2023-12-12 18:36
* @Description: TODO Spring Security权限配置类
* @Version: 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SpringSecurityConf extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST =
{"/login"};
@Resource
private UserDetailsService userDetailsService;
@Resource
private ObjectMapper objectMapper;
/**
* 权限鉴定过滤器
*/
@Resource
private TokenAuthorizationFilter authorizationFilter;
// /**
// * 权限不足结果处理
// */
// @Resource
// private TokenAccessDeniedHandler accessDeniedHandler;
/**
* 未登录结果处理
*/
@Resource
private TokenAuthenticationEntryPoint authenticationEntryPoint;
/**
* 登陆成功处理
*/
@Resource
private PostJsonAuthenticationSuccessHandler postJsonAuthenticationSuccessHandler;
/**
* 登陆失败处理
*/
@Resource
private PostJsonAuthenticationFailureHandler postJsonAuthenticationFailureHandler;
@Resource
private RequestMappingHandlerMapping requestMappingHandlerMapping;
/**
* @description: TODO 定义了一系列安全策略,包括禁用 CSRF 保护、配置会话管理策略、配置白名单、添加自定义过滤器等
*
* @author: LLong
* @date: 2024/03/29 17:54:41
* @param http
* @return: void
**/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置了异常处理,指定了身份验证失败时的处理方式,即当用户未经身份验证访问受保护的资源时,会调用 authenticationEntryPoint 来处理
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
// .accessDeniedHandler(accessDeniedHandler);
//禁用了 CSRF 保护,并启用了 HTTP Basic 认证
http.csrf().disable().httpBasic()
//设置会话管理策略为无状态,即不创建会话
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置一组 URL 的白名单,这些 URL(AUTH_WHITELIST) 不需要身份验证即可访问
.and().authorizeRequests().antMatchers(AUTH_WHITELIST).permitAll()
//获取标有注解 AnonymousAccess 的访问路径,不需要身份验证即可访问
.and().authorizeRequests().antMatchers(getAnonymousUrls()).permitAll()
.anyRequest().authenticated();
//添加登陆过滤器,用来处理基于令牌的身份验证
http.addFilterBefore(createTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//添加权限鉴定过滤器 authorizationFilter,用于进行授权验证。
http.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* @description: TODO 登陆过滤器
* @author: LLong
* @date: 2024/03/29 18:00:31
* @param
* @return: PostJsonAuthenticationFilter
**/
private PostJsonAuthenticationFilter createTokenAuthenticationFilter() throws Exception {
// 创建PostJsonAuthenticationFilter实例,传入AuthenticationManager和ObjectMapper
PostJsonAuthenticationFilter postJsonAuthenticationFilter =
new PostJsonAuthenticationFilter(authenticationManagerBean(), objectMapper);
// 设置身份验证成功处理器
postJsonAuthenticationFilter.setAuthenticationSuccessHandler(postJsonAuthenticationSuccessHandler);
// 设置身份验证失败处理器
postJsonAuthenticationFilter.setAuthenticationFailureHandler(postJsonAuthenticationFailureHandler);
return postJsonAuthenticationFilter;
}
/**
* @description: TODO 密码BCrypt加密
* 密码加密器,在授权时,框架为我们解析用户名密码时,密码会通过加密器加密在进行比较 将密码加密器交给spring管理,在注册时,密码也是需要加密的,再存入数据库中 用户输入登录的密码用加密器加密,再与数据库中查询到的用户密码比较
* @author: LLong
* @date: 2024/03/29 18:00:38
* @param
* @return: BCryptPasswordEncoder 加密器
**/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
// 密码加密
return new BCryptPasswordEncoder();
}
/**
* @description: TODO 配置身份验证管理器,用于验证用户的身份
* @author: LLong
* @date: 2024/03/29 18:01:38
* @param auth
* @return: void
**/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
// 设置使用自己实现的userDetailsService(loadUserByusername)
.userDetailsService(userDetailsService)
// 设置密码加密方式
.passwordEncoder(bCryptPasswordEncoder());
}
/**
* 获取标有注解 AnonymousAccess 的访问路径
*/
/**
* @description: TODO 获取标有注解 AnonymousAccess 的可匿名访问的路径
* @author: LLong
* @date: 2024/03/29 18:03:07
* @param
* @return: String
**/
private String[] getAnonymousUrls() {
// 获取所有的 RequestMapping
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
Set<String> allAnonymousAccess = new HashSet<>();
// 循环 RequestMapping
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
HandlerMethod value = infoEntry.getValue();
// 获取方法上 AnonymousAccess 类型的注解
AnonymousAccess methodAnnotation = value.getMethodAnnotation(AnonymousAccess.class);
// 如果方法上标注了 AnonymousAccess 注解,就获取该方法的访问全路径
if (methodAnnotation != null) {
allAnonymousAccess.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
}
}
return allAnonymousAccess.toArray(new String[0]);
}
/**
* 加密密码测试
*/
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println(encode);
}
}
2、PostJsonAuthenticationFilter
package com.cx.cxBasic.common.config.authorization;
import com.cx.cxBasic.common.exception.ServiceException;
import com.cx.cxBasic.entity.UserEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author: LLong
* @CreateTime: 2023-12-12 19:14
* @Description: TODO 登陆拦截器
* @Version: 1.0
*/
@Slf4j
public class PostJsonAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper objectMapper;
private final AuthenticationManager authenticationManager;
/**
* @description: TODO 配置过滤器的一些参数
* 过滤器处理的请求的 URL、请求方法,并将所需的身份验证管理器和 JSON 序列化对象传递给过滤器
* @author: LLong
* @date: 2024/03/29 18:13:39
* @param authenticationManager
* @param objectMapper
* @return: null
**/
public PostJsonAuthenticationFilter(AuthenticationManager authenticationManager,
ObjectMapper objectMapper) {
super(new AntPathRequestMatcher("/login", HttpMethod.POST.toString()));
this.authenticationManager = authenticationManager;
this.objectMapper = objectMapper;
}
/**
* @description: TODO 处理用户身份验证
* @author: LLong
* @date: 2024/03/29 18:16:35
* @param request
* @param response
* @return: Authentication
**/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
// 从请求体中读取JSON数据并映射到UserEntity对象
UserEntity users = objectMapper.readValue(request.getInputStream(), UserEntity.class);
// 模仿usernamePasswordAuthenticationFilter的方式,创建一个usernamePasswordAuthenticationToken用于进行身份验证
// 创建一个UsernamePasswordAuthenticationToken用于进行身份验证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(users.getUsername(), users.getPassword());
// 进行身份验证
return authenticationManager.authenticate(authenticationToken);
} catch (IOException e) {
// 处理IO异常,例如读取请求体失败
log.error("登录异常: {}", e.getMessage(), e);
// 抛出usernameNotFoundException异常,表示身份验证失败
throw new ServiceException("身份验证失败");
}
}
}
3、UserDetailsServiceImpl
package com.cx.cxBasic.common.config.authorization;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cx.cxBasic.entity.vamil.MailUserEntity;
import com.cx.cxBasic.mapper.vmail.MailUserMapper;
import com.cx.cxBasic.model.vo.admin.AuthLoginVO;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @Author: LLong
* @CreateTime: 2024/06/26 20:32
* @Description: TODO
* @Version: 1.0
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private MailUserMapper mailUserMapper;
/** * 根据用户名获取用户 - 用户的角色、权限等信息 */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 创建查询条件,用于根据用户名查询用户信息
LambdaQueryWrapper<MailUserEntity> lqw = new LambdaQueryWrapper<>();
lqw.eq(MailUserEntity::getUsername, username);
// 使用查询条件从数据库中查询用户信息
MailUserEntity user = mailUserMapper.selectOne(lqw);
if (user == null) {
// 如果用户不存在,抛出 UsernameNotFoundException 异常
throw new UsernameNotFoundException("用户不存在");
}
// 获取用户的权限集合
Collection<GrantedAuthority> authList = getAuthorities();
// 创建自定义的 AuthLoginVO 实例,包含用户名、密码、姓名和权限集合
AuthLoginVO authLoginVO = new AuthLoginVO(user.getUsername(), user.getPassword(), user.getName(), authList);
UserDetails userDetails = authLoginVO;
return userDetails;
}
/** * 获取用户的角色权限,为了降低实验的难度,这里去掉了根据用户名获取角色的步骤 * @param * @return */
private Collection<GrantedAuthority> getAuthorities(){
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
return authList;
}
}
4、PostJsonAuthenticationSuccessHandler
package com.cx.cxBasic.common.config.authorization.handler;
import cn.hutool.core.lang.UUID;
import com.cx.cxBasic.common.Result;
import com.cx.cxBasic.model.vo.admin.AuthLoginVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import static com.cx.recordLibrary.common.constant.RedisKeyConstants.*;
/**
* @Author: LLong
* @CreateTime: 2023-12-14 11:50
* @Description: TODO 登陆成功处理器
* @Version: 1.0
*/
@Component
public class PostJsonAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
// 从 authentication 对象中获取已认证的用户信息
Object principal = authentication.getPrincipal();
// 检查 principal 是否为 AuthLoginVO 实例
if (principal instanceof AuthLoginVO) {
AuthLoginVO user = (AuthLoginVO) authentication.getPrincipal(); // 将 principal 强制转换为 AuthLoginVO 类型
// 从 Redis 中获取与用户名关联的 uuid
String uuid = redisTemplate.opsForValue().get(USER_USERNAME_PRE + user.getUsername());
// 如果 uuid 不存在,则生成一个新的 uuid
if (StringUtils.isEmpty(uuid)) {
uuid = UUID.fastUUID().toString(true);
}
// 将用户名和 uuid 存储到 Redis 中,并设置过期时间为 12 小时
redisTemplate.opsForValue().set(USER_USERNAME_PRE + user.getUsername(), uuid, EXPIRE_TIME_12_HOURS, TimeUnit.SECONDS);
// 将 uuid 和用户信息存储到 Redis 中,并设置过期时间为 12 小时
redisTemplate.opsForValue().set(USER_TOKEN_PRE + uuid, objectMapper.writeValueAsString(user), EXPIRE_TIME_12_HOURS, TimeUnit.SECONDS);
// 设置 HTTP 响应状态为 200
response.setStatus(200);
// 设置响应内容类型为 JSON,字符集为 UTF-8
response.setContentType("application/json;charset=UTF-8");
// 创建一个新的 AuthLoginVO 对象,用于响应
AuthLoginVO authLoginVO = new AuthLoginVO();
authLoginVO.setId(user.getId()); // 设置用户 ID
authLoginVO.setUsername(user.getUsername()); // 设置用户名
authLoginVO.setToken(uuid); // 设置用户 token
// 将用户权限信息转换为 Set 并设置到 authLoginVO 中(此代码被注释掉)
// authLoginVO.setPermissions(AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
// 将 authLoginVO 对象转换为 JSON 并写入响应
response.getWriter().print(objectMapper.writeValueAsString(new Result<>(authLoginVO)));
// 刷新缓冲区,确保所有数据被写入
response.flushBuffer();
}
}
}
5、PostJsonAuthenticationFailureHandler
package com.cx.cxBasic.common.config.authorization.handler;
import cn.hutool.json.JSONUtil;
import com.cx.cxBasic.common.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.cx.cxBasic.common.exception.ResultCode.LOGIN_FAIL;
/**
* @BelongsProject: cx-basic-framework
* @Author: LLong
* @CreateTime: 2023-12-14 11:48
* @Description: TODO 登陆失败处理器
* @Version: 1.0
*/
@Component
public class PostJsonAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
// 设置 HTTP 响应状态码为 200(OK)
response.setStatus(200);
// 设置响应内容类型为 JSON,字符集为 UTF-8
response.setContentType("application/json;charset=UTF-8");
// 将登录失败的结果转换为 JSON 并写入响应
response.getWriter().print(JSONUtil.toJsonStr(new Result<>(LOGIN_FAIL)));
// 刷新缓冲区,确保所有数据被写入
response.flushBuffer();
}
}
6、TokenAuthorizationFilter
package com.cx.cxBasic.common.config.authorization;
import com.cx.cxBasic.model.vo.admin.AuthLoginVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static com.cx.cxBasic.common.constant.RedisKeyConstants.*;
/**
* @BelongsProject: cx-record-library
* @BelongsPackage: com.cx.recordLibrary.common.config.authorization
* @Author: LLong
* @CreateTime: 2023-12-12 18:40
* @Description: TODO 请求认证拦截器
* @Version: 1.0
*/
@Slf4j
@Component
public class TokenAuthorizationFilter extends OncePerRequestFilter {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
/**
* @description: TODO 过滤器 基于令牌设置用户的认证信息
* 自定义的过滤器(Filter),用于在请求中提取令牌(token),然后基于令牌设置用户的认证信息
* @author: LLong
* @date: 2024/03/29 18:06:38
* @param request
* @param response
* @param chain
* @return: void
**/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 从请求头中获取名为 "token" 的令牌
String token = request.getHeader("token");
if (!StringUtils.isEmpty(token)) {
// 从redis中获取用户名
String user = redisTemplate.opsForValue().get(USER_TOKEN_PRE + token);
//从 Redis 中获取到的用户信息 JSON 字符串反序列化为 AuthLoginVO 对象
AuthLoginVO authUser = objectMapper.readValue(user, AuthLoginVO.class);
if (SecurityContextHolder.getContext().getAuthentication() == null && Objects.nonNull(authUser)) {
// 创建一个 UsernamePasswordAuthenticationToken 对象,用于表示用户的认证信息。
// 其中 authUser 是从 Redis 中获取到的用户信息对象,authUser.getAuthorities() 返回用户的权限集合
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将创建的认证信息对象设置到当前的安全上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
//刷新token
redisTemplate.expire(USER_USERNAME_PRE + authUser.getUsername(), EXPIRE_TIME_12_HOURS,
TimeUnit.SECONDS);
redisTemplate.expire(USER_TOKEN_PRE + token, EXPIRE_TIME_12_HOURS, TimeUnit.SECONDS);
}
}
//调用 chain.doFilter(request, response) 继续执行过滤器链中的下一个过滤器
chain.doFilter(request, response);
}
}
7、TokenAuthenticationEntryPoint
package com.cx.cxBasic.common.config.authorization.handler;
import cn.hutool.json.JSONUtil;
import com.cx.cxBasic.common.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.cx.cxBasic.common.exception.ResultCode.UNAUTHORIZED;
/**
* @BelongsProject: cx-basic-framework
* @Author: LLong
* @CreateTime: 2023-12-14 11:51
* @Description: TODO 未登录处理器
* @Version: 1.0
*/
@Component
public class TokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 设置 HTTP 响应状态码为 200(OK)
response.setStatus(200);
// 设置响应内容类型为 JSON,字符集为 UTF-8
response.setContentType("application/json;charset=UTF-8");
// 将未授权的结果转换为 JSON 并写入响应
response.getWriter().print(JSONUtil.toJsonStr(new Result<>(UNAUTHORIZED)));
// 刷新缓冲区,确保所有数据被写入
response.flushBuffer();
}
}
8、AnonymousAccess
package com.cx.cxBasic.common.config.authorization;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @BelongsProject: cx-basic-framework
* @Author: LLong
* @CreateTime: 2023-12-14 11:52
* @Description: TODO
* @Version: 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AnonymousAccess {
}
9、AuthLoginVO
package com.cx.cxBasic.model.vo.admin;
import com.cx.cxBasic.common.config.authorization.CustomAuthorityDeserializer;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.math.BigInteger;
import java.util.Collection;
/**
* @Author: LLong
* @CreateTime: 2023-12-12 16:38
* @Description: TODO token用户登录信息
* @Version: 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthLoginVO implements UserDetails {
/**
* 用户ID 主键id
*/
private BigInteger id;
/**
* 用户名
*/
private String username ;
/**
* 用户密码
*/
private String password ;
/**
* 角色代码
*/
private Integer code;
/**
* 角色名
*/
private String roleName ;
/**
* 部门名
*/
private String deptName ;
/**
* 令牌
*/
private String token ;
@JsonDeserialize(using = CustomAuthorityDeserializer.class)
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
}
10、CustomAuthorityDeserializer
package com.cx.cxBasic.common.config.authorization;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @Author: LLong
* @CreateTime: 2023-12-13 11:19
* @Description: TODO GrantedAuthority集合json解析
* @Version: 1.0
*/
public class CustomAuthorityDeserializer extends JsonDeserializer<List<GrantedAuthority>> {
@Override
public List<GrantedAuthority> deserialize(JsonParser jp, DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode jsonNode = mapper.readTree(jp);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));
}
return grantedAuthorities;
}
}