[SpringBoot+Security+vue]动态管理权限脚手架-前后端分离

1.后端

1.1环境准备

使用Spring Initializr创建一个Spring Boot项目

[Web]:Spring Web

[Security]:Spring Security

[SQL]:MyBatis Framework、MySQL Driver、Durid数据连接池

后面如果有什么需要再添加


		
		
			org.springframework.boot
			spring-boot-starter-security
		
		
		
			org.springframework.boot
			spring-boot-starter-web
		
		
		
			org.mybatis.spring.boot
			mybatis-spring-boot-starter
			2.2.0
		
		
		
			mysql
			mysql-connector-java
			runtime
		
        
		
			com.alibaba
			druid-spring-boot-starter
			1.1.10
		
		
		
			org.springframework.boot
			spring-boot-starter-test
			test
		
		
		
			org.springframework.security
			spring-security-test
			test
		
	

连接数据库: 

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql:///demo1?userUnicode=true&characterEncoding=UTF-8
server.port=8080
logging.level.com.bitk=debug;

1.2 数据库设计

[SpringBoot+Security+vue]动态管理权限脚手架-前后端分离_第1张图片

user表:存储了用户的个人基础信息、账号和密码;

user_role:维护了用户所具有的角色权限

role:存储了系统内所设置的角色权限

menu_role:维护了角色权限所能访问的菜单项(二级菜单)

menu:存储了系统内部所具有的菜单项,路径匹配规则、vue容器名称等

1.3 创建Java代码

1.3.1准备好对应的数据库表的实体类。

注意:其中的User.class要implements UserDetails接口。

1.3.2创建Service

创建UserService实现UserDetailsServicec接口并重写loadUserByUsername方法,去数据库查询用户账号信息和所具有的角色权限。

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;
    /**
     * 查询用户账号&密码&具有的角色
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息[账号、密码、个人信息等]
        User user = userMapper.loadUserByUsername(username);
        if (user==null)
            throw new UsernameNotFoundException("用户不存在!QAQ");
        //查询用户所具有的角色-并赋值
        user.setRoles(userMapper.getUserRolesById(user.getId()));
        return user;
    }
}

查询用户所能访问的菜单项 

@Service
public class MenuService {

    @Autowired
    MenuMapper menuMapper;

    //查询查询所有用户可以访问的菜单和权限信息
    public List getAllMenusWithRole() {
        return menuMapper.getAllMenusWithRole();
    }
}

 1.3.3准备好相关的Mapper.java/.xml

1.4配置Security

1.4.1 自定义CustomFilterInvocationSecurityMetadataSource

 自定义一个CustomFilterInvocationSecurityMetadataSource实现FilterInvocationSecurityMetadataSource接口.

该类的主要功能就是分析出访问当前URL需要哪些权限


/**
 * 根据用户的请求地址,分析出需要的角色
 */
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    MenuService menuService;

    //URL路径匹配工具--spring security自带
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 1.根据用户的请求地址,分析出来他所需要具有的权限
     * @param object 因为是基于过滤器,所有object是FilterInvocation类型的。
     * @return 用户访问的URL所需的权限[Role]
     * @throws IllegalArgumentException
     */
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        //拿到当前请求的地址
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        //拿到角色和菜单项的(1:m)查询结果
        List menus = menuService.getAllMenusWithRole();
        //进行url路径匹配
        for (Menu menu : menus) {
            //根据访问路径找出所需要的权限
            //用户访问的url如果跟当前menu路径规则匹配成功了,[那就是要访问这个路径]
            //然后我们查查访问这个路径所需要的role
            // 拿到访问该路径所需要的ROLE
            List roles = menu.getRoles();
            if(antPathMatcher.match(menu.getUrl(),requestUrl)){
                //路径匹配成功
                String[] needRoles = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    needRoles[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(needRoles);
            }
        }
        //没匹配上的路径,怎么处理?==>登录后访问吧
        //这里的参数只是一个标记,会在下一步的流程中处理
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    /**
     * 返回所有定义好的权限资源
     * Spring Security在启动时会校验相关配置是否正确,如果不需要校验,直接返回null
    **/
    @Override
    public Collection getAllConfigAttributes() {
        return null;
    }

    /**
     * 是否支持校验
     */
    @Override
    public boolean supports(Class clazz) {
        return false;
    }
}

