Skip to main content

如何做好登陆业务

一、用户密码设置

1、限制用户输入易破解的密码

例如 123456,常见的禁用密码如下:

BANNED_PASSWORDS = [000000,111111,11111111,112233,121212,123123,123456,1234567,12345678,123456789,131313,232323,654321,666666,696969,777777,7777777,8675309,987654, “aaaaaa”, “abc123”, “abc123”, “abcdef”, “abgrtyu”, “access”, “access14”, “action”, “albert”, “alberto”, “alexis”, “alejandra”, “alejandro”, “amanda”, “amateur”, “america”, “andrea”, “andrew”, “angela”, “angels”, “animal”, “anthony”, “apollo”, “apples”, “arsenal”, “arthur”, “asdfgh”, “asdfgh”, “ashley”, “asshole”, “august”, “austin”, “badboy”, “bailey”, “banana”, “barney”, “baseball”, “batman”, “beatriz”, “beaver”, “beavis”, “bigcock”, “bigdaddy”, “bigdick”, “bigdog”, “bigtits”, “birdie”, “bitches”, “biteme”, “blazer”, “blonde”, “blondes”, “blowjob”, “blowme”, “bond007”, “bonita”, “bonnie”, “booboo”, “booger”, “boomer”, “boston”, “brandon”, “brandy”, “braves”, “brazil”, “bronco”, “broncos”, “bulldog”, “buster”, “butter”, “butthead”, “calvin”, “camaro”, “cameron”, “canada”, “captain”, “carlos”, “carter”, “casper”, “charles”, “charlie”, “cheese”, “chelsea”, “chester”, “chicago”, “chicken”, “cocacola”, “coffee”, “college”, “compaq”, “computer”, “Cookie”, “cooper”, “corvette”, “cowboy”, “cowboys”, “crystal”, “cumming”, “cumshot”, “dakota”, “dallas”, “daniel”, “danielle”, “debbie”, “dennis”, “diablo”, “diamond”, “doctor”, “doggie”, “dolphin”, “dolphins”, “donald”, “dragon”, “dreams”, “driver”, “eagle1”, “eagles”, “edward”, “einstein”, “erotic”, “estrella”, “extreme”, “falcon”, “fender”, “ferrari”, “firebird”, “fishing”, “florida”, “flower”, “flyers”, “football”, “forever”, “freddy”, “freedom”, “fucked”, “fucker”, “fucking”, “fuckme”, “fuckyou”, “gandalf”, “gateway”, “gators”, “gemini”, “george”, “giants”, “ginger”, “golden”, “golfer”, “gordon”, “gregory”, “guitar”, “gunner”, “hammer”, “hannah”, “hardcore”, “harley”, “heather”, “helpme”, “hentai”, “hockey”, “hooters”, “horney”, “hotdog”, “hunter”, “hunting”, “iceman”, “iloveyou”, “internet”, “iwantu”, “jackie”, “jackson”, “jaguar”, “jasmine”, “jasper”, “jennifer”, “jeremy”, “jessica”, “johnny”, “johnson”, “jordan”, “joseph”, “joshua”, “junior”, “justin”, “killer”, “knight”, “ladies”, “lakers”, “lauren”, “leather”, “legend”, “letmein”, “letmein”, “little”, “london”, “lovers”, “maddog”, “madison”, “maggie”, “magnum”, “marine”, “mariposa”, “marlboro”, “martin”, “marvin”, “master”, “matrix”, “matthew”, “maverick”, “maxwell”, “melissa”, “member”, “mercedes”, “merlin”, “michael”, “michelle”, “mickey”, “midnight”, “miller”, “mistress”, “monica”, “monkey”, “monkey”, “monster”, “morgan”, “mother”, “mountain”, “muffin”, “murphy”, “mustang”, “naked”, “nascar”, “nathan”, “naughty”, “ncc1701”, “newyork”, “nicholas”, “nicole”, “nipple”, “nipples”, “oliver”, “orange”, “packers”, “panther”, “panties”, “parker”, “password”, “password”, “password1”, “password12”, “password123”, “patrick”, “peaches”, “peanut”, “pepper”, “phantom”, “phoenix”, “player”, “please”, “pookie”, “porsche”, “prince”, “princess”,private, “purple”, “pussies”, “qazwsx”, “qwerty”, “qwertyui”, “rabbit”, “rachel”, “racing”, “raiders”, “rainbow”, “ranger”, “rangers”, “rebecca”, “redskins”, “redsox”, “redwings”, “richard”, “robert”, “roberto”, “rocket”, “rosebud”, “runner”, “rush2112”, “russia”, “samantha”, “sammy”, “samson”, “sandra”, “saturn”, “scooby”, “scooter”, “scorpio”, “scorpion”, “sebastian”, “secret”, “sexsex”, “shadow”, “shannon”, “shaved”, “sierra”, “silver”, “skippy”, “slayer”, “smokey”, “snoopy”, “soccer”, “sophie”, “spanky”, “sparky”, “spider”, “squirt”, “srinivas”, “startrek”, “starwars”, “steelers”, “steven”, “sticky”, “stupid”, “success”, “suckit”, “summer”, “sunshine”, “superman”, “surfer”, “swimming”, “sydney”, “tequiero”, “taylor”, “tennis”, “teresa”, “tester”, “testing”, “theman”, “thomas”, “thunder”, “thx1138”, “tiffany”, “tigers”, “tigger”, “tomcat”, “topgun”, “toyota”, “travis”, “trouble”, “trustno1”, “tucker”, “turtle”, “twitter”, “united”, “vagina”, “victor”, “victoria”, “viking”, “voodoo”, “voyager”, “walter”, “warrior”, “welcome”, “whatever”, “william”, “willie”, “wilson”, “winner”, “winston”, “winter”, “wizard”, “xavier”, “xxxxxx”, “xxxxxxxx”, “yamaha”, “yankee”, “yankees”, “yellow”, “zxcvbn”, “zxcvbnm”, “zzzzzz”]

