【个人版】SpringBoot下Spring-Security自定义落地篇【三】

背景: 前两篇文章将spring-security的设计架构、核心类、配置及构建过程基本过了一遍,其实很偏理论,如果对源码不感兴趣或项目使用不深,基本可以忽略,毕竟完全理解可能也不会用到,时间长也忘掉了。但是如果你想对代码进行微调,或者写出自己想要的设计效果,那么读一读还是很有必要,毕竟开发过程就是一个学习的过程。本篇是在参考遍地继承WebSecurityConfigurerAdapter的方案上,再加上自身阅读源码后的理解,结合自身需求而尝试出来的。

Spring-Security全局导读:
1、Security核心类设计
2、HttpSecurity结构和执行流程解读
3、Spring-Security个人落地篇

ps1:WebSecurityConfigurerAdapter在较新的版本中都已经被标注过期了,这也是我读源码时尝试自己尝试新方案的动机之一
ps2: 落地方案其实和最新官方推荐的方案很接近,因为我也是从自动配置类中的SecurityFilterChain的创建过程而猜想过来的
ps3:强烈建议阅读松哥之前写的security系列文章,看看这一篇就知道他的水平了
ps4:如果有时间&有想法,自己写一些试试,很久以前粗略看过松哥Security系列的一部分文章,当时觉得自己懂了,不知过了多久,已经完全忘记基础概念了。

废话过多,以下为个人输出(简略篇):
一、POM依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.14</version>
		<relativePath/> 
	</parent>
	<groupId></groupId>
	<artifactId></artifactId>
	<version></version>
	<name></name>
	<description></description>
	<properties>
		<java.version></java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>compile</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

二、自定义拦截及授权封装类

/**
 * 1、CustomAuthenticationProvider等其它内部类不能设置为静态类
 * 2、CustomUsernamePasswordAuthenticationFilter必须重写afterPropertiesSet方法并忽略管理器校验
 */
@Component
@ConditionalOnProperty(value = "password.enable", havingValue = "true", matchIfMissing = true)
public class CustomAuthenticationContext {

    @Component
    private class CustomAuthenticationProvider implements AuthenticationProvider {
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
            if ("test".equals(token.getPrincipal().toString()) && "test".equals(token.getCredentials().toString())) {
                return UsernamePasswordAuthenticationToken.authenticated("test", null, null);
            }
            throw new BadCredentialsException("账号信息错误");
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
        }
    }

    @Component
    private class CustomUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
        private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

        private ObjectMapper objectMapper;

        public CustomUsernamePasswordAuthenticationFilter() {
            super(new AntPathRequestMatcher("/user/login", "POST"));
        }

        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
            InputStream content = request.getInputStream();
            User user = objectMapper.readValue(content, User.class);
            Set<ConstraintViolation<User>> validate = validator.validate(user, Default.class);
            if (validate.isEmpty()) {
                UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(user.getName(), user.getPassword());
                authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
                return this.getAuthenticationManager().authenticate(authRequest);
            }
            throw new BadCredentialsException("用户名或密码错误");
        }

        @Autowired
        void init(CustomAuthResult authResult, ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
            setAuthenticationSuccessHandler(authResult);
            setAuthenticationFailureHandler(authResult);
        }

        @Override
        // 后期统一设置,未重写则实例创建后校验报错
        public void afterPropertiesSet() {
        }
    }
       
    @Data
    private static class User {
    	@NotBlank
        private String name;

        @NotBlank
        private String password;
   }	
	
}

说明:
1、自定义鉴权中,Filter及AuthenticationProvider通常是一对一的,此处封装为一个类,并用条件注解修饰,避免多场景下的鉴权组合造成的代码混乱(虽然此类场景通常不会有太大的变更)
2、此块逻辑其实可以放到controller中,但注意授权上下文的控制及HttpSecurity的设置

三、Security框架配置入口类

@Configuration
public class SecurityFilterChainConfiguration {
    @Autowired
    // HttpSecurity默认是多例的,此处如果多个方法调用,必须唯一
    private HttpSecurity httpSecurity;

    // 参数列表可通过自定义类上条件注解控制
    // 单个类中同时存在多个@Autowired,执行顺序不固定
    // 这两个类是一体的,可以封装在一起,不用搞得太零散
    @Autowired
    void authenticationManager(List<AuthenticationProvider> customProviders, List<AbstractAuthenticationProcessingFilter> customProcessingFilter) {
        // 自封装授权管理器,没有使用系统AuthenticationManagerBuilder创建的管理器
        AuthenticationManager authenticationManager = new ProviderManager(customProviders);
        for (AbstractAuthenticationProcessingFilter filter : customProcessingFilter) {
            filter.setAuthenticationManager(authenticationManager);
            // UsernamePasswordAuthenticationFilter.class为系统内置Filter
            httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
        }
    }