1.4.2 自定义CustomDecisionManager

 自定义个决定管理者:该类主要是实现:根据当前登录的用户的现有权限&访问URL所需要的权限进行投票决定.

在这里我们和系统选用的默认投票器的处理方法保持一直:选用一票即过.就是用户只需要有访问该URL至少一个权限就可以通过.


@Component
public class CustomDecisionManager implements AccessDecisionManager {

    /**
     * 分析访问URL所需的角色,查看请求用户是否具备?
     * 如果不具备,就抛出AccessDeniedException异常,否则do nothing
     * @param authentication 用户所具有的角色
     * @param object 保护对象,在这里是FilterInvocatin对象.在他里面可以访问到请求的URL
     * @param configAttributes configAttributes.getAttributes()中保存了请求当前所需要的权限角色
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        for (ConfigAttribute configAttribute : configAttributes) {
            String needRole = configAttribute.getAttribute();
            //处理只要登录权限的用户请求.
            if("ROLE_LOGIN".equals(needRole)){
                if (authentication instanceof AnonymousAuthenticationToken)
                    throw new AccessDeniedException("尚未登录,请登录");
                return;
            }

            //如果有一项权限通过了,AccessDecisionManager就同意访问
            Collection authorities = authentication.getAuthorities(); //用户所具有的权限
            for (GrantedAuthority authority : authorities) {
                System.err.println(this.getClass().getName()+"---访问["+ ((FilterInvocation) object).getRequestUrl()+"需要的权限是["+needRole);
                System.err.println(this.getClass().getName()+"---当前用户所具有的权限是:"+authority.getAuthority().toString());
                if(authority.getAuthority().equals(needRole))//对比权限
                    return;
            }
        }
        throw new AccessDeniedException("对不起,您的权限不足.");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

1.4.3配置SecurityConfig


/**
 * 配置Spring Security相关
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Autowired
    private CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
    @Autowired
    private CustomDecisionManager customDecisionManager;

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();//不对密码加密
//        return new BCryptPasswordEncoder();
    }


    /**
     * 进行用户账号密码的验证:配置用户信息,账号密码.角色权限
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    /**
     * 认证设置(HttpSecurity认证用户请求URL认证)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
//                .anyRequest().authenticated()//任何的请求都需要认证
                .withObjectPostProcessor(new ObjectPostProcessor() {
                    @Override
                    public  O postProcess(O object) {
                        object.setAccessDecisionManager(customDecisionManager);
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return object;
                    }
                })

                .and().formLogin()//开启登录认证
                .loginProcessingUrl("/doLogin")//设置表单action="提交接口"
                /**
                 * 登录成功处理器:
                 *  代码逻辑:登录成功后,返回用户个人信息和成功状态码(JSON类型)。
                 */
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        PrintWriter out = response.getWriter();

                        User user = (User) authentication.getPrincipal();
                        RespBean ok = RespBean.ok("登录成功", user);
                        String json = new ObjectMapper().writeValueAsString(ok);
                        out.write(json);
                        out.flush();// flush()表示强制将缓冲区中的数据发送出去,不必等到缓冲区满
                        out.close();
                        System.err.println("SecurityConfig.class:登录成功");
                    }
                })
                /**
                 * 登录失败处理器:
                 * 代码逻辑:登录失败后,判断失败类型&返回给前端(JSON类型)。
                 */
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        PrintWriter out = response.getWriter();
                        RespBean fail = RespBean.error("登录失败");

                        if (e instanceof LockedException)
                            fail.setMsg("账户被锁定,请联系管理员");
                        else if (e instanceof CredentialsExpiredException)
                            fail.setMsg("密码已过期,请重新登录");
                        else if (e instanceof AccountExpiredException)
                            fail.setMsg("密码过期");
                        else if (e instanceof DisabledException)
                            fail.setMsg("账户被禁用");
                        else if (e instanceof BadCredentialsException)
                            fail.setMsg("用户名或者密码输入错误");
                        String s = new ObjectMapper().writeValueAsString(fail);
                        out.write(s);
                        out.flush();
                        out.close();
                    }
                })