另外,还可以:

  • 限制用户密码的长度
  • 是否有大小写
  • 是否有数字
  • ...

可以添加实时密码强度来诱导用户输入强密码。

2、避免明文保存用户密码

用户密码一定要加密保存,最好是用不可逆的加密,如 MD5SHA1 之类有 hash 算法的不可逆加密算法,以降低网站数据泄漏后的风险。

3、使用 HTTPS 协议传输密码

HTTP 是明文协议,对用户名和密码的传输也是明文的,很不安全,要做到加密传输就必需使用 HTTPS 协议。

二、用户登录状态

由于 HTTP 是无状态的协议,每次请求都是独立无关联的,无法记录用户访问状态,而页面跳转过程中常常需要知道用户的状态,例如登录状态,以知道用户是否有权来操作一些功能或查看数据。

因此,每个页面都需要对用户的身份进行认证。但又不能让用户在每个页面上都输入用户名和密码,要实现这一功能,用得最多的技术就是浏览器的 Cookie,把用户登录的信息存放在客户端的 Cookie 中,然后每个页面都从这个 Cookie 中获取用户是否登录的信息,从而达到状态记录和用户验证的目的。使用 Cookie 需要注意:

要避免在 Cookie 中存放用户密码,包括加密的密码,该密码可以被人获取并尝试离线穷举。

2、正确设计「记住密码」功能

