Недавно я обнаружил, что, поскольку разработанный мной проект разделения внешнего и внутреннего интерфейса Vue использует структуру безопасности Spring, даже если некоторые обычные интерфейсы вызываются после успешной аутентификации входа в систему, всегда необъяснимым образом возникает проблема перенаправления 302, что приводит к данные интерфейса не могут выйти. Странно то, что эта проблема не возникает в локальной среде разработки, а возникает только после развертывания на сервере.
Интерфейс не может загрузить данные ответа
Идентификатор перенаправления интерфейса Location показывает, что требуется проверка подлинности при повторном входе, и этот запрос по-прежнему является запросом GET.
Эта проблема возникает, очевидно, потому что текущий пользователь находится в Spring. Информация аутентификации теряется в системе безопасности.,Странно то, что эта проблема не возникает в локальной среде разработки.,Причина в том, что интерфейс моей локальной среды разработки использует интерфейсный сервис, запущенный Vite.,Но при развертывании на сервере этоNginx
интерфейсные услуги。И авторSpring Класс конфигурации безопасности зарегистрирован для Jwt. tokenсертифицированный фильтрJwtAuthenticationFilterBean
, и зарегистрировался вUsernamePasswordAuthenticationFilter
До。проходитьjwt tokenСертификация эквивалентнаspring security
Каждый запрос пользователя сначала должен быть аутентифицирован.,Если информация аутентификации пользователя не сохранена вSecurityContext
в классеauthentication
Эта проблема перенаправления на страницу входа возникает при вызове интерфейса без входа для получения данных.。
Исходный код настроенного класса аутентификации токена Jwt выглядит следующим образом:
JwtAuthenticationFilterBean
private final static Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilterBean.class);
private String AUTHORIZATION_NAME = "Authorization";
// private String BEARER = "Bearer";
private static List<String> whiteRequestList = new ArrayList<>();
static {
whiteRequestList.add("/bonus/member/checkSafetyCode");
whiteRequestList.add("/bonus/login");
whiteRequestList.add("/bonus/member/login");
whiteRequestList.add("/bonus/common/kaptcha");
whiteRequestList.add("/bonus/admin/login");
whiteRequestList.add("/bonus/favicon.ico");
whiteRequestList.add("/bonus/doc.html");
whiteRequestList.add("/bonus/error");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
logger.info("requestUrl="+request.getRequestURI());
if(HttpMethod.OPTIONS.name().equals(request.getMethod())){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(whiteRequestList.contains(request.getRequestURI()) || (request.getRequestURI().contains("admin/dist") &&
request.getRequestURI().endsWith(".css") || request.getRequestURI().equals(".js") ||
request.getRequestURI().endsWith(".png") || request.getRequestURI().endsWith("favicon.ico"))){
// Если это запрос на проверку логина и кода безопасности, он будет отправлен напрямую.
filterChain.doFilter(servletRequest, servletResponse);
return;
} else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication!=null && authentication.getPrincipal()!=null){
MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal();
logger.info("memInfoDTO={}", JSONObject.toJSONString(memInfoDTO));
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String authToken = request.getHeader(AUTHORIZATION_NAME);
if(StringUtils.isEmpty(authToken)){
String message = "http header Authorization is null, user Unauthorized";
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
return;
} else {
try {
DecodedJWT decodedJWT = JWT.decode(authToken);
Map<String, Claim> claimMap = decodedJWT.getClaims();
Claim expireClaim = claimMap.get("exp");
Date expireDate = expireClaim.asDate();
// Токен подтверждения Срок годности истек?
if(expireDate.before(DateUtil.date(System.currentTimeMillis()))){
String message = "Authorization token expired";
this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
return;
}
Claim memAccountClaim = claimMap.get("memAccount");
if(memAccountClaim==null || StringUtils.isEmpty(memAccountClaim.asString())){
String message = "memAccount cannot be null";
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
} catch (JWTDecodeException e) {
String message = "JWT decode authToken failed, caused by " + e.getMessage();
this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
return;
}
}
}
}
вышеwhiteRequestList
Элементы представляют собой запросы белого списка.,Для запросов белого спискаSpring Security
Нет перехвата,Выпустить напрямую。Этого не произойдет после того, как запросы из белого списка будут развернуты на сервере.302Проблема с перенаправлением на страницу входа。Поскольку эти запросы белого списка находятся вSpring Security
Китай также выпустил, Исходный код выглядит следующим образом.
SecurityConfig#configure(HttpSecurity)
Исходный код метода:
@Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean();
// Зарегистрируйте jwt в цепочке фильтров http token Фильтр аутентификации
http.addFilterBefore(jwtAuthenticationFilterBean, UsernamePasswordAuthenticationFilter.class);
// Настройка междоменного доступа
http.cors().configurationSource(corsConfigurationSource())
.and().logout().invalidateHttpSession(true).logoutUrl("/member/logout").permitAll()
;
http.authorizeRequests()
// Освободить запрос на белый список
.antMatchers("/member/checkSafetyCode").permitAll()
.antMatchers("/doc.html").permitAll()
.antMatchers("/common/kaptcha").permitAll()
.antMatchers("/admin/login").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
// Форма аутентификации входа в систему
.and().formLogin()
.loginPage(loginPageUrl)
// Пользовательский интерфейс обработки входа в систему пользователя
.loginProcessingUrl("/member/login")
.successHandler((httpServletRequest, httpServletResponse, authentication) -> { // Возвращает информацию о пользователе и jwt в параметрах httpServletResponse. токен для клиента
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpStatus.OK.value());
PrintWriter printWriter = httpServletResponse.getWriter();
// Получить информацию о пользователе из информации аутентификации
MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal();
Map<String, Object> userMap = new HashMap<>();
userMap.put("memId", memInfoDTO.getMemId());
userMap.put("memAccount", memInfoDTO.getMemAccount());
userMap.put("memPwd", memInfoDTO.getMemPwd());
BigDecimal totalCredit = memInfoDTO.getTotalCreditAmount()!=null?new BigDecimal(memInfoDTO.getTotalCreditAmount()/100, mathContext): new BigDecimal("0.0");
userMap.put("totalCreditAmount", totalCredit);
BigDecimal usedCredit = memInfoDTO.getUsedCreditAmount()!=null?new BigDecimal(memInfoDTO.getUsedCreditAmount()/100, mathContext):new BigDecimal("0.0");
userMap.put("usedCreditAmount", usedCredit);
Long remainCredit = (memInfoDTO.getTotalCreditAmount()==null?0:memInfoDTO.getTotalCreditAmount()) - (memInfoDTO.getUsedCreditAmount()==null?0:memInfoDTO.getUsedCreditAmount());
BigDecimal remainCreditAmount = new BigDecimal(remainCredit/100, mathContext);
userMap.put("remainCreditAmount", remainCreditAmount);
userMap.put("authorities", memInfoDTO.getAuthorities());
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("memInfo", userMap);
dataMap.put("authenticatedToken", JwtTokenUtil.genAuthenticatedToken(userMap)); // Генерировать jwt на основе информации о пользователе token
ResponseResult<Map<String, Object>> responseResult = ResponseResult.success(dataMap, "login success");
printWriter.write(JSONObject.toJSONString(responseResult));
printWriter.flush();
printWriter.close();
}).permitAll()
.and().csrf().disable() // отключить csrf
.exceptionHandling() //Обработка исключений аутентификации
.accessDeniedHandler(accessDeniedHandler());
}
Существует два способа решения проблемы перенаправления 302, возникающей после развертывания на сервере.
configure(HttpSecurity)
Пара появляется в методе302重定Киз请求进行放行,КОсвободить запрос на белый список Относитесь к этому так же。Однако решить ее таким образом равносильно отказу от нее.Spring Security
система безопасности,Любой пользователь может получить доступ к внутреннему интерфейсу,В приложении нет безопасности,Не рекомендуется;JwtAuthenticationFilterBean#doFilter
方法серединапроходить反解jwt tokenПосле получения идентификационной информации пользователя, осуществляющего доступ,,затем внесите этоSpringSecurityContextHolder
Класс, привязанный к текущему потокуSecurityContext
переменная классаcontext
изauthentication
в переменной,Исходный код выглядит следующим образом:try {
DecodedJWT decodedJWT = JWT.decode(authToken);
Map<String, Claim> claimMap = decodedJWT.getClaims();
Claim expireClaim = claimMap.get("exp");
Date expireDate = expireClaim.asDate();
// Токен подтверждения Срок годности истек?
if(expireDate.before(DateUtil.date(System.currentTimeMillis()))){
String message = "Authorization token expired";
this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
return;
}
Claim memAccountClaim = claimMap.get("memAccount");
if(memAccountClaim==null || StringUtils.isEmpty(memAccountClaim.asString())){
String message = "memAccount cannot be null";
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
return;
}
logger.info("Пользователь:"+memAccountClaim.asString()+" запрос на звонок "+request.getRequestURI()+" Необходимо пройти переаттестацию");
// Информация о сертификации сборки
MemInfoDTO memInfoDTO = new MemInfoDTO();
memInfoDTO.setMemAccount(memAccountClaim.asString());
Claim memIdClaim = claimMap.get("memId");
memInfoDTO.setMemId(memIdClaim.asLong());
Claim memPwdClaim = claimMap.get("memPwd");
memInfoDTO.setMemPwd(memPwdClaim.asString());
Claim totalCreditClaim = claimMap.get("totalCreditAmount");
Double totalCreditAmount = totalCreditClaim.asDouble()*100;
String totalCreditAmountStr = String.valueOf(totalCreditAmount);
logger.info("totalCreditAmountStr={}", totalCreditAmountStr);
if(totalCreditAmountStr.lastIndexOf(".")>-1){
memInfoDTO.setTotalCreditAmount(Long.valueOf(totalCreditAmountStr.substring(0, totalCreditAmountStr.lastIndexOf("."))));
} else {
memInfoDTO.setTotalCreditAmount(Long.valueOf(totalCreditAmountStr));
}
Claim usedCreditClaim = claimMap.get("usedCreditAmount");
Double usedCreditAmount = usedCreditClaim.asDouble()*100;
String usedCreditAmountStr = String.valueOf(usedCreditAmount);
if(usedCreditAmountStr.lastIndexOf(".")>-1){
memInfoDTO.setUsedCreditAmount(Long.valueOf(usedCreditAmountStr.substring(0, usedCreditAmountStr.lastIndexOf("."))));
} else {
memInfoDTO.setUsedCreditAmount(Long.valueOf(usedCreditAmountStr));
}
Claim authorityClaim = claimMap.get("authorities");
List<String> authorities = authorityClaim.asList(String.class);
List<GrantedAuthority> authorityList = new ArrayList<>(authorities.size());
for(String authority: authorities){
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority);
authorityList.add(grantedAuthority);
}
memInfoDTO.setAuthorities(authorityList);
// Собрать объект аутентификации
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memInfoDTO, memInfoDTO.getMemPwd(), memInfoDTO.getAuthorities());
// Поместите объект аутентификации в SecurityContext.
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// Аутентификация заголовка запроса пройдена, Запрос на выпуск
filterChain.doFilter(servletRequest, servletResponse);
После изменения исходного кода переупакуйте и разверните его на сервере (в Интернете есть много подробных руководств о том, как упаковать и развернуть, поэтому я не буду здесь вдаваться в подробности)
После развертывания приложения и входа в систему система автоматически перейдет на домашнюю страницу http://javahsf.club:3000/home.
На данный момент проблем с перенаправлением 302 не возникнет, и вы также увидите, что данные страницы успешно загружены.
Просматривая сетевой запрос в режиме отладки F12, вы можете видеть, что проблемы с перенаправлением 302 нет и данные возвращаются успешно.
Чтобы дополнительно убедиться в том, что данные для входа в систему пользователя необходимо повторно аутентифицировать при вызове этого интерфейса, мы выполняем в каталоге развертывания cat ./logs/spring.log
Вы можете просмотреть следующие строки информации журнала, выполнив команду
2023-01-15 16:22:10.418 INFO 9638 --- [http-nio-0.0.0.0-8090-exec-2] c.b.b.c.JwtAuthenticationFilterBean : requestUrl=/bonus/openResult/page/data
2023-01-15 16:22:10.509 INFO 9638 --- [http-nio-0.0.0.0-8090-exec-2] c.b.b.c.JwtAuthenticationFilterBean : Пользователь: heshengfu запрос на звонок /bonus/openResult/page/data Необходимо пройти повторную сертификацию
Это проверено302重定Киз问题是接口До是spring security
框架需要重新认证用户登录信息却没有拿到用户из认证信息导致из,Вам нужно только вызвать этот интерфейс, чтобы проверить информацию токена jwt.,Затем проанализируйте идентификационную информацию пользователя и снова сохраните ее вSecurityContextHolder
类изSecurityContext
тип переменнойcontext
серединаизAuthentication
переменнаяauthentication
середина,Проблема решена.