問題描述
最近發(fā)現(xiàn)自己開發(fā)的vue前后端分離項目因為使用了spring security 安全框架,即使在登錄認證成功之后再調(diào)用一些正常的接口總是會莫名奇妙地出現(xiàn)302重定向的問題,導致接口數(shù)據(jù)出不來。奇怪的是這個問題在本地開發(fā)環(huán)境并沒有,而是部署到了服務(wù)器之后才會有。
接口無法加載響應(yīng)數(shù)據(jù)
接口重定向標識Location顯示需要重新登錄認證,而且這個請求還是GET請求
問題原因定位
出現(xiàn)這個問題很顯然是當前用戶在Spring Security中丟失了認證信息,奇怪的是本地開發(fā)環(huán)境并不會出現(xiàn)這種問題,原因是我本地開發(fā)環(huán)境的前端用的是Vite啟動的前端服務(wù),而部署到服務(wù)器時卻是Nginx
起的前端服務(wù)。而筆者在Spring Security的配置類中注冊了一個用于Jwt token認證的過濾器JwtAuthenticationFilterBean
, 并注冊在UsernamePasswordAuthenticationFilter
之前。通過jwt token認證相當于spring security
需要對用戶的每次請求都先認證一次,如果用戶的認證信息沒有保存到SecurityContext
類中的authentication
中就會在調(diào)用非登錄接口獲取數(shù)據(jù)時出現(xiàn)這種重定向到登錄頁面的問題。
自定義的Jwt token認證類源碼如下:
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();
// 校驗token 是否過期
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
不進行攔截,直接放行。對于白名單中的請求部署到服務(wù)器后是不會有這種302重定向到登錄頁面的問題。因為這些白名單請求在Spring Security
中也進行了放行, 源碼如下。
SecurityConfig#configure(HttpSecurity)
方法源碼:
@Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean();
// http過濾器鏈中注冊jwt 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) -> { // httpServletResponse參數(shù)中返回用戶信息和jwt token給客戶端
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)); // 根據(jù)用戶信息生成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());
}
問題解決方案
有兩種方式解決這個部署到服務(wù)器后產(chǎn)生的302重定向問題
- 第一種就是在Spring Security的配置類的
configure(HttpSecurity)
方法中對出現(xiàn)302重定向的請求進行放行,向放行白名單請求一樣進行處理。不過這種方式解決的話相當于棄用了Spring Security
安全框架,任意用戶都能訪問后臺接口,應(yīng)用沒有安全可言,不推薦使用; - 第二種方式便是在
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();
// 校驗token 是否過期
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()+" 調(diào)用請求 "+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);
校驗修改效果
修改好源碼后重新打包部署到服務(wù)器(關(guān)于如何打包部署,網(wǎng)上已有很多詳細的指導文章,這里就不贅述了)
部署好應(yīng)用之后登錄之后系統(tǒng)會自動跳轉(zhuǎn)到首頁http://javahsf.club:3000/home
這時候就不會有之前的302重定向問題了,也可以看到頁面的數(shù)據(jù)成功加載出來了
通過F12調(diào)試模式查看網(wǎng)絡(luò)請求也可以看到?jīng)]有302重定向的問題了,數(shù)據(jù)也成功返回了
為了進一步驗證調(diào)用這個接口時需要重新認證用戶的登錄信息,我們通過在部署目錄執(zhí)行 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 調(diào)用請求 /bonus/openResult/page/data 需要重新獲得認證
由此驗證了302重定向的問題是接口之前是spring security
框架需要重新認證用戶登錄信息卻沒有拿到用戶的認證信息導致的,只需要調(diào)用這個接口驗證jwt token信息,然后解析出用戶身份信息后重新保存到SecurityContextHolder
類的SecurityContext
類型變量context
中的Authentication
變量authentication
中,問題就得到了解決。
相關(guān)閱讀
【1】Spring Security的項目中集成JWT Token令牌安全訪問后臺API
需要本文源碼的朋友可通過筆者發(fā)布在個人微信公眾號上的這篇在文末的獲取項目源碼的方式獲取文章來源:http://www.zghlxwxcb.cn/news/detail-648159.html
寫在最后
本文首發(fā)個人微信公眾號【阿福談Web編程】,歡迎喜歡我的文章的讀者朋友們加個關(guān)注,大家一起交流學習,謝謝。
文章來源地址http://www.zghlxwxcb.cn/news/detail-648159.html
到了這里,關(guān)于解決前后端分離Vue項目部署到服務(wù)器后出現(xiàn)的302重定向問題的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!