SpringSecurity框架流程定制化的进一步优化方案,精简代码

之前讲解过一篇SpringSecurity定制化流程的方案,很多人说看起来比较复杂,这次对其中流程优化部分,顺便记录下最新的理解心得。
由于是优化,肯定是基于之前的代码,因此有些地方不清楚的可以参考上篇文章:SpringSecurity框架——认证流程介绍,实战代码

与全定制化的区别:

  1. 上文提到自定义UsernamePasswordAuthenticationFilter过滤器,重写attemptAuthentication()方法,通常这里会在这里对验证码,请求方法类型等进行合法性校验。这次以内联的形式:将依赖中的类,以相同的包路径在项目中创建一份,可以理解为替换Security框架中的UsernamePasswordAuthenticationFilter过滤器,实现方式:将jar中的filter复制一份到项目中,按照业务需求重新attemptAuthentication()方法。
  • 这里对上文自定义的AbstractAuthenticationToken和AbstractUserDetailsAuthenticationProvider均抛弃掉,AbstractUserDetailsAuthenticationProvider使用内联的方式进行替换,AbstractAuthenticationToken使用默认的,如果是无特殊需求,默认的两个字段可以满足密码、手机号登录等模式,若为第三方授权登录需要三个以上登录信息字段,则可以去单独定制化token

  • 最后通过token的类型做认证方法的转发的入口,this.getAuthenticationManager().authenticate(authRequest)。这里只做了一种默认的token类型,如果需要定制化则在这里返回定制化的token类型,因为最终在AuthenticationProvider中会通过suppors方法进行匹配,通过类型匹配就会进入相对应的认证方法流程。具体逻辑可以参考上文定制化FIlter中Authentication对象流程详解。

 @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        UsernamePasswordAuthenticationToken authRequest;
        String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE);
        if (MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType)) {
        	Map<String, String> authenticationBean = JsonUtil.defaultObjectMapper().readValue(request.getInputStream(), new TypeReference<Map<String, String>>() {
        	});
        	String username = authenticationBean.getOrDefault(this.usernameParameter, "").trim();
        	String password = authenticationBean.getOrDefault(this.passwordParameter, "").trim();
        	authRequest = new UsernamePasswordAuthenticationToken(username, password);
        } else {
         	String username = obtainUsername(request);
        	username = (username != null) ? username : "";
        	username = username.trim();
        	String password = obtainPassword(request);
        	password = (password != null) ? password : "";
            authRequest = new UsernamePasswordAuthenticationToken(username, password);
        }
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);

    }

  1. AbstractUserDetailsAuthenticationProvider使用内联的方式,不改动上文的代码方法体逻辑,可以理解为是之前自定义的Provider重命名了。
 @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(CustomAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only CustomAuthenticationToken is supported"))
        UserDetails user = this.userCache.getUserFromCache(authJson);
 
        if (user == null) {
            try {
            	// 调用retrieveUser 就是做了一层转发 这里会抓取抛出的异常,对异常进行重新抛出,目的是对应失败处理器的异常类型
                user = retrieveUser(authJson, (CustomAuthenticationToken) authentication);
            } catch (UsernameNotFoundException notFound) {
                logger.debug("Identity '" + loginName + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                } else {
                    throw notFound;
                }
            }

            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (CustomAuthenticationToken) authentication);
        } catch (Exception exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                  // 查询到用户
                user = retrieveUser(authJson, (CustomAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                // 查询到用户后 检测存储的密码和提交的密码是否相同,否则抛出异常
                additionalAuthenticationChecks(user, (CustomAuthenticationToken) authentication);
             } else {
                throw exception;
            }
        }
        // 这里是返回了一个认证过的自定义token
        // 自定义的token会有一个属性: Boolean  Authenticated; 走到这里会创建一个token对象并将这个属性设为true
        return CustomAuthenticationToken(platform, organId, principalToReturn, authentication, user);
    }

	// 这里需要注意,在用户注册时的密码加密必须要使用passwordEncoder.encode()方法
	@Override
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  CustomAdminUsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }




 @Override
    protected final UserDetails retrieveUser(String authJson,
                                             CustomHealthUsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(authJson);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }


  1. token不做展示了,使用的框架中默认类型
  2. UserDetailsService的实现这次继承UserDetailsManager,原因是UserDetailsService是顶级父类,它只定义了一个抽象方法loadUserByUsername(String username),而UserDetailsManager所定义的抽象方法更加完善,包含了创建新用户 和修改密码等,易于扩展比如在登录后修改密码。代码方法体方面与上文相较是不变的,不再过多展示。
// 源码
public interface UserDetailsService {

	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

// 源码
public interface UserDetailsManager extends UserDetailsService {

	/**
	 * Create a new user with the supplied details.
	 */
	void createUser(UserDetails user);

