使用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;
user表:存储了用户的个人基础信息、账号和密码;
user_role:维护了用户所具有的角色权限
role:存储了系统内所设置的角色权限
menu_role:维护了角色权限所能访问的菜单项(二级菜单)
menu:存储了系统内部所具有的菜单项,路径匹配规则、vue容器名称等
注意:其中的User.class要implements UserDetails接口。
创建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
自定义一个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
自定义个决定管理者:该类主要是实现:根据当前登录的用户的现有权限&访问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 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;
}
}
/**
* 配置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:为什么要继承?
继承等于是我们自己创建的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/**
,那么当用户在客户端发起一个/admin/user
的请求,将被/admin/**
拦截到,系统再去查看这个规则对应的角色是哪些,然后再去查看该用户是否具备相应的角色,进而判断该请求是否合法。
一级菜单不分配角色、只给二级菜单分配。
如果给一级菜单也分配角色,那么对于该一级菜单下的二级菜单就不好设计了。如果一级菜单下面有多个二级菜单。并且他具有一级菜单的访问权,那么每个二级菜单的访问权限问题该怎么划分呢?为了设计上的简单:我们只给二级菜单分配访问控制角色,如果他具有所有的二级菜单的访问权限=》都显示。如果他只具有一个,=》显示一个。如果具有0个=》那就连这个以及菜单也不显示了
前端采用vue来实现前端页面的开发
主要的主要有:vue,axios(和后端通信用的),vuex(存数据的)
打开cmd命令行,输入vue ui敲击[回车键],将需要的项目依赖都添加好
然后用编辑器打开项目
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
/**
* 专门用来管理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: {
}
})
// 封装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
})
}
需要说明的是:这里定义的 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;
}
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')
let proxyObj = {};
proxyObj['/'] = {
ws:false,
target:'http://localhost:8081', //目标地址(后端地址)
changeOrigin:true,
pathRewrite:{
'^/':''
}
}
module.exports = {
devServer:{
host:'localhost',//前端地址
port:8080, //前端端口
proxy:proxyObj
}
}
后端:动态权限认证的脚手架: 基于Springboot+Spring Security +vue的前后端分离的动态权限控制脚手架
https://gitee.com/twentyseven/vue-backstage-scaffold