SpringBoot+vue+shiro前后端分离开发整合部署详解

一、前言

1.综合概述

  1. 前后端分离项目越来越成为主流,目前应用较多的是前端为vue+elementUI 后端用SpringBoot。
  2. 一般的管理系统都要有登录和权限管理的功能,这里选用Apache的Shiro做为项目的安全框架。
  3. 项目一般会采取集群部署的方式,这是就涉及到session共享的问题。这里采用的是shiro结合redis来做session共享。
  4. 前后端分离的项目部署方式分为两种,一种是前端项目和后端项目分离部署,另一种方式是将前端编译好的文件放到后端项目中整合部署,这里选择后者。

2.开发环境

后台:

  • jdk: 1.8.0_45
  • lombok
  • SpringBoot: 2.1.3.RELEASE
  • mybatis: 1.3.2
  • pagehelper: 1.2.3
  • swagger: 2.5.0
  • druid: 1.1.10
  • shiro 1.4.0

前端:

  • vue: 2.6.10
  • element-ui: 2.8.2

项目:

  • 后台:https://gitee.com/OneBytee/hope
  • 前端:https://gitee.com/OneBytee/hope-admin

二、代码实现

1、前端项目

前端选用的lin-xin大神的前端模板(github地址)。该方案作为一套多功能的后台框架模板,适用于绝大部分的后台管理系统(Web Management System)开发。基于 vue.js,使用 vue-cli3 脚手架,引用 Element UI 组件库,方便开发快速简洁好看的组件。分离颜色样式,支持手动切换主题色,而且很方便使用自定义主题色。

1.1 前端登录页代码
<template>
    <div class="login-wrap">
        <div class="ms-login">
            <div class="ms-title">后台管理系统</div>
            <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="0px" class="ms-content">
                <el-form-item prop="userId">
                    <el-input v-model="ruleForm.userId" placeholder="userId">
                        <el-button slot="prepend" icon="el-icon-lx-people"></el-button>
                    </el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input type="password" placeholder="password" v-model="ruleForm.password" @keyup.enter.native="submitForm('ruleForm')">
                        <el-button slot="prepend" icon="el-icon-lx-lock"></el-button>
                    </el-input>
                </el-form-item>
                <div class="login-btn">
                    <el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
                </div>
                <p class="login-tips">Tips : 用户名和密码随便填。</p>
            </el-form>
        </div>
    </div>
</template>