「记住密码」功能是一个安全隐患,一般情况下,用户勾选这个功能,系统会生成一个 Cookie,包括用户名和一个固定的散列值,但这种做法并不安全,可以通过以下更为安全的方法来完成:

  • 在 Cookie 中保存:用户名、登录序列、登录 token

    • 用户名:明文存放;
    • 登录序列:一个被 MD5 散列过的随机数,仅当强制用户输入密码时更新(例如用户修改了密码)
    • 登录 token:一个被 MD5 散列过的随机数,仅在一个登录 session 内有效,新的登录 session 会更新它。
  • 上面三个会存在服务器上,服务器会对客户端的 Cookie 进行验证,这样做会有以下效果:

    • 登录 token 是单实例登录,即一个用户只能有一个登录实例;
    • 登录序列用来做盗用行为检测,如果用户的 Cookie 被盗,盗用者用这个 Cookie 访问网站时系统会以为是合法用户,然后更新 登录token,而原用户重新访问时,系统发现只有用户名登录序列相同,但是登录 token 不正确,这样系统就知道该用户可能被盗号,于是系统可以清除并更改登录序列和 登录 token,使所有 Cookie 失效,要求用户输入密码和发出警告。
  • 上述的设计还有一些问题,例如:同一用户的不同设备登录,或在同一设备使用不同的浏览器登录,一个设备会让另一个设备的登录 token登录序列失效,从而使其它设备和浏览器需要重新登录,造成 Cookie 被盗用的假象,因此在服务器还需要考虑 **IP 地址:

    • 如果以密码方式登录,则无需更新服务器的登录序列和 登录 token(但需更新 Cookie —— 假设密码只有用户自己知道)
    • 如果 IP 相同,则无需更新服务器的登录序列和 登录 token(但需更新 Cookie —— 假设同一用户只有同一 IP)
    • 如果 IP 不同且没有用密码登录,则登录 token 会在多个 IP 间发生变化(登录 token 在两个或多个 IP 间被来回切换),当一段时间内达到一定次数后,系统才会觉得被盗用,此时在后台清除登录序列登录 token,让 Cookie 失效,强制用户输入或更改密码,以保证多台设备上的 Cookie 一致;
  • 避免让 Cookie 有权访问所有操作,否则就是 XSS 攻击,下面的这些功能一定要用户输入密码:

    • 修改用户关键信息,如密码邮箱等;
    • 用户支付功能。
  • 权衡 Cookie 的过期时间,如果永不过期,用户使用体验不错但容易让用户忘记登录密码。一般过期期限设为 2 周或一个月。

三、正确设计「忘记密码」功能

1、避免安全问答验证

诸如生日、学校之类的安全问答,在很多社交平台上可以查到,因此这种验证并不是很安全。

2、邮箱重置及多重认证

  • 通过邮件重置:当用户申请找回密码时,系统生成一个 MD5 唯一的随机字串放在数据库中,然后设置上时限(比如 1 小时内),给用户发一个邮件,包含这个 MD5 字串的链接,用户通过点击链接来重新设置新的密码;

  • 多重认证:例如通过手机 + 邮件的方式让用户输入验证码,也可以使用 U 盾、人工等方式核实用户身份。

四、密码探测防守

1、使用验证码

通过后台随机产生的一个短暂的验证码,一般是计算机难以识别的图片,防止以程序的方式来不断尝试用户密码。可以在发现同个 IP 地址发出大量搜索后,再要求输入验证码,提升用户体验。

2、限制密码失败次数

调置密码失败上限,如果失败过多则锁定帐号,需要用户以找回密码的方式来重新激活帐号。但该功能可能会被恶意使用。最好的方法是增加尝试的时间成本,例如手机密码输入错误要求再次尝试的时间间隔,为了防止恶意脚本的攻击,最好再加上验证码,验证码出错次数过多不禁止登录而是禁 IP。

3、系统全局防守

上述的防守只针对某一个别用户。恶意者们深知这一点,所以,他们一般会动用“僵尸网络”轮着尝试一堆用户的密码,所以上述的那种方法可能还不够好。我们需要在系统全局域上监控所有的密码失败的次数。当然,这个需要我们平时没有受到攻击时的数据做为支持。比如你的系统,平均每天有5000次的密码错误的事件,那么你可以认为,当密码错误大幅超过这个数后,而且时间相对集中,就说明有黑客攻击。这个时候你怎么办?一般最常见使用的方法是让所有的用户输错密码后再次尝试的时间成本增加。

五、SSO 单点登录

SSO(Single Sign On)单点登录即在多个应用系统中,只需登录一次,就可以访问其他相互信任的应用系统。例如:

  • 在登录天猫的情况下,打开淘宝会自动登录;
  • 在登录百度的情况下,打开百度地图也自动登录。

1、SSO 实现原理

要实现SSO,需要以下主要的功能:

  • 所有应用系统共享一个身份认证系统:统一认证系统是 SSO 的前提之一,认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对 ticket 进行效验,判断其有效性;

  • 所有应用系统 (业务系统) 能够识别和提取 ticket 信息。要实现 SSO 的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户,应用系统 (业务系统) 应该能对 ticket 进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。

