Springboot + Vue + shiro achieve separation of the front and rear ends, access control

This paper summarizes the practice reconstructed from the project. Original project uses Springboot + freemarker template development process felt too sick to write front-end logic, backend Controller layer must also return Freemarker template ModelAndView, before and after the end of the idea of ​​separation has gradually because before, no contact, the main reference Some blog or online tutorial, initially completed the separation of the front and rear end, in this record for future reference.

First, the idea of ​​separating the front and rear end

Peeling the front end from the rear end, forming a front-end engineering, using only the front end and a rear end Json to interact, the rear end does not return a page only returns Json data. Complete agreement between the front and rear ends by the public API.

Second, the back-end Springboot

Springboot will not repeat, Controller layer returns Json data.

@RequestMapping(value = "/add", method = RequestMethod.POST)
    @ResponseBody
    public JSONResult addClient(@RequestBody String param) {
        JSONObject jsonObject = JSON.parseObject(param);
        String task = jsonObject.getString("task");
        List<Object> list = jsonObject.getJSONArray("attributes");
        List<String> attrList = new LinkedList(list);
        Client client = JSON.parseObject(jsonObject.getJSONObject("client").toJSONString(),new TypeReference<Client>(){});
        clientService.addClient(client, task, attrList);
        return JSONResult.ok();
    }

Post @RequestBody request using the received parameter.

Third, the front end Vue + ElementUI + Vue router + Vuex + axios + webpack

The main reference:

https://cn.vuejs.org/v2/guide/
https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md
https://github.com/PanJiaChen/vue-element-admin

Here mainly talk about the problems encountered in the development of the project:

1. Cross-Domain

Due to the development of front-end engineering webpack start using a service, so the front and rear end is not in a port, necessarily involves cross-domain:

XMLHttpRequest会遵守同源策略(same-origin policy). 也即脚本只能访问相同协议/相同主机名/相同端口的资源, 如果要突破这个限制, 那就是所谓的跨域, 此时需要遵守CORS(Cross-Origin Resource Sharing)机制。

To solve cross-domain in two ways:

1, server-side development of their own, so you can add an interceptor at the back end

@Component
public class CommonIntercepter implements HandlerInterceptor {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        //允许跨域,不能放在postHandle内
        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;
    }
}
response.setHeader(“Access-Control-Allow-Origin”, “*”);

The main is to increase the "Access-Control-Allow-Origin: *" in the Response Header

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");
        }

Since we shiro integrated front rear separation, it is necessary to customize a 'Authorization' field in the headers, in which case ordinary GET, POST request and the like will become preflighted request, i.e. is pre-made before a GET, POST request OPTIONS request, the back say. Recommend a blog presentation preflighted request.

https://blog.csdn.net/cc1314_/article/details/78272329

2, server end not own development, you can add proxyTable at the front.

But this can only be used in the development, deployment follow-up, you can put resources into the front end of the project as a static back-end, so there is no cross-domain (due to the project need, I am now doing, according to online blog reports, you can use nginx, specifically how to do it can be found on the internet).

I met online a lot of people say, proxyTable modify anyway, no effect of the phenomenon.

1 (very important) to ensure that the address proxyTable configured to access, because if you can not access the browser when debugging see F12 will still be prompted 404.

并且注意,在F12看到的js提示错误的域名,是js写的那个域名,并不是代理后的域名。(l楼主就遇到这个问题,后端地址缺少了查询参数,代理设置为后端地址,然而F12看到的错误依然还是本地的域名,并不是代理后的域名)
2、就是要手动再执行一次npm run dev

Fourth, the integrated front and rear ends of the separate items shiro

You can refer to:

blog.csdn.net/u013615903/article/details/78781166
said here about the practical problems encountered in the development of the integration process:

1, OPTIONS request without 'Authorization' field in the request header:

Separate front and rear ends project, since the cross-domain, leads to a complex request, i.e. sent preflighted request, this will result in prior GET / POST request and the like will first send an OPTIONS request, but not with shiro OPTIONS request of 'Authorization' field (shiro of the Session), i.e. shiro OPTIONS request can not be authenticated, the authentication information is not returned.

