文章目录
- 场景
- 实现
- 认证过滤器具体实现
- 认证过滤器放到Spring Security UsernamePasswordAuthenticationFilter过滤器之前
场景
spring-boot-starter-security不够灵活, 项目需要二次开发。下面展示一个替换security认证功能的替代方案。
实现
- 项目实现生成accessToken以及checkAccessToken的功能
- 创建一个token过滤器,该过滤器有3部分组成:
2.1 校验token是否有效
2.2 根据token生成用户实体 生成Authentication
2.3 SecurityContextHolder.getContext().setAuthentication(authentication); 将给定的 Authentication 对象设置为当前线程的安全上下文中,表示当前用户已经通过身份验证 - 将token认证过滤器加入到UsernamePasswordAuthenticationFilter过滤器之前
认证过滤器具体实现
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {private final SecurityProperties securityProperties;private final GlobalExceptionHandler globalExceptionHandler;private final OAuth2TokenApi oAuth2TokenApi;@Overrideprotected 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) {// 校验tokenOAuth2AccessTokenCheckRespDTO 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;}
}
认证过滤器放到Spring Security UsernamePasswordAuthenticationFilter过滤器之前
/*** @version V1.0* @author: carsonlius* @date: 2023/12/20 15:38* @company* @description 自定义的 Spring Security 配置适配器实现*/
@AutoConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DemoWebSecurityConfigurerAdapter {@Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint;@Autowiredprivate AccessDeniedHandler accessDeniedHandler;@Resourceprivate TokenAuthenticationFilter tokenAuthenticationFilter;/*** 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入* 通过覆写父类的该方法,添加 @Bean 注解,解决该问题*/@Beanpublic 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 | 用户登录后可访问*/@Beanprotected 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();}}