上图有 4 个系统:Application1、Application2、Application3、和 SSO。其中:

  • Application1、Application2、Application3 没有登录模块,而 SSO 只有登录模块,没有其他的业务模块;
  • Application1、Application2、Application3 需要登录时,将统一跳转到 SSO 系统,随着 SSO 系统完成登录,其他的应用系统也就随之登录了,这样就符合单点登录(SSO)的定义。

2、SSO 实现方案

2-1、普通的登录认证过程

浏览器(Browser)中访问一个需要登陆的业务系统,填写完用户名和密码后,完成登录认证。这时,在用户的 session 中标记登录状态为 yes(已登录),同时在浏览器(Browser)中写入 Cookie(),这个 Cookie 是用户的唯一标识,当用户再访问这个应用时,请求中会带上这个 Cookie,服务端根据这个 Cookie 找到对应的 session,通过 session 来判断用户是否登录。如果不做特殊配置,这个 Cookie 的名字叫做 JSESSIONID,值在服务端(server)是唯一的。

2-2、同域名下的单点登录

Cookie 的 domain 属性设为当前域的父域,父域的 Cookie 会被子域所共享,path 属性默认为 web 应用的上下文路径。

利用 Cookie 的这个特性,只需将 Cookie 的 domain 属性设为父域的域名(主域名),同时将 Cookie 的 path 属性设为根路径,将 Token 保存到父域中,这样所有的子域应用就都可以访问到这个 Cookie。

举个例子:

现在需要一个登录系统,叫 sso.leophen.com,只要在 sso.leophen.com 登录,blog.leophen.commap.leophen.com 也就登录了。由上面 SSO 实现原理可知,在 sso.leophen.com 中登录了,其实是在 sso.leophen.com 服务端的 session 中记录登录状态,同时在浏览器端(Browser)的 sso.leophen.com 下写入 Cookie。那怎么才能让 blog.leophen.commap.leophen.com 登录呢?这里有两个问题:

  • 问题一:note Cookie 不能跨域的,Cookie 的 domain 属性是sso.leophen.com,在给 blog.leophen.commap.leophen.com 发送请求时是带不上的。

针对这个问题,可利用上述 Cookie 的特性,将 Cookie 的 domain 属性设为父域的域名(主域名),同时将 Cookie 的 path 属性设为根路径,将 Token 保存到父域中,这样所有的子域应用就都可以访问到这个 Cookie。不过这要求应用的域名建立在共同的主域名下,如 blog.leophen.commap.leophen.com,都建立在 leophen.com 这个主域名之下,就可以通过这种方式来实现单点登录。

  • 问题二:sso、blog 和 map 是不同的应用,它们的 session 存在自己的应用中,互不共享。

针对这个问题,当在 sso 系统登录后,再访问 blog,Cookie 也带到了 blog 的服务端(Server),blog 的服务端要怎么找到这个 Cookie 对应的 session 呢?这里就要把 3 个系统的 session 共享,例如使用 Spring-Session。

流程图:

2-3、不同域名下的单点登录

比较常见的情况是不同顶域下的多业务系统实现 SSO 登录,因为不同顶域之间的 Cookie 无法共享的,这时就无法用上述同顶域下的单点登录了。下面是 CAS 标准的单点登录流程:

上图 CAS 的标准单点登录流程是企业中最常用到的 SSO 登录标准规范,具体流程如下:

  1. 用户访问 app1 系统,app1 系统是需要登录的,但用户现在没有登录;
  2. 跳转到 CAS server,即 SSO 登录系统,图中的 CAS Server 就是 SSO 系统。SSO 系统也没有登录态(浏览器端没有 Cookie,服务器端没有 session),弹出用户登录页;
  3. 用户填写用户名、密码,SSO 系统进行认证后,将登录状态写入 SSO 服务端的 session,浏览器中写入 SSO 域下的 Cookie;
  4. SSO 系统登录完成后会生成一个 ST(Service Ticket),然后跳转到 app1 系统,同时将 ST 作为参数传递给 app 系统;
  5. app1 系统拿到 ST 后,从后台向 SSO 发送请求,验证 ST 是否有效;
  6. 验证通过后,app1 系统将登录状态写入 session 并设置 app1 域下的 Cookie。