	/**
	 * Update the specified user.
	 */
	void updateUser(UserDetails user);

	/**
	 * Remove the user with the given login name from the system.
	 */
	void deleteUser(String username);

	/**
	 * Modify the current user's password. This should change the user's password in the
	 * persistent user repository (datbase, LDAP etc).
	 * @param oldPassword current password (for re-authentication if required)
	 * @param newPassword the password to change to
	 */
	void changePassword(String oldPassword, String newPassword);

	/**
	 * Check if a user with the supplied login name exists in the system.
	 */
	boolean userExists(String username);

}

重点讲一下manager的实现类,业务逻辑无关紧要就删除了,重点是它的应用和注解,在上文提到是由WebSecurityConfigurerAdapter中方法注册定制化的provider,在provider的属性中注入定制化的UserDetailsService,而这里使用内联形式是如何被发现并注册进provider中的。
一共有两点

  • 首先是注解,@Service、@Primary两个注解分别作用是交给spring去管理、作为同类型首先使用需要UserDetailsService的类型时,默认注入带有@Primary的类。
  • 第二点是在项目启动初始化时,InitializeUserDetailsBeanManagerConfigurer对manager的管理加载机制是通过类型获取,因此与@Primary注解相互作用下,项目会把定制化的SecurityUserDetailsManager优先注入provider中去。
@Primary
@Service
public class SecurityUserDetailsManager implements UserDetailsManager {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        return null;
    }

    @Override
    public void createUser(UserDetails user) {

    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }


}

// 源码
@Order(InitializeUserDetailsBeanManagerConfigurer.DEFAULT_ORDER)
class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {

	static final int DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE - 5000;

	private final ApplicationContext context;

	/**
	 * @param context
	 */
	InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
		this.context = context;
	}

	@Override
	public void init(AuthenticationManagerBuilder auth) throws Exception {
		auth.apply(new InitializeUserDetailsManagerConfigurer());
	}

	class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {

		@Override
		public void configure(AuthenticationManagerBuilder auth) throws Exception {
			if (auth.isConfigured()) {
				return;
			}
			UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
			if (userDetailsService == null) {
				return;
			}
			PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
			UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
			DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
			provider.setUserDetailsService(userDetailsService);
			if (passwordEncoder != null) {
				provider.setPasswordEncoder(passwordEncoder);
			}
			if (passwordManager != null) {
				provider.setUserDetailsPasswordService(passwordManager);
			}
			provider.afterPropertiesSet();
			auth.authenticationProvider(provider);
		}

		/**
		 * @return a bean of the requested class if there's just a single registered
		 * component, null otherwise.
		 */
		private <T> T getBeanOrNull(Class<T> type) {
			String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type);
			if (beanNames.length != 1) {
				return null;
			}
			return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
		}

	}

}

  1. 最重要的一点来了,对流程的配置。上文提到继承WebSecurityConfigurerAdapter然后重写configure()方法,目的是为了配置登录的一些定制化配置,并且加入了定制化的provider等认证流程。这次也换了一种形式使用,并且部分定制、部分内联的方式,是如何启用的呢?下面结合代码分析:
    主要起作用的是注解:@EnableWebSecurity两个作用,1: 加载了WebSecurityConfiguration配置类,注入了一个非常重要的bean, bean的name为: springSecurityFilterChain,配置安全认证策略。2: 加载了AuthenticationConfiguration, 配置了认证信息,启用了自定义的manager。
    内联不用配置即可生效,因为内联本就是覆盖依赖中的同包路径下同名的文件,定制的一些类则是在内联中的方法调用时的规则上动手脚。

/**
将此注释添加到 @Configuration 类以在任何 WebSecurityConfigurer 中定义 Spring Security 配置,或者更可能通过扩展 WebSecurityConfigurerAdapter 基类并覆盖各个方法:
   @Configuration
   @EnableWebSecurity
   公共类 MyWebSecurityConfiguration 扩展 WebSecurityConfigurerAdapter {
  
   @Override
   public void configure(WebSecurity web) 抛出异常{
   网络忽略()
   // Spring Security 应该完全忽略以 /resources/ 开头的 URL
   .antMatchers("/资源/**");
   }
  
   @Override
   受保护的无效配置(HttpSecurity http)抛出异常{
   http.authorizeRequests().antMatchers("/public/**").permitAll().anyRequest()
   .hasRole("用户").and()
   // 可能更多的配置...
   .formLogin() // 启用基于表单的登录
   // 为所有与表单登录关联的 URL 设置 permitAll
   .permitAll();
   }
  
   @Override
   受保护的无效配置(AuthenticationManagerBuilder auth)抛出异常{
   授权
   // 使用名为“user”和“admin”的用户启用基于内存的身份验证
   .inMemoryAuthentication().withUser("用户").password("密码").roles("USER")
   .and().withUser("admin").password("密码").roles("USER", "ADMIN");
   }
  
   // 可能有更多重写的方法...
   }

 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
		HttpSecurityConfiguration.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

	/**
	 * Controls debugging support for Spring Security. Default is false.
	 * @return if true, enables debug support with Spring Security
	 */
	boolean debug() default false;

}



@RequiredArgsConstructor
@EnableWebSecurity
public class AuthenticationServerConfig {

    /**
     * {@link SecurityBeanConfig#corsConfigurationSource()}
     */
     // 注入跨域配置对象
    private final CorsConfigurationSource corsConfigurationSource;
     // 注入权限校验配置对象
    private final CustomSecurityExpressionRoot customSecurityExpressionRoot;
     // 注入验证码定制化配置对象
    private final ValidateCodeRepository validateCodeRepository;

    @Bean
    // 定义接口白名单
    WebSecurityCustomizer ignoringCustomizer() {
        String[] swaggerExportPaths = {
                "/doc.html", "/index.html", "/swagger-ui.html",
                "/api-docs-ext", "/swagger-resources", "/api-docs", "/v2/api-docs-ext", "/v2/api-docs",
                "/swagger-resources/configuration/ui", "/swagger-resources/configuration/security",
                "/manifest.json", "/robots.txt", "/favicon.ico",
                "/webjars/css/chunk-*.css", "/webjars/css/app.*.css",
                "/webjars/js/app.*.js", "/webjars/js/chunk-*.js", "/precache-manifest.*.js", "/service-worker.js",
        };
        String[] actuatorPaths = {
                "/actuator/health",
                "/actuator/prometheus"
        };
        return (web) -> web.ignoring().antMatchers(swaggerExportPaths).antMatchers(actuatorPaths);
    }

    @Bean
    // 这里对应上文重写configure()的方法体,已bean的形式定制化后交给spring
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .csrf()
                .disable()
            .cors()
                .configurationSource(corsConfigurationSource);
        http
            .exceptionHandling()
                .accessDeniedHandler(new DefaultAccessDeniedHandler())
                .authenticationEntryPoint(new DefaultAuthenticationEntryPoint());
        http
            .logout()
                .clearAuthentication(true)
                .logoutUrl(SecurityConstants.URI.URL_AUTH_LOGOUT)
                .logoutSuccessHandler(new DefaultLogoutSuccessHandler());

        http
            .formLogin()
                // .loginPage("http://127.0.0.1:3001/#/login")
                .loginProcessingUrl(SecurityConstants.URI.URL_LOGIN_PROCESSING)
                .successHandler(new CustomAuthenticationSuccessHandler())
                .failureHandler(new DefaultAuthenticationFailureHandler());
        // http.authenticationProvider(new DaoAuthenticationProvider());

        // 验证码配置
        http.apply(new ValidateCodeConfigurer<>())
                .failureHandler(new DefaultAuthenticationFailureHandler())
                .validateCodeGenerator(SecurityConstants.URI.URL_IMAGE_CAPTCHA, new ImageCodeGenerator(validateCodeRepository).setExpireIn(60 * 3))
                // .validateCodeGenerator("/auth/captcha/mobile", new SmsCodeGenerator(validateCodeRepository).setNeedAuthenticated(true))
                .validateCodeProcessor(SecurityConstants.URI.URL_LOGIN_PROCESSING, new DefaultValidateCodeProcessor(validateCodeRepository))
                .and()
        ;

        http.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
            authorizationManagerRequestMatcherRegistry
                    .antMatchers(SecurityConstants.URI.URL_IMAGE_CAPTCHA,"/event/evaluation/**","/openapi/**","/bw/**")
                    .permitAll()
                    .anyRequest()
                    .access(customSecurityExpressionRoot);
        });
        // sessionManagement
        http.sessionManagement(httpSecuritySessionManagementConfigurer -> {
            httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
            httpSecuritySessionManagementConfigurer.sessionConcurrency(concurrencyControlConfigurer -> {
                // 触发创建 ConcurrentSessionFilter
                concurrencyControlConfigurer.maximumSessions(3);
                concurrencyControlConfigurer.expiredSessionStrategy(event -> {
                    HttpServletResponse response = event.getResponse();
//                    SessionInformation sessionInformation = event.getSessionInformation();
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    // 构建项目统一接口响应对象
                    Rest<BaseResponse> rest = Rest.error(AuthenticationErrorCodeEnum.LOGIN_ON_ANOTHER_DEVICE, "本次登录失效");
                    HttpContextUtil.write(response, rest);
                });
                httpSecuritySessionManagementConfigurer.invalidSessionStrategy((request, response) -> {
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    // 构建项目统一接口响应对象
                    HttpContextUtil.write(response, Rest.error(AuthenticationErrorCodeEnum.CREDENTIALS_EXPIRED));
                });
            });
        });

        // @formatter:on
        return http.build();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值