Spring boot + Vue + Shiro realize front and back end separation and permission control

Keywords: Shiro JSON Vue Session

Yunqi information:[ Click to see more industry information]
Here you can find the first-hand cloud information of different industries. What are you waiting for? Come on!

This paper summarizes the reconstruction of the project from the practice. The original project uses the Springboot+freemarker template. During the development process, the front-end logic is disgusting. The back-end Controller layer must also return the Freemarker template Model andview gradually has the idea of front and back end separation. Since it has not been contacted before, it mainly refers to some blog tutorials on the Internet, etc., and has preliminarily completed the front and back end separation, which is recorded here for reference.

1, Separation of front and back

The front-end is separated from the back-end to form a front-end project. The front-end only uses Json to interact with the back-end. The back-end does not return the page, but only returns the Json data. The front and back end are completely through the public API convention.

2, Backend Springboot

Spring boot doesn't go into details any more. The 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 requests are received using the @ RequestBody parameter.

3, Front end Vue + ElementUI + Vue router + Vuex + axios + webpack

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 we mainly talk about the problems encountered in the development project:

1. Cross domain
Because the front-end project in the development uses webpack to start a service, the front-end and the back-end are not under the same port, which inevitably involves cross domain:

XMLHttpRequest will comply with the same origin policy. That is to say, scripts can only access resources of the same protocol / host name / port. If you want to break this limitation, it is the so-called cross domain. At this time, you need to comply with the cross origin resource sharing mechanism.

There are two solutions to cross domains:

1. The server side is developed by itself, so an interceptor can be added 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 {
        //Cross domain allowed, not in 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", "*");

It is mainly to add "access control allow origin: *" in 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");
        }

Due to the integration of shiro in the front-end and back-end separation, we need to customize a 'Authorization' field in the headers. At this time, ordinary requests such as GET and POST will become preflight request, that is, an OPTIONS request will be sent in advance before the GET and POST requests, which will be discussed later. Recommend a blog about preflight request.

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

2. The server is not developed by itself. You can add proxyTable in the front end.

However, this can only be used during development. For subsequent deployment, the front-end project can be put on the back-end as a static resource, so there is no cross domain (because of the needs of the project, I do this now. According to the introduction of online blog, nginx can be used, and how to do it can be searched on the Internet).

Encountered a lot of people on the Internet said that no matter how the proxyTable is modified, it has no effect.

1. (very important) make sure that the address of the proxyTable configuration can be accessed, because if it cannot be accessed, you will still see the prompt 404 when debugging the browser F12.

And notice that the domain name that js prompts for error in F12 is the domain name written by js, not the domain name after proxy. (l the landlord has encountered this problem. The back-end address lacks query parameters, and the proxy is set as the back-end address. However, the error seen in F12 is still the local domain name, not the domain name after the proxy.)

2. To execute npm run dev again manually

4, Integration of shiro in front and back end separation project

Refer to:
blog.csdn.net/u013615903/article/details/78781166

Here are the problems encountered in the actual development integration process:

1. OPTIONS request without 'Authorization' request header field:

In front end and back end separation projects, because of cross domain, complex requests will be generated, that is, preflight requests will be sent, which will cause an OPTIONS request to be sent before GET / POST and other requests, but the OPTIONS request does not take shiro's' Authorization 'field (shiro's Session), that is, the OPTIONS request cannot pass shiro verification, and will return the unauthenticated information.
Solution: add a filter to shiro to filter OPTIONS requests

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", "Not logged in");
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }
}

Paste my config file:

@Configuration
public class ShiroConfig {

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

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

    /**
     * cookie Object;
     * rememberMeCookie()The method is to set the cookie generation template, such as Cookie name, cookie validity time, etc.
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie(){
        //System.out.println("ShiroConfiguration.rememberMeCookie()");
        //This parameter is the name of the cookie, corresponding to the name of the front-end checkbox = rememberme
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<! -- remember that my cookie takes effect in 30 days, in seconds; -- >
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * cookie Management object;
     * rememberMeManager()The method is to generate the rememberMe manager, and set the rememberMe manager to the security manager
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        //System.out.println("ShiroConfiguration.rememberMeManager()");
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //The key encrypted by rememberMe cookie is recommended to be different for each item. The default AES algorithm key length is 128 256 512 bits
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        return cookieRememberMeManager;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
        sm.setRealm(realm());
        sm.setCacheManager(cacheManager());
        //Inject remember me manager
        sm.setRememberMeManager(rememberMeManager());
        //Inject custom session manager
        sm.setSessionManager(sessionManager());
        return sm;
    }

    //Custom session manager
    @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<>();
        //Configure links that will not be blocked, and judge in order
        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:All URLs must be authenticated for access. anon: all URLs can be accessed anonymously
        filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //Custom filter
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
        shiroFilter.setFilters(filterMap);

        return shiroFilter;
    }

    /**
     * Shiro Life cycle processor * @ return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * Turn on Shiro's comments (e.g@ RequiresRoles,@RequiresPermissions )The following two beans (defaultadvisorautoproxycreator (optional) and AuthorizationAttributeSourceAdvisor) need to be configured to implement this function * @ 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 session expiration time

shiro session The default expiration time is 30 min,We're customizing sessionManager Set the expiration time to another value in the constructor of
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);//If Authorization exists in the request header, its value is 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 {
            //Otherwise, the sessionId is taken from the cookie according to the default rule
            return super.getSessionId(request, response);
        }
    }
}

5, Deploy project

There are two main methods for front-end project deployment:
1. Package the front-end project (npm run build) into a static resource file, put it into the back-end, and package it together. The back-end writes a Controller to return to the front-end interface (I used Vue to develop a single page application), but in fact, the front and back ends are coupled together. However, at least the front and back ends are separated for development, so that the purpose of development is achieved and the requirements are preliminarily met. For the needs of the project, I do this and avoid cross domain problems.

@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
    public String index() {
        return "/index";
    }
  1. Start another service (tomcat, nginx, nodejs) for the front-end project, which has cross domain problems.
    Tell me about my problems:

1. nginx reverse proxy causes shiro 302 to unauth's controller when accessing an unauthorized page. The access address is https and the redirection address is http, which makes it impossible to access.
Without shiro shiroFilter.setLoginUrl("/unauth");
When the page has no permission to access, we directly return error information in the filter instead of using shiro's own jump. Look at the onAccessDenied function in the filter

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", "Not logged in");
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }
}

So many records first, there is something wrong, welcome to point out!

[yunqi online class] product technology experts share every day!
Course address: https://yqh.aliyun.com/zhibo

Join the community immediately, face to face with experts, and keep abreast of the latest news of the course!
[yunqi online classroom community] https://c.tb.cn/F3.Z8gvnK

Original release time: June 17, 2020
Author:_ Yufan
This article comes from:“ Internet architect WeChat official account ”, you can pay attention to“ Internet architect"

Posted by poirot on Thu, 18 Jun 2020 00:04:07 -0700