<script>
    export default {
        data: function(){
            return {
                ruleForm: {
                    userId: 'admin',
                    password: '1234'
                },
                rules: {
                    userId: [
                        { required: true, message: '请输入用户名', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入密码', trigger: 'blur' }
                    ]
                }
            }
        },
        methods: {
            submitForm(formName) {
                this.$refs[formName].validate((valid) => {
                    if (valid) {
                        this.$axios.post('/sys/login', this.ruleForm ).then((res) => {
                            if(res.data.success){
                                localStorage.setItem('ms_userId',this.ruleForm.userId);
                                this.$router.push('/dashboard');
                            } else {
                                this.$message.error(res.data.msg);
                            }
                        })
                        
                    } else {
                        console.log('error submit!!');
                        return false;
                    }
                });
            }
        }
    }
</script>

<style scoped>
    .login-wrap{
        position: relative;
        width:100%;
        height:100%;
        background-image: url(../../assets/img/login-bg.jpg);
        background-size: 100%;
    }
    .ms-title{
        width:100%;
        line-height: 50px;
        text-align: center;
        font-size:20px;
        color: #fff;
        border-bottom: 1px solid #ddd;
    }
    .ms-login{
        position: absolute;
        left:50%;
        top:50%;
        width:350px;
        margin:-190px 0 0 -175px;
        border-radius: 5px;
        background: rgba(255,255,255, 0.3);
        overflow: hidden;
    }
    .ms-content{
        padding: 30px 30px;
    }
    .login-btn{
        text-align: center;
    }
    .login-btn button{
        width:100%;
        height:36px;
        margin-bottom: 10px;
    }
    .login-tips{
        font-size:12px;
        line-height:30px;
        color:#fff;
    }
</style>

1.2 前端路由代码
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export default new Router({
    mode: 'hash',
    base: __dirname,
    routes: [
        {
            path: '/',
            redirect: '/login'
        },
        {
            path: '/',
            component: resolve => require(['../components/common/Home.vue'], resolve),
            meta: { title: '自述文件' },
            children:[
                {
                    path: '/dashboard',
                    component: resolve => require(['../components/page/Dashboard.vue'], resolve),
                    meta: { title: '系统首页' }
                },
                {
                    path: '/icon',
                    component: resolve => require(['../components/page/Icon.vue'], resolve),
                    meta: { title: '自定义图标' }
                },
                {
                    path: '/table',
                    component: resolve => require(['../components/page/BaseTable.vue'], resolve),
                    meta: { title: '基础表格' }
                },
                {
                    path: '/user',
                    component: resolve => require(['../components/page/User.vue'], resolve),
                    meta: { title: '用户管理' }
                },
                {
                    path: '/tabs',
                    component: resolve => require(['../components/page/Tabs.vue'], resolve),
                    meta: { title: 'tab选项卡' }
                },
                {
                    path: '/form',
                    component: resolve => require(['../components/page/BaseForm.vue'], resolve),
                    meta: { title: '基本表单' }
                },
                {
                    // 富文本编辑器组件
                    path: '/editor',
                    component: resolve => require(['../components/page/VueEditor.vue'], resolve),
                    meta: { title: '富文本编辑器' }
                },
                {
                    // markdown组件
                    path: '/markdown',
                    component: resolve => require(['../components/page/Markdown.vue'], resolve),
                    meta: { title: 'markdown编辑器' }    
                },
                {
                    // 图片上传组件
                    path: '/upload',
                    component: resolve => require(['../components/page/Upload.vue'], resolve),
                    meta: { title: '文件上传' }   
                },
                {
                    // vue-schart组件
                    path: '/charts',
                    component: resolve => require(['../components/page/BaseCharts.vue'], resolve),
                    meta: { title: 'schart图表' }
                },
                {
                    // 拖拽列表组件
                    path: '/drag',
                    component: resolve => require(['../components/page/DragList.vue'], resolve),
                    meta: { title: '拖拽列表' }
                },
                {
                    // 拖拽Dialog组件
                    path: '/dialog',
                    component: resolve => require(['../components/page/DragDialog.vue'], resolve),
                    meta: { title: '拖拽弹框' }
                },
                {
                    // 国际化组件
                    path: '/i18n',
                    component: resolve => require(['../components/page/I18n.vue'], resolve),
                    meta: { title: '国际化' }
                },
                {
                    // 权限页面
                    path: '/permission',
                    component: resolve => require(['../components/page/Permission.vue'], resolve),
                    meta: { title: '权限测试', permission: true }
                },
                {
                    path: '/404',
                    component: resolve => require(['../components/page/404.vue'], resolve),
                    meta: { title: '404' }
                },
                {
                    path: '/403',
                    component: resolve => require(['../components/page/403.vue'], resolve),
                    meta: { title: '403' }
                }
            ]
        },
        {
            path: '/login',
            component: resolve => require(['../components/page/Login.vue'], resolve)
        },
        {
            path: '*',
            redirect: '/404'
        }
    ]
})
1.3 理由钩子函数实现前端权限管理
axios.interceptors.response.use(res => {
    if(!res.data.success && res.data.code == 1000001){
        router.replace({
            path: 'login'
        })
        return Promise.reject(res);
    } else {
        // 对响应数据做些什么
        return res
    }
}, err => {
    // 对响应错误做些什么
    console.log('err', err.response) // 修改后
    return Promise.resolve(errsresponse) // 可在组件内获取到服务器返回信息
})

//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
    debugger
    const userId = localStorage.getItem('ms_userId');
    if (!userId && to.path !== '/login') {
        next('/login');
    } else if(to.path==="/login"){
        next();
    }else if (to.meta.permission) {
        // 如果是管理员权限则可进入,这里只是简单的模拟管理员权限而已
        userId === 'admin' ? next() : next('/403');
    } else {
        let userMenu = localStorage.getItem('user_menu');
        if(!userMenu){
            var params ={
                userId : userId
            }
            axios.post('/sys/selectUserPermission', params ).then((res) => {
                console.log(res.data)
                if(res.data.success){
                    console.log(res.data.data)
                    userMenu = res.data.data;
                    localStorage.setItem('user_menu',res.data.data);
                    // 简单的判断IE10及以下不进入富文本编辑器,该组件不兼容
                    if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') {
                        Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', {
                            confirmButtonText: '确定'
                        });
                    } else {
                        if(to.path !== '/403' && to.path !== '/login'&& userMenu){
                            let result = userMenu.indexOf(to.path);
                            if(result>=0){
                                next();
                            } else{
                                next('/403'); 
                            }
                        } else {
                            next();
                        }
                    }
                } else {
                    Vue.prototype.$alert(res.data.msg, '错误提示', {
                        confirmButtonText: '确定'
                    });
                }
            })
        } else {
            // 简单的判断IE10及以下不进入富文本编辑器,该组件不兼容
            if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') {
                Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', {
                    confirmButtonText: '确定'
                });
            } else {
                if(to.path !== '/403' && to.path !== '/login'&& userMenu){
                    let result = userMenu.indexOf(to.path);
                    if(result>=0){
                        next();
                    } else{
                        next('/403'); 
                    }
                } else {
                    next();
                }
            }
        }
    }   
})

