spring-boot-starter-security不够灵活, 项目需要二次开发。下面展示一个替换security认证功能的替代方案。
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final SecurityProperties securityProperties;
private final GlobalExceptionHandler globalExceptionHandler;
private final OAuth2TokenApi oAuth2TokenApi;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
if (StrUtil.isNotBlank(token)) {
Integer userType = SecurityFrameworkUtils.getLoginUserType(request);
try {
// 基于token构建登录用户
LoginUser loginUser = buildLoginUserByToken(token, userType);
// 设置当前用户
if (loginUser != null) {
SecurityFrameworkUtils.setLoginUser(loginUser, request);
}
} catch (Throwable e) {
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, e);
ServletUtils.writeJSON(response, result);
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 根据token构建用户
*/
private LoginUser buildLoginUserByToken(String token, Integer userType) {
// 校验token
OAuth2AccessTokenCheckRespDTO tokenCheckRespDTO = oAuth2TokenApi.checkAccessToken(token);
if (tokenCheckRespDTO == null) {
return null;
}
// 校验用户类型
if (ObjectUtil.notEqual(tokenCheckRespDTO.getUserType(), userType)) {
throw new AccessDeniedException("用户类型错误");
}
// 构建登录用户
return LoginUser.builder()
.userType(userType)
.id(tokenCheckRespDTO.getUserId())
.tenantId(tokenCheckRespDTO.getTenantId())
.scopes(tokenCheckRespDTO.getScopes())
.build();
}
}
public class SecurityFrameworkUtils {
public final static String AUTHORIZATION_BEARER = "Bearer";
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";
private SecurityFrameworkUtils() {
}
/**
* 从header中提取token
*/
public static String obtainAuthorization(HttpServletRequest request, String header) {
String authorization = request.getHeader(header);
if (StrUtil.isEmpty(authorization)) {
return null;
}
if (!StrUtil.startWith(authorization, AUTHORIZATION_BEARER + " ")) {
return null;
}
return authorization.substring(AUTHORIZATION_BEARER.length() + 1).trim();
}
/**
* 获取当前用户类型
*/
public static Integer getLoginUserType(HttpServletRequest request) {
if (request == null) {
return null;
}
Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
if (userType != null) {
return userType;
}
return UserTypeEnum.ADMIN.getValue();
}
/**
* 设置用户
*/
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
// 创建 Authentication,并设置到上下文
/**
* SecurityContextHolder.getContext().setAuthentication(authentication) 是 Spring Security 中用于设置当前用户认证信息的方法。
* 它的作用是将给定的 Authentication 对象设置为当前线程的安全上下文中,表示当前用户已经通过身份验证。
* */
Authentication authentication = buildAuthentication(loginUser,request);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* 构建Authentication
* */
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
/**
* 创建 UsernamePasswordAuthenticationToken 对象
* public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
* Collection<? extends GrantedAuthority> authorities);
* principal: 表示身份验证的主体,通常是用户名或用户实体。
* credentials: 表示凭证,通常是密码或其他用于验证身份的凭据。
* authorities: 表示用户的权限集合,通常是一组 GrantedAuthority 对象。
* 举个例子
* // 定义用户信息
* String username = "user";
* String password = "password";
*
* // 定义用户权限
* Collection<? extends GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
*
* // 创建 UsernamePasswordAuthenticationToken 实例
* UsernamePasswordAuthenticationToken authenticationToken =
* new UsernamePasswordAuthenticationToken(username, password, authorities);
* */
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return authentication;
}
}
/**
* @version V1.0
* @author: carsonlius
* @date: 2023/12/20 15:38
* @company
* @description 自定义的 Spring Security 配置适配器实现
*/
@AutoConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DemoWebSecurityConfigurerAdapter {
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Resource
private TokenAuthenticationFilter tokenAuthenticationFilter;
/**
* 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
* 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
*/
@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authentication) throws Exception {
return authentication.getAuthenticationManager();
}
/**
* 配置 URL 的安全配置
* <p>
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 登出
httpSecurity.cors().and() // 开启跨域
.csrf().disable() // csrf禁用,因为不使用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // STATELESS(无状态): 表示应用程序是无状态的,不会创建会话。这意味着每个请求都是独立的,不依赖于之前的请求。适用于 RESTful 风格的应用。
.and().headers().frameOptions().disable()
.and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) // 身份未认证时响应
.accessDeniedHandler(accessDeniedHandler); // 身份已经认证(登录),但是没有权限的情况的响应
// 设置具体请求的权限
httpSecurity.authorizeRequests()
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() // 静态资源无需认证
.antMatchers("/websocket/message").permitAll() // websocket无需认证
.antMatchers("/system/auth/login").permitAll()
.and().authorizeRequests().anyRequest().authenticated(); // 其他请求必须认证
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}