2020年8月17日,shiro发布了1.6.0版本,修复了绕过认证的bug。但实际升级后,使用中发现第一次打开浏览器访问时会出现Invalid request。具体错误提示如下:
降级到1.4.2时是可以正常登录的,反复试验几次,确定是升级shiro版本引起,阅读shiro 1.6.0源码,发现shiro引入了InvalidRequestFilter过滤器,目的是验证url上是否有恶意伪造的特殊字符。但这个类也正好将登录url拼接的
;jsessionId=xxx
也一起拦截了,结果就出现了上图中的错误提示。
InvalidRequestFilter的containsSemicolon()方法对url上的特殊符号“;”进行了拦截处理,但不知为何没有考虑到登录时拼接的jsessionId。
找到了问题点,解决起来就很简单,那就是改造containsSemicolon方法,对登录的url进行特殊处理放行。这里告诉大家个小秘密,那就是怎么修改别人jar包里面的代码。只需在自己项目的源码里面创建一个相同的class(包路径类名都必须相同)就可以了,然后将源代码全部复制粘贴过来,接下来就是修改源码。修改后的源码如下,大家可以直接复制使用。
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.shiro.web.filter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* A request filter that blocks malicious requests. Invalid request will respond with a 400 response code.
* <p>
* This filter checks and blocks the request if the following characters are found in the request URI:
* <ul>
* <li>Semicolon - can be disabled by setting {@code blockSemicolon = false}</li>
* <li>Backslash - can be disabled by setting {@code blockBackslash = false}</li>
* <li>Non-ASCII characters - can be disabled by setting {@code blockNonAscii = false}, the ability to disable this check will be removed in future version.</li>
* </ul>
*
* @see <a href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html">This class was inspired by Spring Security StrictHttpFirewall</a>
* @since 1.6
*/
public class InvalidRequestFilter extends AccessControlFilter {
private static final List<String> SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
private static final List<String> BACKSLASH = Collections.unmodifiableList(Arrays.asList("\", "%5c", "%5C"));
private boolean blockSemicolon = true;
private boolean blockBackslash = true;
private boolean blockNonAscii = true;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
String uri = WebUtils.toHttp(request).getRequestURI();
return !containsSemicolon(request,uri)
&& !containsBackslash(uri)
&& !containsNonAsciiCharacters(uri);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String uri = WebUtils.toHttp(request).getRequestURI();
WebUtils.toHttp(response).sendError(400, "Invalid request");
return false;
}
private String ctx=null;
private boolean containsSemicolon(ServletRequest request,String uri) {
if (isBlockSemicolon()) {
if(ctx == null) {
ctx = WebUtils.toHttp(request).getContextPath();
}
// 登录url拼接的jsessionId进行放行
if(uri.startsWith(ctx + this.getLoginUrl() + ";jsessionid=") ||
uri.startsWith(ctx + this.getLoginUrl() + "%3bjsessionid=") ||
uri.startsWith(ctx + this.getLoginUrl() + "%3Bjsessionid=")) {
return false;
}
boolean value = SEMICOLON.stream().anyMatch(uri::contains);
return value;
}
return false;
}
private boolean containsBackslash(String uri) {
if (isBlockBackslash()) {
return BACKSLASH.stream().anyMatch(uri::contains);
}
return false;
}
private boolean containsNonAsciiCharacters(String uri) {
if (isBlockNonAscii()) {
return !containsOnlyPrintableAsciiCharacters(uri);
}
return false;
}
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
int length = uri.length();
for (int i = 0; i < length; i++) {
char c = uri.charAt(i);
if (c < ' ' || c > '~') {
return false;
}
}
return true;
}
public boolean isBlockSemicolon() {
return blockSemicolon;
}
public void setBlockSemicolon(boolean blockSemicolon) {
this.blockSemicolon = blockSemicolon;
}
public boolean isBlockBackslash() {
return blockBackslash;
}
public void setBlockBackslash(boolean blockBackslash) {
this.blockBackslash = blockBackslash;
}
public boolean isBlockNonAscii() {
return blockNonAscii;
}
public void setBlockNonAscii(boolean blockNonAscii) {
this.blockNonAscii = blockNonAscii;
}
}
shiro在浏览器第一次访问时会在url上拼接jsessionId到url后面,目的是确保cookie里面有sessionId以保持会话,但拼接时是用
;jsessionid=xxx
的形式拼接的,常规url后面拼接的都是“”和“&”,这是shiro过滤器处理的结果。
以上是我想到的解决办法,如果大家也遇到了这个问题并有更好的办法,希望能一起探讨。