Implementing Front‑Back End Separation with Spring Boot, Vue, and Shiro: CORS Handling and Deployment
This article explains how to build a front‑back end separated application using Spring Boot for the backend and Vue.js for the frontend, covering JSON APIs, CORS interception, Shiro security integration, custom session management, and two deployment strategies with code examples.
In a front‑back end separated architecture, the frontend is isolated from the backend and communicates solely through JSON APIs; the backend no longer returns HTML pages, only data.
Backend – Spring Boot
The controller returns JSON data via a POST endpoint:
@RequestMapping(value = "/add", method = RequestMethod.POST)
@ResponseBody
public JSONResult addClient(@RequestBody String param) {
JSONObject jsonObject = JSON.parseObject(param);
String task = jsonObject.getString("task");
List
list = jsonObject.getJSONArray("attributes");
List
attrList = new LinkedList<>(list);
Client client = JSON.parseObject(jsonObject.getJSONObject("client").toJSONString(), new TypeReference
(){});
clientService.addClient(client, task, attrList);
return JSONResult.ok();
}The POST request receives parameters via @RequestBody .
Frontend – Vue + ElementUI + Vue‑Router + Vuex + Axios + Webpack
The frontend stack references the official Vue guide and popular admin templates. Common issues include cross‑origin requests because the webpack dev server runs on a different port.
CORS Handling
Two solutions are presented:
When the backend is self‑developed, add a Spring interceptor to set the required CORS headers:
@Component
public class CommonInterceptor implements HandlerInterceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin", "*");
if (request.getMethod().equals("OPTIONS")) {
response.addHeader("Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH");
response.addHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
}
return true;
}
}When the backend is not under your control, a proxyTable can be configured in the webpack dev server (only for development).
Shiro Integration in a Front‑Back End Separated Project
Because pre‑flight OPTIONS requests do not carry the Authorization header, they fail Shiro authentication. A custom filter extending FormAuthenticationFilter is added to allow OPTIONS requests and to return a JSON error for unauthenticated access:
public class CORSAuthenticationFilter extends FormAuthenticationFilter {
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (request instanceof HttpServletRequest) {
if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
}
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setStatus(HttpServletResponse.SC_OK);
res.setCharacterEncoding("UTF-8");
PrintWriter writer = res.getWriter();
Map
map = new HashMap<>();
map.put("code", 702);
map.put("msg", "未登录");
writer.write(JSON.toJSONString(map));
writer.close();
return false;
}
}The Shiro configuration registers this filter and defines the filter chain:
@Configuration
public class ShiroConfig {
@Bean
public Realm realm() { return new DDRealm(); }
@Bean
public CacheManager cacheManager() { return new MemoryConstrainedCacheManager(); }
@Bean
public SimpleCookie rememberMeCookie() {
SimpleCookie cookie = new SimpleCookie("rememberMe");
cookie.setMaxAge(259200);
return cookie;
}
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager manager = new CookieRememberMeManager();
manager.setCookie(rememberMeCookie());
manager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return manager;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
sm.setRealm(realm());
sm.setCacheManager(cacheManager());
sm.setRememberMeManager(rememberMeManager());
sm.setSessionManager(sessionManager());
return sm;
}
@Bean
public SessionManager sessionManager() { return new CustomSessionManager(); }
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map
filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login/**", "anon");
filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
Map
filterMap = new LinkedHashMap<>();
filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
shiroFilter.setFilters(filterMap);
return shiroFilter;
}
@Bean
public CORSAuthenticationFilter corsAuthenticationFilter() { return new CORSAuthenticationFilter(); }
// additional beans for lifecycle, advisors, etc.
}A custom CustomSessionManager overrides getSessionId to read the session ID from the Authorization header and extends the default timeout:
public class CustomSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public CustomSessionManager() {
super();
setGlobalSessionTimeout(DEFAULT_GLOBAL_SESSION_TIMEOUT * 48);
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if (!StringUtils.isEmpty(sessionId)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
} else {
return super.getSessionId(request, response);
}
}
}Deployment
Two deployment approaches are described:
Package the Vue project into static files ( npm run build ) and serve them from a Spring Boot controller, eliminating cross‑origin issues.
Run the frontend as a separate service (Tomcat, Nginx, Node.js), which re‑introduces CORS considerations.
Example controller for serving the index page:
@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
public String index() {
return "/index";
}Additional deployment challenges such as Nginx reverse‑proxy redirects from HTTPS to HTTP and handling unauthenticated requests without Shiro’s default redirect are also discussed.
The article concludes with a reminder that the presented solutions may need adjustments for specific projects.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.