至此,跨域单点登录就完成了,当用户再访问 app1 系统时,app1 就是登录的。

下面来看访问 app2 系统的流程:

  1. 用户访问 app2 系统,app2 系统没有登录,跳转到 SSO;
  2. 由于此时 SSO 已登录,不需要重新登录认证;
  3. SSO 生成 ST,浏览器跳转到 app2 系统,并将 ST 作为参数传递给 app2
  4. app2 拿到 ST,后台访问 SSO,验证 ST 是否有效;
  5. 验证成功后,app2 将登录状态写入 session,并在 app2 域下写入 Cookie。

这样,app2 系统不需要走登录流程就可以登录。SSO、app1app2 在不同的域,它们之间的 session 不共享也没问题。

另外:用户登录成功之后,会与 SSO 认证中心及各个子系统建立会话,用户与 SSO 认证中心建立的会话称为全局会话。用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过 SSO 认证中心。全局会话与局部会话有如下约束关系:

  • 局部会话存在,全局会话一定存在;
  • 全局会话存在,局部会话不一定存在;
  • 全局会话销毁,局部会话必须销毁。

3、具体代码的实现

3-1、前端代码

// axios.js
const CAS = 'https://sso.machao.com/cas/login';
const CALLBACK = window.location.origin + '/rest/sso/callback';

// 有的首页需要自动 sso 登录的话,可以将这个注释掉
const AUTO_GOTO_CAS_SKIP_URL = ['/'];

axios.interceptors.request.use(config => {
config.headers.Accept = 'application/json';
config.withCredentials = true;
config.baseURL = API_URL;
return config;
});

axios.interceptors.response.use(
res => {
const { data } = res;
if (data && data.code === '200') {
return data;
}
if (data && data.code !== '200' && data.message) {
message.error(data.message);
}
return Promise.reject(data);
},
// 前端和后端约定好无登录态返回的状态码 401
err => {
if (err?.response?.status === 401 &&
!AUTO_GOTO_CAS_SKIP_URL.includes(window.location.pathname)
) {
// 跳转 SSO 登录页面,service 里是业务系统对应的登录鉴权接口
// 拿到 CAS 登录后的 ticket,用 callback 接口进行权限认证
// 鉴权通过,会在业务域名下种业务系统局部登录态的 Cookie
// 认证通过,就重定向到浏览器 url 输入的原页面
window.location.href = `${CAS}?service=${encodeURIComponent(
CALLBACK + `?redirect=${encodeURIComponent(window.location.href)}`
)}`;
}
Promise.reject(err);
}
);

3-2、后端代码

// controller 层
@ResponseBody
@RequestMapping(value = "/callback")
public void callback(HttpServletRequest request, HttpServletResponse response) throws IOException {
String userName = getUserName(request);
if (StringUtils.isNotBlank(userName)) {
String userToken = tokenService.getTokenByUserName(userName);
log.info("userName: {}, userToken:{}", userName, userToken);
Cookie cookieToken = new Cookie("userToken", userToken);
cookieToken.setMaxAge(ApplicationConstants.COOKIE_AGE_SECONDS);
cookieToken.setPath("/");
cookieToken.setHttpOnly(true);
response.addCookie(cookieToken);
Cookie cookieName = new Cookie("userName", userName);
cookieName.setMaxAge(ApplicationConstants.COOKIE_AGE_SECONDS);
cookieName.setPath("/");
cookieName.setHttpOnly(true);
response.addCookie(cookieName);

String redirect = CommonUtils.safeGetParameter(request, "redirect");
SecurityUtils.checkResponseSplitting(redirect);
response.sendRedirect(redirect);
}
}

private String getUserName(HttpServletRequest request) {
String ticket = CommonUtils.safeGetParameter(request, "ticket");
if (CommonUtils.isNotBlank(ticket)) {
try {
Assertion assertion = validator.validate(ticket, constructServiceUrl(request));
return assertion.getPrincipal().getName();
} catch (Exception e) {
log.error("failed to validate sso", e);
}
}
log.warn("sso callback, ticket is blank");
return null;
}

private String constructServiceUrl(HttpServletRequest request) throws Exception {
String uri = request.getRequestURI();
String redirect = request.getParameter("redirect");
if (StringUtils.isNotBlank(redirect)) {
uri = uri + "?redirect=" + URLEncoder.encode(redirect, "UTF-8");
}
return "https://" + request.getServerName() + uri;
}
/**
* Spring MVC 配置
*/
@Configuration
class SpringMvcConfig {
private static final int TOKEN_FILTER_ORDER = xxxx;

@Bean
WebServerCustomizer applicationWebServerCustomizer() {
return new WebServerCustomizer();
}

@Bean
WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addFormatters(@Nonnull FormatterRegistry registry) {
// 用于 string <==> HasIntValue 之间的转换
HasIntValueConverter converter = new HasIntValueConverter();
registry.addConverter(converter);
}
};
}

@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}

@Bean
public FilterRegistrationBean<TokenFilter> filterRegistrationBeanTokenFilter(TokenFilter tokenFilter) {
FilterRegistrationBean<TokenFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(tokenFilter);
registrationBean.setOrder(TOKEN_FILTER_ORDER);
return registrationBean;
}
}
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.springframework.http.HttpStatus;

import com.google.common.collect.Sets;

import lombok.extern.slf4j.Slf4j;


@Slf4j
public class TokenFilter implements Filter {
private static final String KEY = "xxxx";
private static final String ROOT_PATH = "/";

private PatternFilter allowList = new PatternFilter(Sets.newHashSet("/rest/sso/callback"));

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.warn("enter doFilter");
// 本地环境不校验
if (ApplicationEnv.current() == EnvironmentEnum.DEV) {
log.warn("enter dev");
chain.doFilter(request, response);
return;
}

// 白名单 path 不校验
String path = ((HttpServletRequest) request).getServletPath();
if (ROOT_PATH.equals(path) || this.allowList.contains(path)) {
log.warn("enter white path");
chain.doFilter(request, response);
return;
}

Cookie[] cookies = ((HttpServletRequest) request).getCookies();
log.info("cookies:");
if (cookies == null) {
log.warn("TokenFilter.doFilter: cookies is null");
((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}

String userNameByToken = null;
String userName = null;
for (Cookie cookie : cookies) {
log.info("进入cookie循环");
if (Objects.equals("userToken", cookie.getName())) {
userNameByToken = getUserNameByToken(cookie.getValue());
} else if (Objects.equals("userName", cookie.getName())) {
userName = cookie.getValue();
}
}

if (userNameByToken != null && Objects.equals(userNameByToken, userName)) {
log.info("TokenFilter success for {}", userName);
chain.doFilter(request, response);
return;
}

//鉴权失败,给客户端返回401
log.warn("TokenFilter.doFilter: UNAUTHORIZED");
((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value());
}

@Override
public void destroy() {
log.info("destroy");
}

private static String getUserNameByToken(String token) {
int index = token.indexOf("-");
if (index != -1) {
byte[] bytes = Base64.decodeBase64(token.substring(0, index));
Key keySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher;

try {
cipher = Cipher.getInstance("AES");
cipher.init(2, keySpec);
String userName = new String(cipher.doFinal(bytes), StandardCharsets.UTF_8);
if (token.substring(index + 1).equals(userName)) {
return userName;
}
} catch (Exception var6) {
log.error("aes decrypt error. token: {}", token, var6);
}
}

return null;
}

public static class PatternFilter {
private final List<Pattern> patternList = new LinkedList<>();

public PatternFilter(Set<String> patternStrList) {
if (Objects.isNull(patternStrList) || patternStrList.isEmpty()) {
return;
}
init(patternStrList);
}

private void init(Set<String> patternStrList) {
for (String regex : patternStrList) {
Pattern pattern = Pattern.compile(regex);
this.patternList.add(pattern);
}
}

public boolean contains(String path) {
for (Pattern p : this.patternList) {
if (p.matcher(path).find()) {
return true;
}
}
return false;
}
}
}