//                .loginPage("/login")这个加不加都一样,登录请求默认为/login
                .permitAll()//表单登录接口公开
                //开启注册接口,接口默认为"/logout"
                .and().logout().logoutSuccessHandler(new LogoutSuccessHandler() {
                    /**
                     * 退出登录成功处理器
                     */
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        PrintWriter out = response.getWriter();
                        out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功")));
                        out.flush();
                        out.close();
                    }
                }).permitAll()//开启注销接口&注销接口公开

                .and().csrf().disable();//
    }
}

在项目的config包下创建一个SecurityConfig,并继承[extends]WebSecurityConfigurerAdapter

tips:为什么要继承?

继承等于是我们自己创建的SecurityConfigWebSecurityConfigurerAdapter的基础之上去[扩展]、[修改(重写)]父类的功能。

相当于站在巨人的肩膀上前进

1.5创建测试Controller

@RestController
@RequestMapping("/admin")
public class AdminController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello admin";
    }
}



@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello user";
    }

}

账号           密码

admin        admin  

user           user

分别用这两个用户登录,然后访问[localhost:8080/admin/hello]和[localhost:8080/user/hello]查看是否可以访问的到.

1.x menus的作用解释

1.user表是用户表,存放了用户的基本信息。

2.role是角色表,name字段表示角色的英文名称,按照SpringSecurity的规范,将以ROLE_开始,nameZh字段表示角色的中文名称。

3.menu表是一个资源表,该表涉及到的字段有点多,由于前端采用了Vue来做,因此当用户登录成功之后,系统将根据用户的角色动态加载需要的模块,所有模块的信息将保存在menu表中,menu表中的path、component、iconCls、requireAuth等字段都是Vue-Router中需要的字段,也就是说menu中的数据到时候会以json的形式返回给前端,再由vue动态更新router,menu中还有一个字段url,表示一个url pattern,即路径匹配规则,假设有一个路径匹配规则为/admin/**,那么当用户在客户端发起一个/admin/user的请求,将被/admin/**拦截到,系统再去查看这个规则对应的角色是哪些,然后再去查看该用户是否具备相应的角色,进而判断该请求是否合法。

一级菜单不分配角色、只给二级菜单分配。

如果给一级菜单也分配角色,那么对于该一级菜单下的二级菜单就不好设计了。如果一级菜单下面有多个二级菜单。并且他具有一级菜单的访问权,那么每个二级菜单的访问权限问题该怎么划分呢?为了设计上的简单:我们只给二级菜单分配访问控制角色,如果他具有所有的二级菜单的访问权限=》都显示。如果他只具有一个,=》显示一个。如果具有0个=》那就连这个以及菜单也不显示了

2.前端准备

前端采用vue来实现前端页面的开发

主要的主要有:vue,axios(和后端通信用的),vuex(存数据的)

打开cmd命令行,输入vue ui敲击[回车键],将需要的项目依赖都添加好

[SpringBoot+Security+vue]动态管理权限脚手架-前后端分离_第2张图片

 然后用编辑器打开项目

2.1 配置路由Router

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login'
import Home from '../views/Home'

Vue.use(VueRouter)


const routes = [{
    path: '/'
    ,name: 'Login'
    ,component: Login
    ,hidden:true
  },
  {
    path: '/home'
    ,name: '导航一'
    ,component: Home
    ,hidden:false   //此处的hidden只是个标记相当于bool flag,并不会把该路由隐藏
  },
]

const router = new VueRouter({
  // mode: 'history'
  base: process.env.BASE_URL
  ,routes
})

export default router

2.2配置store,即vuex

/**
 * 专门用来管理vue.js中的数据的
 */
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  //存放数据的
  state: {
    routes:[]
  },
  //操作state中的数据
  mutations: {
    initRoutes(state,data){
      state.routes = data;
    }
  },

  actions: {
  },
  modules: {
  }
})