2.后端项目

后端是自己搭建的springboot项目,数据库用的是mysql。数据库连接池用的druid,mybatis持久层框架,pagehelper分页插件。

2.1 maven依赖
    <properties>
        <java.version>1.8</java.version>
        <mybatis.version>1.3.2</mybatis.version>
        <pagehelper.version>1.2.3</pagehelper.version>
        <shiro.version>1.4.0</shiro.version>
        <fastjson.version>1.2.31</fastjson.version>
        <druid.version>1.1.10</druid.version>
        <swagger.version>2.5.0</swagger.version>
        <swagger.ui.version>2.5.0</swagger.ui.version>
        <commons.lang3.version>3.4</commons.lang3.version>
        <commons.beanutils.version>1.9.3</commons.beanutils.version>
        <shiro.redis.version>3.1.0</shiro.redis.version>
    </properties>

    <dependencies>
        <!--SpringBoot Web支持-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--数据库相关-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- alibaba的druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- alibaba的json格式化对象 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!-- pagehelper分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>${pagehelper.version}</version>
        </dependency>

        <!-- swagger2自动生成API文档 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger.ui.version}</version>
        </dependency>

        <!-- 工具类-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons.lang3.version}</version>
        </dependency>

        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>${commons.beanutils.version}</version>
        </dependency>

        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>

        <!-- shiro整合redis做session共享 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>${shiro.redis.version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>shiro-core</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

    </dependencies>
2.2 后端整合shiro

Apache的Shiro是一个非常易用的安全框架,提供了包括认证、授权、加密、会话管理等功能,与Spring Security一样属基于权限的安全框架,但是与Spring Security 相比,Shiro使用了比较简单易懂易于使用的授权方式。Shiro属于轻量级框架,相对于Spring Security简单很多,并没有security那么复杂。

  • HopeShiroRealm 继承了AuthorizingRealm,这个类的作用是两处获取信息,一处是Subject即用户传过来的信息;一处是通过我们提供给shiro的SysUserService接口从数据库获取权限信息和角色信息。拿这两个信息之后AuthorizingRealm会自动进行比较,判断用户名密码,用户权限等等。拿用户凭证信息的是doGetAuthenticationInfo接口,拿角色权限信息的是doGetAuthorizationInfo接口。两个重要参数,AuthenticationToken是我们可以自己实现的用户凭证/密钥信息,PrincipalCollection是用户凭证信息集合。配置完成之后Subject.login(token)的时候就会调用doGetAuthenticationInfo方法;涉及到Subject.hasRole或者Subject.hasPermission的时候就会调用doGetAuthorizationInfo方法。
2.2.1 ShiroConfig

在配置shiro的拦截信息的时候,要忽略拦截"/" ,由于前端路由配置了根路径的路由,忽略拦截,会访问前端路由。

package com.lh.config.shiro;

import com.lh.common.HopeExceptionHandler;
import com.lh.config.properties.RedisInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

@Slf4j
@Configuration
public class ShiroConfig {

    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        log.info("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //注意过滤器配置顺序 不能颠倒
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
        filterChainDefinitionMap.put("/logout", "logout");
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");
        // 首页
        filterChainDefinitionMap.put("/", "anon");
        // 登录
        filterChainDefinitionMap.put("/sys/login", "anon");
        filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
        //配置 shiro 默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl("/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //自定义过滤器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 将自己的用户认证验证方式加入容器
     */
    @Bean
    public HopeShiroRealm hopeShiroRealm() {
        return new HopeShiroRealm();
    }

    /**
     * 权限管理,配置主要是Realm的管理认证 和session管理
     *
     * @param redisSessionManager redis session共享
     * @param redisCacheManager   redis 缓存
     */
    @Bean
    public SecurityManager securityManager(
            @Qualifier("redisSessionManager") DefaultWebSessionManager redisSessionManager,
            @Qualifier("redisCacheManager") RedisCacheManager redisCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(hopeShiroRealm());
        securityManager.setSessionManager(redisSessionManager);
        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisManager redisManager(RedisInfo redisConfig) {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisConfig.getHost());
        redisManager.setPort(redisConfig.getPort());
        redisManager.setTimeout(redisConfig.getTimeout());
        return redisManager;
    }

    /**
     * redisSession相关配置
     * 自定义session持久化
     * 为啥session也要持久化?
     *             重启应用,用户无感知,可以继续以原先的状态继续访问
     *       注意点:
     *             DO对象需要实现序列化接口 Serializable
     *             logout接口和以前一样调用,请求logout后会删除redis里面的对应的key,即删除对应的token
     */
    @Bean
    public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager);
        redisSessionDAO.setKeyPrefix("HOPE_SHIRO_SESSION:");// key
        redisSessionDAO.setExpire(1000); // 过期时间
        return redisSessionDAO;
    }

    /**
     * session的管理 用redis实现session共享
     */
    @Bean
    public DefaultWebSessionManager redisSessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }

    /**
     * 配置具体cache实现类RedisCacheManager
     * 为什么要使用缓存:
     * 缓存组件位于SecurityManager中,在HopeShiroRealm数据域中,由于授权方法中每次都要查询数据库,性能受影响,因此将数据缓存起来,提高查询效率
     * 除了使用Redis缓存,还能使用shiro-ehcache
     */
    @Bean
    public RedisCacheManager redisCacheManager(RedisManager redisManager) {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager);
        return redisCacheManager;
    }


    /**
     * 加入注解的使用,不加入这个注解不生效
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 注册全局异常处理
     */
    @Bean(name = "exceptionHandler")
    public HandlerExceptionResolver handlerExceptionResolver() {
        return new HopeExceptionHandler();
    }

}
2.2.2 HopeShiroRealm
package com.lh.config.shiro;

