微信小程序前期开发准备,可以参考这篇文章微信小程序前期准备
1、学习过Spring Secrity oauth2.0的都知道,他有四种登录模式可以选择
authorization code(授权码模式)
implicit(简化模式)
resource owner password credentials(密码模式)
client credentials(客户端模式)
前三种模式都需要用户的密码才能认证成功,客户端模式虽然不需要密码,但是也不会跟用户绑定。所以也是不符合的。我们去微信拿到用户的认证之后,需要自己的系统认证通过,然后返回token给前端。如果系统采用oauth2.0来做认证,这时候我们是没办法拿到用户的明文密码的。并且一般密码都是用BCryptPasswordEncoder加密处理,是不可逆的。这个时候,我们虽然通过了微信的认证,但是如何通过自身系统的认证就是个问题了。那么这时候就需要自定义oauth2.0的授权模式了,通过微信返回的用户唯一标识来完成认证。
这里的方法,适用用于任何的第三方登录:
首先,我们需要一个拦截器:
登录接口如下
9999端口是gateway网关端口。auth是网关路由 http://192.168.2.171:9999/auth/custom/token/social?grant_type=custom&custom=WXCUSTOM@{这后面是微信获取的code} 头信息:Basic d3hNaW5pQXBwQ3VzdG9tOnd4TWluaUFwcEN1c3RvbQ==(这后面是BASE64加密信息)
注意这里是 filter 来接管这个请求,而不是到 Controller)
import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author Mylucy * @Date 2022-01-18 10:15 * 客户微信小程序登录 */ public class SocialAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static final String SPRING_SECURITY_FORM_CUSTOM_KEY = "custom"; @Getter @Setter private String customWxLogin = SPRING_SECURITY_FORM_CUSTOM_KEY; @Getter @Setter private AuthenticationEventPublisher eventPublisher; @Getter @Setter private boolean postOnly = true; // @Getter // @Setter // private AuthenticationEventPublisher eventPublisher; @Getter @Setter private AuthenticationEntryPoint authenticationEntryPoint; public SocialAuthenticationFilter() { super(new AntPathRequestMatcher(SecurityConstants.CUSTOM_TOKE_URL, "POST")); } /** * Performs actual authentication. * <p> * The implementation should do one of the following: * <ol> * <li>Return a populated authentication token for the authenticated user, indicating * successful authentication</li> * <li>Return null, indicating that the authentication process is still in progress. * Before returning, the implementation should perform any additional work required to * complete the process.</li> * <li>Throw an <tt>AuthenticationException</tt> if the authentication process * fails</li> * </ol> * * @param request from which to extract parameters and perform the authentication * @param response the response, which may be needed if the implementation has to do a * redirect as part of a multi-stage authentication process (such as OpenID). * @return the authenticated user token, or null if authentication is incomplete. * @throws AuthenticationException if authentication fails. */ @Override @SneakyThrows public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SocialAuthenticationToken socialAuthenticationToken = new SocialAuthenticationToken(mobile); setDetails(request, socialAuthenticationToken); Authentication authResult = null; try { authResult = this.getAuthenticationManager().authenticate(socialAuthenticationToken); logger.debug("Authentication success: " + authResult); SecurityContextHolder.getContext().setAuthentication(authResult); } catch (Exception failed) { SecurityContextHolder.clearContext(); logger.debug("Authentication request failed: " + failed); eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed), new PreAuthenticatedAuthenticationToken("access-token", "N/A")); try { authenticationEntryPoint.commence(request, response, new UsernameNotFoundException(failed.getMessage(), failed)); } catch (Exception e) { logger.error("authenticationEntryPoint handle error:{}", failed); } } return authResult; } private String obtainMobile(HttpServletRequest request) { return request.getParameter(customWxLogin); } private void setDetails(HttpServletRequest request, SocialAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } }
注:里面有一些常量,我这里也给出方法代码
public interface SecurityConstants { /** * 启动时是否检查Inner注解安全性 */ boolean INNER_CHECK = true; /** * 刷新 */ String REFRESH_TOKEN = "refresh_token"; /** * 验证码有效期 */ int CODE_TIME = 60; /** * 验证码长度 */ String CODE_SIZE = "4"; /** * 角色前缀 */ String ROLE = "ROLE_"; /** * 前缀 */ String PIGX_PREFIX = "pigx_"; /** * token 相关前缀 */ String TOKEN_PREFIX = "token:"; /** * oauth 相关前缀 */ String OAUTH_PREFIX = "oauth:"; /** * 授权码模式code key 前缀 */ String OAUTH_CODE_PREFIX = "oauth:code:"; /** * 项目的license */ String PIGX_LICENSE = "made by HuaBing"; /** * 内部 */ String FROM_IN = "Y"; /** * 标志 */ String FROM = "from"; /** * OAUTH URL */ String OAUTH_TOKEN_URL = "/oauth/token"; /** * 手机号登录URL */ String SMS_TOKEN_URL = "/mobile/token/sms"; /** * 社交登录URL */ String SOCIAL_TOKEN_URL = "/mobile/token/social"; /** * 自定义登录URL */ String MOBILE_TOKEN_URL = "/mobile/token/*"; /** * 客户社交登录 */ String CUSTOM_TOKEN = "/custom/token/social"; /** * 客户微信登录 */ String CUSTOM_TOKE_URL = "/custom/token/*"; /** * 微信获取OPENID */ String WX_AUTHORIZATION_CODE_URL = "https://api.weixin.qq.com/sns/oauth2/access_token" + "?appid=%s&secret=%s&code=%s&grant_type=authorization_code"; /** * 微信小程序OPENID */ String MINI_APP_AUTHORIZATION_CODE_URL = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"; /** * 客户微信小程序登录 */ String CUSTOM_WX_MINI_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session"; /** * 客户微信小程序登录 */ String CUSTOM_WX_MINI_LOGIN_TYPE = "authorization_code"; String CUSTOM_APPID = "wx94e1329ed5419b55"; String CUSTOM_SECRET="6d09e94f564d263340da3a3ede4ca6ab"; /** * 码云获取token */ String GITEE_AUTHORIZATION_CODE_URL = "https://gitee.com/oauth/token?grant_type=" + "authorization_code&code=%S&client_id=%s&redirect_uri=" + "%s&client_secret=%s"; /** * 开源中国获取token */ String OSC_AUTHORIZATION_CODE_URL = "https://www.oschina.net/action/openapi/token"; /** * 码云获取用户信息 */ String GITEE_USER_INFO_URL = "https://gitee.com/api/v5/user?access_token=%s"; /** * 开源中国用户信息 */ String OSC_USER_INFO_URL = "https://www.oschina.net/action/openapi/user?access_token=%s&dataType=json"; /** * {bcrypt} 加密的特征码 */ String BCRYPT = "{bcrypt}"; /** * sys_oauth_client_details 表的字段,不包括client_id、client_secret */ String CLIENT_FIELDS = "client_id, CONCAT('{noop}',client_secret) as client_secret, resource_ids, scope, " + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " + "refresh_token_validity, additional_information, autoapprove"; /** * JdbcClientDetailsService 查询语句 */ String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS + " from sys_oauth_client_details"; /** * 按条件client_id 查询 */ String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ? and del_flag = 0 and tenant_id = %s"; /** * 资源服务器默认bean名称 */ String RESOURCE_SERVER_CONFIGURER = "resourceServerConfigurerAdapter"; /** * 客户端模式 */ String CLIENT_CREDENTIALS = "client_credentials"; /** * 客户端编号 */ String CLIENT_ID = "client_id"; /** * 用户ID字段 */ String DETAILS_USER_ID = "id"; /** * 用户名 */ String DETAILS_USERNAME = "username"; /** * 用户基本信息 */ String DETAILS_USER = "user_info"; /** * 用户名phone */ String DETAILS_PHONE = "phone"; /** * 头像 */ String DETAILS_AVATAR = "avatar"; /** * 用户部门字段 */ String DETAILS_DEPT_ID = "deptId"; /** * 租户ID 字段 */ String DETAILS_TENANT_ID = "tenantId"; /** * 协议字段 */ String DETAILS_LICENSE = "license"; /** * 激活字段 兼容外围系统接入 */ String ACTIVE = "active"; /** * AES 加密 */ String AES = "aes"; }
(注:我这里分后台用户登录,前台客户登录,不同用户查询两张不同的表)
package com.pig4cloud.pigx.common.security.custom; /** * @author Mylucy * @Date 2022-01-18 10:24 */ import com.pig4cloud.pigx.common.security.component.PigxPreAuthenticationChecks; import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService; import com.pig4cloud.pigx.common.security.util.PigxSecurityMessageSourceUtil; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsChecker; /** * 社交登录 */ @Slf4j public class SocialAuthenticationProvider implements AuthenticationProvider { private MessageSourceAccessor messages = PigxSecurityMessageSourceUtil.getAccessor(); private UserDetailsChecker detailsChecker = new PigxPreAuthenticationChecks(); @Getter @Setter private PigxUserDetailsService userDetailsService; @Override @SneakyThrows public Authentication authenticate(Authentication authentication) { SocialAuthenticationToken socialAuthenticationToken = (SocialAuthenticationToken) authentication; String principal = socialAuthenticationToken.getPrincipal().toString(); //这里就是获取客户信息的方法 UserDetails userDetails = userDetailsService.customLoadBySocial(principal); if (userDetails == null) { log.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages .getMessage("AbstractUserDetailsAuthenticationProvider.noopBindAccount", "Noop Bind Account")); } // 检查账号状态 detailsChecker.check(userDetails); SocialAuthenticationToken authenticationToken = new SocialAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationToken.setDetails(socialAuthenticationToken.getDetails()); return authenticationToken; } @Override public boolean supports(Class<?> authentication) { return SocialAuthenticationToken.class.isAssignableFrom(authentication); } }
获取用户信息的方法
/** 这里可以扩展你自己的用户查询方法 * @author lengleng * @date 2018/8/15 */ public interface PigxUserDetailsService extends UserDetailsService { /** * 客户社交登录 * * @param code 前端向微信获取的唯一标识 * @return * @throws UsernameNotFoundException */ UserDetails customLoadBySocial(String code) throws UsernameNotFoundException; } ==============================================分割================================================ /** * 用户详细信息 * * @author lengleng */ @Slf4j @Primary @RequiredArgsConstructor public class PigxUserDetailsServiceImpl implements PigxUserDetailsService { private final RemoteUserService remoteUserService; private final RemoteCustomService remoteCustomService; private final CacheManager cacheManager; private final TokenStore tokenStore; /** * 客户社交登录 * * @param code * @return * @throws UsernameNotFoundException */ @Override @SneakyThrows public UserDetails customLoadBySocial(String code) { //这里时远程调用查询客户表信息 FoodMacCustom customSocialInfo = remoteCustomService.getCustomSocialInfo(code, SecurityConstants.FROM_IN).getData(); UserDetails customDetails = getCustomDetails(customSocialInfo); return customDetails; } /** * 构建客户信息 customDetails * * @param foodMacCustom */ private UserDetails getCustomDetails(FoodMacCustom foodMacCustom) { if (foodMacCustom == null) { throw new UsernameNotFoundException("客户未注册"); } boolean is_lock = false; if (CommonConstants.CUSTOM_LOCK.equals(foodMacCustom.getStatus())) { is_lock = true; } if (is_lock == false) { throw new PigxAuth2Exception("账号已被锁定"); } log.debug("用户信息::{}", foodMacCustom.getUsername()); PigxCustomUser pigxCustomUser = new PigxCustomUser(foodMacCustom.getId(), 1, foodMacCustom.getMobile(), foodMacCustom.getAvatar(), foodMacCustom.getTenantId(), foodMacCustom.getUsername(), "{noop}" + foodMacCustom.getPassword(), true, true, true, is_lock, AuthorityUtils.NO_AUTHORITIES); return pigxCustomUser; } } ===================================================分割============================================================== /** * 系统社交登录账号表 * * @author Mylucy * @date 2021-08-16 21:30:41 */ public interface SysSocialDetailsService extends IService<SysSocialDetails> { /** * 根据入参查询客户信息 * * @param code * @return */ FoodMacCustom getCustomSocialInfo(String code); } ===================================================分割============================================================== /** * @author Mylucy * @date 2021年08月16日 */ @Slf4j @AllArgsConstructor @Service("sysSocialDetailsService") public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMapper, SysSocialDetails> implements SysSocialDetailsService { //注意这个方法。这个做了很多惭怍 private final Map<String, CustomLoginHandle> customLoginHandlerMap; private final CacheManager cacheManager; private final SysUserMapper sysUserMapper; /** * 根据入参查询客户信息 * * @param code * @return */ @Override public FoodMacCustom getCustomSocialInfo(String code) { String[] inStrs = code.split(StringPool.AT); String type = inStrs[0]; String loginStr = inStrs[1]; FoodMacCustom foodMacCustom = customLoginHandlerMap.get(type).handle(loginStr); return foodMacCustom; } } ===================================================分割============================================================== /** * @author Mylucy * @Date 2022-01-18 11:02 */ public interface CustomLoginHandle { /*** * 数据合法性校验 * @param loginStr 通过用户传入获取微信唯一标识 * @return */ Boolean check(String loginStr); /** * 通过用户传入获取微信唯一标识 * @param loginStr * @return */ String identify(String loginStr); /** * 通过微信openId 获取用户信息 * @param identify * @return */ FoodMacCustom customInfo(String identify); /** * 处理方法 * @param loginStr 登录参数 * @return */ FoodMacCustom handle(String loginStr); } ===================================================分割============================================================== /** * @author Mylucy * @Date 2022-01-18 11:04 */ public abstract class AbstractCustomLoginHandler implements CustomLoginHandle { /*** * 数据合法性校验 * @param loginStr 通过用户传入获取唯一标识 * @return 默认不校验 */ @Override public Boolean check(String loginStr) { return true; } /** * 处理方法 * * @param loginStr 登录参数 * @return */ @Override public FoodMacCustom handle(String loginStr) { if (!check(loginStr)) { return null; } String identify = identify(loginStr); FoodMacCustom foodMacCustom = customInfo(identify); return foodMacCustom; } } ===================================================分割============================================================== /** * @author Mylucy * @Date 2022-01-18 11:03 */ @Slf4j @Component("WXCUSTOM") @AllArgsConstructor public class CustomWeChatLoginHandler extends AbstractCustomLoginHandler { private final IFoodMacCustomService foodMacCustomService; private final SysSocialDetailsMapper sysSocialDetailsMapper; private final StringRedisTemplate stringRedisTemplate; private static final String REDIS_KEY = "WX:CUSTOM:LOGIN:"; private final Sequence sequence; /** * 两个小时过期 */ private static final long EXPIRE_TIME = 7200000; /** * 微信登录传入code * <p> * 通过code 调用qq 获取唯一标识 * * @param code * @return */ @Override public String identify(String code) { String sessionKeyOpenId = (String) stringRedisTemplate.opsForValue().get(REDIS_KEY + code); if (StrUtil.isEmpty(sessionKeyOpenId)) { SysSocialDetails condition = new SysSocialDetails(); condition.setType(LoginTypeEnum.WECHAT.getType()); SysSocialDetails socialDetails = sysSocialDetailsMapper.selectOne(new QueryWrapper<>(condition)); /**String url = String.format(SecurityConstants.MINI_APP_AUTHORIZATION_CODE_URL, socialDetails.getAppId(),socialDetails.getAppSecret(), code); String result = HttpUtil.get(sendUrl);**/ log.debug("客户--->{}", code); String result = sendGet(SecurityConstants.CUSTOM_WX_MINI_LOGIN_URL, "appid=" + socialDetails.getAppId() + "&" + "secret=" + socialDetails.getAppSecret() + "&" + "js_code=" + code + "&" + "grant_type=" + SecurityConstants.CUSTOM_WX_MINI_LOGIN_TYPE); log.info("微信响应报文:{}", result); JSONObject jsonObject = JSON.parseObject(result); String openId = jsonObject.getString("openid"); String sessionKey = jsonObject.getString("session_key"); log.info("微信openId::{}", openId); log.info("微信sessionKey::{}", sessionKey); if (openId != null && !"".equals(openId)) { //将信息保存在redis中 log.info("+++++++++++++微信key+++++++++++{}", sessionKey); String redisKey = REDIS_KEY + code; StringBuilder stringBuilder = new StringBuilder(); String openIdSessionKey = stringBuilder.append(sessionKey).append("@").append(openId).toString(); // String openIdSessionKey= sessionKey + "#" + openId; stringRedisTemplate.opsForValue().set(redisKey,openIdSessionKey,EXPIRE_TIME, TimeUnit.SECONDS); } return openId; } else if (StrUtil.isNotEmpty(sessionKeyOpenId)) { String[] split = sessionKeyOpenId.split("@"); String session = split[0]; String openId = split[1]; return openId; } return null; } /** * openId 获取用户信息 * * @param openId * @return */ @Override public FoodMacCustom customInfo(String openId) { FoodMacCustom custom = foodMacCustomService.getOne(Wrappers.<FoodMacCustom>query().lambda() .eq(FoodMacCustom::getWechatopenid, openId)); if (custom == null) { log.info("微信未绑定:{}", openId); return null; } return custom; } /** * 向指定URL发送GET方法的请求 * * @param url 发送请求的URL * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 * @return URL 所代表远程资源的响应结果 */ public static String sendGet(String url, String param) { String result = ""; BufferedReader in = null; try { String urlNameString = url + "?" + param; URL realUrl = new URL(urlNameString); // 打开和URL之间的连接 URLConnection connection = realUrl.openConnection(); // 设置通用的请求属性 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 建立实际的连接 connection.connect(); // 获取所有响应头字段 Map<String, List<String>> map = connection.getHeaderFields(); // 遍历所有的响应头字段 for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); } // 定义 BufferedReader输入流来读取URL的响应 in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("发送GET请求出现异常!" + e); e.printStackTrace(); } // 使用finally块来关闭输入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } return result; } }
远程调用的controller和fegin调用方法截图
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.SneakyThrows; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import java.util.Collection; /** * 第三方登录令牌 * * @author Mylucy * @Date 2022-01-18 10:23 */ public class SocialAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; public SocialAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } @JsonCreator public SocialAuthenticationToken(@JsonProperty("principal") Object principal, @JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } @Override public Object getPrincipal() { return this.principal; } @Override public Object getCredentials() { return null; } @Override @SneakyThrows public void setAuthenticated(boolean isAuthenticated) { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } }
```java package com.pig4cloud.pigx.common.security.custom; /** * @author Mylucy * @Date 2022-01-18 10:27 */ import com.fasterxml.jackson.databind.ObjectMapper; import com.pig4cloud.pigx.common.security.component.PigxCommenceAuthExceptionEntryPoint; import com.pig4cloud.pigx.common.security.handler.CustomLoginSuccessHandler; import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService; import lombok.Getter; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * * 第三方登录配置入口 * @author Mylucy */ @Getter @Setter public class SocialSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private ObjectMapper objectMapper; @Autowired private AuthenticationEventPublisher defaultAuthenticationEventPublisher; /** * @Qualifier("socialLoginSuccessHandler") */ @Autowired private CustomLoginSuccessHandler socialLoginSuccessHandler; @Autowired private PigxUserDetailsService userDetailsService; @Override public void configure(HttpSecurity http) { SocialAuthenticationFilter socialAuthenticationFilter = new SocialAuthenticationFilter(); socialAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); socialAuthenticationFilter.setAuthenticationSuccessHandler(socialLoginSuccessHandler); socialAuthenticationFilter.setEventPublisher(defaultAuthenticationEventPublisher); socialAuthenticationFilter.setAuthenticationEntryPoint(new PigxCommenceAuthExceptionEntryPoint(objectMapper)); SocialAuthenticationProvider socialAuthenticationProvider = new SocialAuthenticationProvider(); socialAuthenticationProvider.setUserDetailsService(userDetailsService); http.authenticationProvider(socialAuthenticationProvider).addFilterAfter(socialAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }