使用Spring Initializr创建一个Spring Boot项目
[Web]:Spring Web
[Security]:Spring Security
[SQL]:MyBatis framework、MySQL Driver、Durid数据连接池
后面如果有什么需要再添加
org.springframework.boot spring-boot-starter-securityorg.springframework.boot spring-boot-starter-weborg.mybatis.spring.boot mybatis-spring-boot-starter2.2.0 mysql mysql-connector-javaruntime com.alibaba druid-spring-boot-starter1.1.10 org.springframework.boot spring-boot-starter-testtest org.springframework.security spring-security-testtest
连接数据库:
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 数据库设计
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;
@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
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();
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
//拿到当前请求的地址
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//拿到角色和菜单项的(1:m)查询结果
List
1.4.2 自定义CustomDecisionManager
自定义个决定管理者:该类主要是实现:根据当前登录的用户的现有权限&访问URL所需要的权限进行投票决定.
在这里我们和系统选用的默认投票器的处理方法保持一直:选用一票即过.就是用户只需要有访问该URL至少一个权限就可以通过.
@Component
public class CustomDecisionManager implements AccessDecisionManager {
@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 extends GrantedAuthority> 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
@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();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@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="提交接口"
.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:登录成功");
}
})
.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
1.5创建测试Controllertips:为什么要继承?
继承等于是我们自己创建的SecurityConfig在WebSecurityConfigurerAdapter的基础之上去[扩展]、[修改(重写)]父类的功能。
相当于站在巨人的肩膀上前进
@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 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"; 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 = ''; 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' 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.vue2.6 配置main.jsimport 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) 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


![[SpringBoot+Security+vue]动态管理权限脚手架-前后端分离 [SpringBoot+Security+vue]动态管理权限脚手架-前后端分离](http://www.mshxw.com/aiimages/31/328649.png)