Solution: shiro increased to a filter, filtering the OPTIONS request

public class CORSAuthenticationFilter extends FormAuthenticationFilter {

    private static final Logger logger = LoggerFactory.getLogger(CORSAuthenticationFilter.class);

    public CORSAuthenticationFilter() {
        super();
    }

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //Always return true if the request's method is OPTIONSif (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<String, Object> map= new HashMap<>();
        map.put("code", 702);
        map.put("msg", "未登录");
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }
}

Posted about my config file:

@Configuration
public class ShiroConfig {

    @Bean
    public Realm realm() {
        return new DDRealm();
    }

    @Bean
    public CacheManager cacheManager() {
        return new MemoryConstrainedCacheManager();
    }

    /**
     * cookie对象;
     * rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie(){
        //System.out.println("ShiroConfiguration.rememberMeCookie()");
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * cookie管理对象;
     * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        //System.out.println("ShiroConfiguration.rememberMeManager()");
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        return cookieRememberMeManager;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
        sm.setRealm(realm());
        sm.setCacheManager(cacheManager());
        //注入记住我管理器
        sm.setRememberMeManager(rememberMeManager());
        //注入自定义sessionManager
        sm.setSessionManager(sessionManager());
        return sm;
    }

    //自定义sessionManager
    @Bean
    public SessionManager sessionManager() {
        return new CustomSessionManager();
    }

    public CORSAuthenticationFilter corsAuthenticationFilter(){
        return new CORSAuthenticationFilter();
    }

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //SecurityUtils.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //配置不会被拦截的链接,顺序判断
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/static/js/**", "anon");
        filterChainDefinitionMap.put("/static/css/**", "anon");
        filterChainDefinitionMap.put("/static/fonts/**", "anon");
        filterChainDefinitionMap.put("/login/**", "anon");
        filterChainDefinitionMap.put("/corp/call_back/receive", "anon");
        //authc:所有url必须通过认证才能访问,anon:所有url都可以匿名访问
        filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //自定义过滤器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
        shiroFilter.setFilters(filterMap);

        return shiroFilter;
    }

    /**
     * Shiro生命周期处理器 * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能 * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

2, set time lapse session

shiro session default expiration time is 30min, we set in the constructor sessionManager custom of the time to failure for other values

public class CustomSessionManager extends DefaultWebSessionManager {

    private static final Logger logger = LoggerFactory.getLogger(CustomSessionManager.class);

    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);//如果请求头中有 Authorization 则其值为sessionId
        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 {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}

Fifth, the deployment project

The front end of the deployment project is divided into two methods:

1, the front end of the project package (npm run build) a static resource files into the backend, packaged together. Controller backend to write a front-end interface to return (I use Vue to develop a single-page application), but this is actually the front and rear end in turn coupled together, but at least do the purpose of separating the front and rear ends of development, facilitate the development has been reached, and a preliminary reached a requirement, due to the need of the project, I was doing this, and eliminating the need for cross-domain issues.

@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
    public String index() {
        return "/index";
    }

2. Another start a front-end engineering services (tomcat, nginx, nodejs), so there is the problem of cross-domain.

Talk about my problems:

1, nginx reverse proxy, resulting in no access privileges when the page, controller, visit the address shiro 302 to unauth is https, redirection address is http, led inaccessible.

Shiro not used in shiroFilter.setLoginUrl ( "/ unauth");

When the page is no access, we directly returns an error message in the filter, do not use shiro own jump. Filter function seen in onAccessDenied

---------------------------------------------------------------
注:我这有个学习基地,里面有很多学习资料,感兴趣的+Q群:895817687
---------------------------------------------------------------

public class CORSAuthenticationFilter extends FormAuthenticationFilter {

    private static final Logger logger = LoggerFactory.getLogger(CORSAuthenticationFilter.class);

    public CORSAuthenticationFilter() {
        super();
    }

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //Always return true if the request's method is OPTIONS
        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<String, Object> map= new HashMap<>();
        map.put("code", 702);
        map.put("msg", "未登录");
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }
}

First record so much, there is something wrong, please point out!

Guess you like

Origin blog.csdn.net/weixin_42625143/article/details/95186941