    @Bean
    SecurityFilterChain securityFilterChain(CustomAuthResult authResult) throws Exception {
        /*
         * 此块HttpSecurity链式配置包含三块内容
         * 1、csrf会在response中添加token header,以避免非原始请求客户端伪造访问
         * 2、关闭默认的表单登录,所有鉴权逻辑自定义
         * 3、针对logout、异常等场景细节进行配置
         */
        httpSecurity.csrf().disable()
        		// 如果自定义授权了,表单登录可以关闭,通常用于前后台分离项目
                .formLogin().disable()
                .authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()
                // 这块是为在controller中做授权校验而放开的,此场景后续补充
                .antMatchers(HttpMethod.POST, "/doLogin").permitAll()
                .anyRequest().authenticated()
                .and().logout().logoutSuccessHandler(authResult).permitAll()
                // 设置访问未授权资源的处理器
                .and().exceptionHandling().authenticationEntryPoint(authResult);
        // 手工build,没有借助WebSecurity类的管理
        return httpSecurity.build();
    }
}

四、响应封装合体类

@Component
public class CustomAuthResult implements AuthenticationFailureHandler, AuthenticationSuccessHandler, AuthenticationEntryPoint, LogoutSuccessHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write("{\"code\":\"1004\",\"msg\":\"用户名或密码错误\"}");
        response.getWriter().flush();
        response.getWriter().close();
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("{\"code\":\"1000\",\"msg\":\"登录成功\"}");
        response.getWriter().flush();
        response.getWriter().close();
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().write("{\"code\":\"1001\",\"msg\":\"用户未登录\"}");
        response.getWriter().flush();
        response.getWriter().close();
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("{\"code\":\"1003\",\"msg\":\"成功退出\"}");
        response.getWriter().flush();
        response.getWriter().close();
    }
}

说明:此类自己调试随便建的,生产项目需要有严格而准确的封装和处理

简单自定义使用,以上就足够了,相关注意事项也在注释中说明,不管是继承WebSecurityConfigurerAdapter的方式,还是其他方式,核心类的执行流程其实是不变的,变的只是配置方式或组合方式的外皮,所以把基础概念了解清楚了,不管是spring5还是spring6的版本变化,基本不会有太大的挑战。

PS:
1、关于用户权限数据管理逻辑,框架层提供了UserDetailsService接口及对应内存&数据库的默认实现,这个需求差异较大,如验证码登录,此处不再展开,直接在验证逻辑处写死配置。
2、在新版本中(参考依赖),不管是继承WebSecurityConfigurerAdapter的方式,还是哪种方式,都不需要再自行使用@EnableWebSecurity注解在类上进行标注了,自动配置类中已告知。

附Adapter模式简化配置版:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    // 代码参考上述方案定义
    private CustomAuthResult authResult;

    @Autowired
    // AuthenticationManager配置类,复用全局AuthenticationManager
    private AuthenticationConfiguration configuration;

    @Override
    // 适配器模式只需要重写此方法,完成内部过滤器链配置
    protected void configure(HttpSecurity http) throws Exception {
    	// 自定义安全管理过滤器,对应鉴权和参数封装逻辑此处忽略
        http.addFilterBefore(adaptUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        http.csrf().disable()
                .formLogin().disable()
                .authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated().and().logout().logoutSuccessHandler(authResult).permitAll()
                .and().exceptionHandling().authenticationEntryPoint(authResult);
    }

    @Bean
    // 自定义Filter内部有其他Autowired注解需要处理,所以需要发布到容器
    // 如果手工set,可以考虑不发布到spring容器中,限制其作用域范围
    AdaptUsernamePasswordAuthenticationFilter adaptUsernamePasswordAuthenticationFilter() throws Exception {
        AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
        AdaptUsernamePasswordAuthenticationFilter adaptUsernamePasswordAuthenticationFilter = new AdaptUsernamePasswordAuthenticationFilter("/user/login");
        adaptUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager);
        return adaptUsernamePasswordAuthenticationFilter;
    }

    @Bean
    // 自定义授权管理器,代码和上述方案定义类一致,但上述是内部类,包路径不一致
    public AuthenticationProvider authenticationProvider() {
        return new CustomAuthenticationProvider();
    }
}

可以看到继承WebSecurity适配器的代码同样也很简单,核心还是HttpSecurity的构建,不过第一种方案灵活性更高,我们自己组织和封装的可能性更好,体现模块化的思想,适配方式已经标注过期,最新版本已经删除,可作参考。

你可能感兴趣的:(spring-security,spring,boot,spring-security)