登录实践
csrf未关闭导致的错误
现象:url被重定向到/error
通过一次debug找出错误:
https://www.bilibili.com/video/BV1pg411X7ZP/?p=8&spm_id_from=pageDriver 时间37:02
自定义短信验证码登录
SmsCodeAuthenticationFilter
设置短信验证码登录的过滤器SmsCodeAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//传一个sms登录的页面
protected SmsCodeAuthenticationFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String mobilePhone = request.getParameter("mobilePhone");
String code = request.getParameter("code");
//创建token
SmsCodeAuthenticationToken smsCodeAuthenticationToken = new SmsCodeAuthenticationToken(mobilePhone, code);
smsCodeAuthenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
//进行认证并return结果
return this.getAuthenticationManager().authenticate(smsCodeAuthenticationToken);
}
}
SmsCodeAuthenticationToken
仿照UsernamePasswordAuthenticationToken设置我们自己的SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
}
public SmsCodeAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
SmsCodeAuthenticationProvider
实现自己的SmsCodeAuthenticationProvider
因为在默认的AbstractUserDetailsAuthenticationProvider的authenticate方法中,
第一行就Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class...),
说明改provider只对默认的UsernamePasswordAuthenticationToken有效,
因此我们要实现自己的provider
@Slf4j
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobilePhone = (String) authentication.getPrincipal();
String code = (String) authentication.getCredentials();
log.info("手机号【{}】验证码【{}】开始登录",mobilePhone,code);
//这里用于比对验证码的正确性,此处直接判断是否等于123456
if (StringUtils.equals(code,"123456")) {
return new SmsCodeAuthenticationToken(mobilePhone,code, Collections.emptyList());
}
//SpringSecurity规范:认证失败就抛异常,不要返回null
throw new BadCredentialsException("验证码错误");
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
SmsCodeLoginConfigurer
问题
在构建SmsCodeLoginConfigurer时,如果直接调用super构造器传入我们的自定义filter,运行会产生如下错误
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.DefaultSecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: The Filter class com.xiang.support.SmsCodeAuthenticationFilter does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:171) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:650) ~[spring-beans-6.0.13.jar:6.0.13]
... 19 common frames omitted
Caused by: java.lang.IllegalArgumentException: The Filter class com.xiang.support.SmsCodeAuthenticationFilter does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.
at org.springframework.security.config.annotation.web.builders.HttpSecurity.addFilter(HttpSecurity.java:3298) ~[spring-security-config-6.1.5.jar:6.1.5]
at org.springframework.security.config.annotation.web.builders.HttpSecurity.addFilter(HttpSecurity.java:143) ~[spring-security-config-6.1.5.jar:6.1.5]
at org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer.configure(AbstractAuthenticationFilterConfigurer.java:302) ~[spring-security-config-6.1.5.jar:6.1.5]
at org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer.configure(AbstractAuthenticationFilterConfigurer.java:61) ~[spring-security-config-6.1.5.jar:6.1.5]
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.configure(AbstractConfiguredSecurityBuilder.java:355) ~[spring-security-config-6.1.5.jar:6.1.5]
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:309) ~[spring-security-config-6.1.5.jar:6.1.5]
at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38) ~[spring-security-config-6.1.5.jar:6.1.5]
at com.xiang.config.SecurityConfig.securityFilterChain(SecurityConfig.java:49) ~[classes/:na]
at com.xiang.config.SecurityConfig$$SpringCGLIB$$0.CGLIB$securityFilterChain$0(<generated>) ~[classes/:na]
at com.xiang.config.SecurityConfig$$SpringCGLIB$$FastClass$$1.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258) ~[spring-core-6.0.13.jar:6.0.13]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-6.0.13.jar:6.0.13]
at com.xiang.config.SecurityConfig$$SpringCGLIB$$0.securityFilterChain(<generated>) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:139) ~[spring-beans-6.0.13.jar:6.0.13]
... 20 common frames omitted
打开HttpSecurity.java:3298,发现在addFilter方法中,
@Override
public HttpSecurity addFilter(Filter filter) {
Integer order = this.filterOrders.getOrder(filter.getClass());
if (order == null) {
throw new IllegalArgumentException("The Filter class " + filter.getClass().getName()
+ " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.");
}
this.filters.add(new OrderedFilter(filter, order));
return this;
}
order == null,故抛出异常。
原因
过滤器链中的filter是有顺序的
找到getOrder方法所在的类FilterOrderRegistration
final class FilterOrderRegistration {
private static final int INITIAL_ORDER = 100;
//步长为100
private static final int ORDER_STEP = 100;
private final Map<String, Integer> filterToOrder = new HashMap<>();
FilterOrderRegistration() {
//每执行一次next增加100
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(DisableEncodeUrlFilter.class, order.next());
put(ForceEagerSessionCreationFilter.class, order.next());
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextHolderFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next(); // gh-8105
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter",
order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(AuthorizationFilter.class, order.next());
put(SwitchUserFilter.class, order.next());
}
void put(Class<? extends Filter> filter, int position) {
this.filterToOrder.putIfAbsent(filter.getName(), position);
}
Integer getOrder(Class<?> clazz) {
while (clazz != null) {
Integer result = this.filterToOrder.get(clazz.getName());
if (result != null) {
return result;
}
clazz = clazz.getSuperclass();
}
return null;
}
private static class Step {
private int value;
private final int stepSize;
Step(int initialValue, int stepSize) {
this.value = initialValue;
this.stepSize = stepSize;
}
int next() {
int value = this.value;
this.value += this.stepSize;
return value;
}
}
}
因此,需要我们为自定义的SmsCodeAuthenticationFilter添加一个顺序,使order不为null
解决
在AbstractAuthenticationFilterConfigurer类中的configure方法中最后调用了addFilter方法,但是SpringSecurity并没有在AbstractAuthenticationFilterConfigurer类中添加addFilter方法,我们无法在自己的SmsCodeLoginConfigurer类中通过addFilter方法加入我们的filter。
有两种解决方法:
1.通过反射机制调用HttpSecurity实例的filterOrders属性的put方法,手动添加我们的filter并指定order(推荐)
SmsCodeLoginConfigurer类
public class SmsCodeLoginConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractAuthenticationFilterConfigurer<H, SmsCodeLoginConfigurer<H>, SmsCodeAuthenticationFilter> {
public SmsCodeLoginConfigurer(String defaultLoginProcessingUrl) {
super(new SmsCodeAuthenticationFilter(defaultLoginProcessingUrl), defaultLoginProcessingUrl);
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, HttpMethod.POST.name());
}
}
config类
@Configuration
public class SecurityConfig {
@Bean
public DefaultSecurityFilterChain securityFilterChain(HttpSecurity http,
AuthenticationSuccessHandler authenticationSuccessHandler,
AuthenticationFailureHandler authenticationFailureHandler,
AuthenticationEntryPoint authenticationEntryPoint) throws Exception {
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.csrf()
.disable()
//配置权限
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
//配置登录入口
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint);
Field filterOrdersField = HttpSecurity.class.getDeclaredField("filterOrders");
filterOrdersField.setAccessible(true);
Object filterRegistration = filterOrdersField.get(http);
Method putMethod = filterRegistration.getClass().getDeclaredMethod("put", Class.class, int.class);
putMethod.setAccessible(true);
//要把SmsCodeAuthenticationFilter的order放在UsernamePasswordAuthenticationFilter附近
//因为UsernamePasswordAuthenticationFilter是1900,所以将其order设为1901
putMethod.invoke(filterRegistration, SmsCodeAuthenticationFilter.class,1901);
SmsCodeLoginConfigurer<HttpSecurity> httpSecuritySmsCodeLoginConfigurer = new SmsCodeLoginConfigurer<>("/smsCodeLogin");
httpSecuritySmsCodeLoginConfigurer
.loginProcessingUrl("/smsCodeLogin")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
http.apply(httpSecuritySmsCodeLoginConfigurer);
http.authenticationProvider(new SmsCodeAuthenticationProvider());
return http.build();
}
}
2.重写AbstractAuthenticationFilterConfigurer类的configure方法
SmsCodeLoginConfigurer类
public class SmsCodeLoginConfigurer2<B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<SmsCodeLoginConfigurer2<B>,B> {
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
public void configure(B builder) throws Exception {
SmsCodeAuthenticationFilter authenticationFilter = new SmsCodeAuthenticationFilter("/smsCodeLogin");
//AuthenticationManager在构建的时候会把AuthenticationManager放到SharedObject中
authenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
authenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
authenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
authenticationFilter.setAuthenticationDetailsSource(new WebAuthenticationDetailsSource());
builder.addFilterAfter(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
public final SmsCodeLoginConfigurer2<B> successHandler(AuthenticationSuccessHandler successHandler) {
this.authenticationSuccessHandler = successHandler;
return this;
}
public final SmsCodeLoginConfigurer2<B> failureHandler(AuthenticationFailureHandler failureHandler) {
this.authenticationFailureHandler = failureHandler;
return this;
}
}
config类
//短信验证码登录第二种配置方式
@Configuration
public class SecurityConfig2 {
@Bean
public DefaultSecurityFilterChain securityFilterChain(HttpSecurity http,
AuthenticationSuccessHandler authenticationSuccessHandler,
AuthenticationFailureHandler authenticationFailureHandler,
AuthenticationEntryPoint authenticationEntryPoint) throws Exception {
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.csrf()
.disable()
//配置权限
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
//配置登录入口
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint);
SmsCodeLoginConfigurer2<HttpSecurity> smsCodeLoginConfigurer2 = new SmsCodeLoginConfigurer2<>();
smsCodeLoginConfigurer2
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
http.apply(smsCodeLoginConfigurer2);
http.authenticationProvider(new SmsCodeAuthenticationProvider());
return http.build();
}
}
关于@Configuration注解的proxyBeanMethods属性
@Configuration中有属性proxyBeanMethods,默认为true,即是否使用类中bean的代理bean
在含有@Configuration(proxyBeanMethods = false)的类中,
假设有两个bean,记为xxx和yyy
如果yyy有需要注入xxx的属性,
@Bean
public Xxx xxx() {
return Inew Xxx() ;
}
@Bean
public Yyy yyy() {
Yyy yyy = new Yyy();
yyy.setXxx(xxx());
}
则我们如代码所示手动调用xxx方法为属性赋值
会使yyy的Xxx属性维护一个Xxx的实例,同时spring容器也会维护一个Xxx的实例
这时会破坏xxx的单例。
而proxyBeanMethods默认为true,使用代理类就可以避免这个问题