2.3 封装axios的通信

// 封装axios通信

import axios from "axios";
import {Message} from "element-ui";

/**
 * success:后端服务器返回的数据都在里面
 *  success.status:HTTP的状态码
 *  success.data:后端返回的数据
 *      success.data.status:后端RespBean里面自定义的数据
 *          500表示后端业务错误
 * 
 * @function response拦截器=>对各种response进行处理
 */
axios.interceptors.response.use(success => {
    //  HTTP状态码存在    HTTP请求成功(访问到了接口)|后端处理结果状态码:业务处理失败
    if (success.status && success.status==200 && success.data.status==500){
        //打印后端处理的业务错误消息
        Message.error({message:success.data.msg})
        return; 
    }
    if(success.data.msg){
        Message.success({message:success.data.msg})
    }
    return success.data;
}, error => {
    console.log(error);
    if(error.response.status == 504 || error.response.status == 404){
        Message.error({message:"服务器被吃掉啦"})
    }else if(error.response.status==403){
        Message.error({message:'权限不足'})
    }else if(error.response.status==401){
        Message.error({message:'尚未登录,请登录'})
    }else {
        if(error.response.data.msg){
            Message.error({message:error.response.data.msg})
        }else{
            Message.error({message:'未知错误'})
        }
    }
    return;
})

//全局请求变量--请求前缀,万一某天给路径添加前缀,配置这个就不需要我们一个一个的去加了
let base = ''; 

/**
 * 补充的知识:
 * 登录用key:value传参
 * SpringSecurity:登录请求默认支持KeyValue传参,不能用Json传递. 
 *     原因:因为SpringSecurity中的过滤器用的是request.getParameter("key")方法接收参数的.
 *     如果想要接收Json就需要自定义重写接收过滤器了.
 * 
 * 
 * @function 封装了一个axios通信函数
 * 
 */
export const postKeyValueRequest=(url,params)=>{
    return axios({
        method:'post',
        url:`${base}${url}`,//注意这里不是单引号
        data:params,
        //transformRequest允许请求的数据在发送至服务器之前进行处理,这个属性只适用于put、post、patch方式
        transformRequest:[function (data){
            let temp = '';
            for (let i in data){
                //字符串拼接成  接口地址?username=value&password=pwd
                temp+=encodeURIComponent(i)+'='+encodeURIComponent(data[i])+'&'
            }
            return temp;
        }],
        //指定头部的一些数据编码方式,下面这种是form表单默认的编码方式.常用的还有json
        headers:{
            'Content-Type':'application/x-www-form-urlencoded'
        }
    })
}

//以下封装了 post,get,put,delete四种请求方法
//以json的格式传递
export const postRequest = (url, params) => {
    return axios({
        method: 'post',
        url: `${base}${url}`,
        data: params
    })
}
export const putRequest = (url, params) => {
    return axios({
        method: 'put',
        url: `${base}${url}`,
        data: params
    })
}
//post和put用data
//注意delete和get用params
export const getRequest = (url, params) => {
    return axios({
        method: 'get',
        url: `${base}${url}`,
        params: params
    })
}
export const deleteRequest = (url, params) => {
    return axios({
        method: 'delete',
        url: `${base}${url}`,
        params: params
    })
}

2.4 配置菜单工具类

需要说明的是:这里定义的 fmRouter是用来格式化冲数据库查到的菜单项的,并

import store from '../store'
import {getRequest} from './api'

/**
 * 用来初始化组件
 * 拿到菜单信息
 * @param {*} router 路由
 * @param {*} store Vuex
 * @returns
 */
export const initMenu=(router,store)=>{
    //检查store中的routes是否存在数据,
    if(store.state.routes.length>0){
        //存在则返回---有数据就算了
        return;
    }
    //store.status.routes不存在数据,就向服务端发送get请求数据
    // @param data 后端返回的菜单信息
    getRequest("/system/config/menu").then(data=>{
        if(data){//数据存在
            //格式化
            let fmtRoutes = formatRoutes(data);
            //向路由中添加
            router.addRoutes(fmtRoutes);
            //commit
            store.commit('initRoutes',fmtRoutes);
        }
    })
}