import com.lh.entity.sys.SysPermission;
import com.lh.entity.sys.SysRole;
import com.lh.entity.sys.SysUser;
import com.lh.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import javax.annotation.Resource;

/**
 * 实现AuthorizingRealm接口用户认证
 */
@Slf4j
public class HopeShiroRealm extends AuthorizingRealm {

    @Resource
    private SysUserService sysUserService;

    /**
     * 角色权限和对应权限添加
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("权限配置-->HopeShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUser sysUser = (SysUser) principals.getPrimaryPrincipal();
        for (SysRole role : sysUser.getSysRoles()) {
            // 添加角色
            authorizationInfo.addRole(role.getRoleId());
            for (SysPermission p : role.getSysPermissions()) {
                // 添加权限
                authorizationInfo.addStringPermission(p.getPermission());
            }
        }
        return authorizationInfo;
    }

    /**
     * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("HopeShiroRealm.doGetAuthenticationInfo()");
        //获取用户的输入的账号.
        String userId = (String) token.getPrincipal();
        log.info("用户密码 ={}", token.getCredentials());
        //通过username从数据库中查找 User对象,如果找到,没找到.
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        SysUser sysUser = sysUserService.selectSysUserByUserId(userId);
        log.info("----->>sysUser=" + sysUser);
        if (sysUser == null) {
            return null;
        }
        if (sysUser.getStatus() == 1) { //账户冻结
            throw new LockedAccountException();
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                sysUser, //用户名
                sysUser.getPassword(), //密码
                null,
                //ByteSource.Util.bytes(sysUser.getCredentialsSalt()),//salt=username+salt
                getName()  //realm name
        );
        return authenticationInfo;
    }
}

2.2.3 CORSAuthenticationFilter

前后端分离项目中,由于跨域,会导致复杂请求,即会发送preflighted request,这样会导致在GET/POST等请求之前会先发一个OPTIONS请求,但OPTIONS请求并不带cookie,即OPTIONS请求不能通过shiro验证,会返回未认证的信息。

package com.lh.config.shiro;

import com.alibaba.fastjson.JSON;
import com.lh.entity.common.Result;
import com.lh.common.enums.TransactionCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Slf4j
public class CORSAuthenticationFilter extends FormAuthenticationFilter {

    @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();
        writer.write(JSON.toJSONString(Result.error(TransactionCode.NO_LOGION.getCode(), TransactionCode.NO_LOGION.getMsg())));
        writer.close();
        return false;
    }
}

2.3 后端登录接口
package com.lh.controller;

import com.lh.entity.sys.SysUser;
import com.lh.entity.common.Result;
import com.lh.common.enums.TransactionCode;
import com.lh.entity.sys.common.UserMenu;
import com.lh.service.SysService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

@Slf4j
@RestController
public class SysController {

    @Resource
    private SysService sysService;

    /**
     * 登录方法
     *
     * @param sysUser
     * @return
     */
    @RequestMapping(value = "/sys/login", method = RequestMethod.POST)
    public Result ajaxLogin(@RequestBody SysUser sysUser) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUserId(), sysUser.getPassword());
        try {
            subject.login(token);
            return Result.ok(subject.getSession().getId());
        } catch (IncorrectCredentialsException e) {
            return Result.error("密码错误!");
        } catch (LockedAccountException e) {
            return Result.error("登录失败,该用户已被冻结!");
        } catch (AuthenticationException e) {
            return Result.error("该用户不存在!");
        } catch (Exception e) {
            log.info("用户登录时发生异常!e={}", e);
            return Result.error();
        }
    }

    /**
     * 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面
     *
     * @return
     */
    @RequestMapping(value = "/unauth")
    public Result<String> unauth() {
        return Result.error(TransactionCode.NO_LOGION.getCode(), TransactionCode.NO_LOGION.getMsg());
    }

    /**
     * 根据用户编号查询用户所有菜单
     *
     * @param sysUser
     * @return
     */
    @RequestMapping(value = "/sys/selectUserMenu", method = RequestMethod.POST)
    public Result<List<UserMenu>> selectUserMenu(@RequestBody SysUser sysUser) {
        // 根据用户编号查询用户所有可见菜单
        if (StringUtils.isNotBlank(sysUser.getUserId())) {
            return sysService.selectUserMenu(sysUser.getUserId());
        } else {
            return Result.error("未获取到当前登录人信息!");
        }
    }

    @RequestMapping(value = "/sys/selectUserPermission", method = RequestMethod.POST)
    public Result<List<String>> selectUserPermission(@RequestBody SysUser sysUser) {
        if (StringUtils.isNotBlank(sysUser.getUserId())) {
            return sysService.selectUserPermission(sysUser.getUserId());
        } else {
            return Result.error("未获取到当前登录人信息!");
        }
    }

}

三、整合部署

1.前端编译

编译前端项目,编译好的文件如下:
在这里插入图片描述

2.整合

将构建好的dist下static文件夹拷贝到springboot的resource的static下,index.html也拷贝到springboot的resource的static下。
在这里插入图片描述

2.访问
  • 浏览器输入:http://localhost:8081/
    在这里插入图片描述
  • 输入用户名密码登录
    在这里插入图片描述
    完成~
  • 9
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值