//对路由信息做一个转换---用来初始化组件
export const formatRoutes = (routes)=>{
    let fmRoutes = [];
    routes.forEach(router => {
        let {
            path,
            component,
            name,
            requireAuth,
            iconCls,
            children
        } = router;
        if (children && children instanceof Array) {
            children = formatRoutes(children);
        }
        let fmRouter = {
            path: path,
            name: name,
            iconCls: iconCls,
            requireAuth: requireAuth,
            children: children,
            component(resolve) {
                if (component.startsWith("Home")) {//主页
                    require(['../views/' + component + '.vue'], resolve);//往路由[router]中添加组件和对应的路径[path]
                } else if (component.startsWith("Goods")) {//商品管理
                    require(['../views/goods/' + component + '.vue'], resolve);
                } else if (component.startsWith("Category")) {//分类管理
                    require(['../views/category/' + component + '.vue'], resolve);
                } else if (component.startsWith("Order")) {//订单管理
                    require(['../views/order/' + component + '.vue'], resolve);
                } else if (component.startsWith("User")) {//用户管理
                    require(['../views/user/' + component + '.vue'], resolve);
                } else if (component.startsWith("Carousel")) {//轮播图管理
                    require(['../views/system/' + component + '.vue'], resolve);
                }else if (component.startsWith("Role")){//权限管理
                    require(['../views/role/'+component+'.vue'],resolve);
                }else if (component.startsWith("Comment")){//评价管理
                    require(['../views/comment/'+component+'.vue'],resolve);
                }else if (component.startsWith("Self")){
                    require(['../views/self/'+component+'.vue'],resolve);
                }
            }
        }
        fmRoutes.push(fmRouter);
    })
    return fmRoutes;
}

2.5 配置App.vue






2.6 配置main.js

import Vue from 'vue'
import './plugins/axios'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import 'element-ui/lib/theme-chalk/index.css' 
import ElementUI from 'element-ui'
//导入四种封装好的axios的通信方法
import {postKeyValueRequest}  from "./utils/api"
import {postRequest} from './utils/api'
import {getRequest} from './utils/api'
import {putRequest} from './utils/api'
import {deleteRequest} from './utils/api'
//导入图标
import 'font-awesome/css/font-awesome.min.css'
//导入初始化菜单的方法
import {initMenu} from './utils/menus'

Vue.config.productionTip = false
//添加到Vue原型对象上.
Vue.prototype.postKeyValueRequest = postKeyValueRequest
Vue.prototype.postRequest=postRequest
Vue.prototype.getRequest=getRequest
Vue.prototype.putRequest=putRequest
Vue.prototype.deleteRequest=deleteRequest


Vue.use(ElementUI)

/**
 * 创建了一个全局的路由守卫~类似后端的过滤器,filter(request,response,chain)
 * @param to    要去哪里
 * @param from  从哪里来
 * @next  放行
 */
router.beforeEach((to,from,next)=>{
  if(to.path=='/')//如果去login页面直接放行
    next()
  else{//如果去其他页面的话就对菜单进行初始化
    if(window.sessionStorage.getItem('user')){
      //如果已经登录了,那就正常去请求获取菜单接口
      initMenu(router,store)
      next()
    }else{
      console.log(to.path);

      //如果没登录,就返回到登录页面
      next('/?redirect='+to.path)
    }
  }
})


new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

2.7 配置一个代理服务器

let proxyObj = {};

proxyObj['/'] = {
    ws:false,
    target:'http://localhost:8081', //目标地址(后端地址)
    changeOrigin:true,
    pathRewrite:{
        '^/':''
    }
}

module.exports = {
    devServer:{
        host:'localhost',//前端地址
        port:8080,  //前端端口
        proxy:proxyObj
    }
}

3地址

后端:动态权限认证的脚手架: 基于Springboot+Spring Security +vue的前后端分离的动态权限控制脚手架
https://gitee.com/twentyseven/vue-backstage-scaffold
 

你可能感兴趣的:(spring,security,springboot,vue,spring,boot,vue.js,java)