效果:
前台空架子:
后台管理系统
架构图:
单点登录算法步奏:
1.全路由设置(import页面、基础路由、模块路由)准备,路由就是把所有页面模块的路径都按照权限管理起来.要和后台路由对应起来
import Vue from 'vue'
import Router from 'vue-router' //导入Router 路由跳转
slow.so only in production use Lazy Loading
/* layout */
import Layout from '../views/layout/Layout' //导入主页面布局
import inj_pro from "../views/production_opt/inj_pro"; //导入模块页面
import well_loc from "../views/production_opt/well_loc";
import method from "../views/production_opt/method";
import formation from "../views/production_opt/formation";
import all from "../views/production_opt/all";
import cat from "../views/model/cat";
import upload from "../views/model/upload";
const _import = require('./_import_' + process.env.NODE_ENV)
Vue.use(Router)
//持久通用的路由
export const constantRouterMap = [
{path: '/login', component: _import('login/index'), hidden: true}, //登陆页面
{path: '/404', component: _import('404'), hidden: true}, //错误页面
//主页面的映射方式/
{
path: '/', //路径
component: Layout, //主页面
redirect: '/dashboard', //main重定向到dashboard页面,真正的页面
name: '首页',
hidden: true,
children: [{
path: 'dashboard', component: _import('dashboard/index')
}]
}
]
export default new Router({
mode: 'history', //后端支持可开
scrollBehavior: () => ({y: 0}),
routes: constantRouterMap
})
//动态路由
export const asyncRouterMap = [
{
path: '/model', //映射
component: Layout, //组件是主页
redirect: upload, //模块页面
name: '模型管理',
meta: {title: '模型管理', icon: 'tree'}, //图标
children: [
{
path: 'upload', 映射
name: '上传模型',
component: upload, //组件是上传页
meta: {title: '上传模型', icon: 'example'},
menu: 'upload'
},
{
path: 'cluster',
name: '查看模型',
component: cat,
meta: {title: '查看模型', icon: 'example'},
menu: 'cat'
}
]
},
{
path: '/production',
component: Layout,
redirect: inj_pro,
name: '生产优化',
meta: {title: '生产优化', icon: 'table'},
children: [
{
path: 'injec_pro',
name: '注采优化',
component: inj_pro,
meta: {title: '注采优化', icon: 'user'},
menu: 'injec_pro'
},
{
path: 'well_loc',
name: '井位优化',
component: well_loc,
meta: {title: '井位优化', icon: 'user'},
menu: 'well_loc'
},
{
path: 'method',
name: '措施优化',
component: method,
meta: {title: '措施优化', icon: 'user'},
menu: 'method'
},
{
path: 'formation',
name: '层位优化',
component: formation,
meta: {title: '层位优化', icon: 'user'},
menu: 'formation'
},
{
path: 'all',
name: '整体优化',
component: all,
meta: {title: '整体优化', icon: 'password'},
menu: 'all'
},
]
},
{path: '*', redirect: '/404', hidden: true}
]
2.后台请求拦截器、数据库配置等等配置
web拦截器:控制请求类型,启动就一直在监控
Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
//允许全部请求跨域
registry.addMapping("/**") //**代表全部
//.allowedOrigins("http://localhost:8080")
// .allowedOrigins("http://localhost:8088")
.allowedMethods("GET", "POST","OPTIONS")
.allowCredentials(true).maxAge(3600);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SsoInterceptor())
.excludePathPatterns("/shiro/updatePermission")
.excludePathPatterns("/login/auth")
.addPathPatterns("/**");
//验证有问题,待解决
// registry.addInterceptor(new LoginInterceptor())
// .excludePathPatterns("/error")
// .excludePathPatterns("/login/**")
// .addPathPatterns("/**");
}
}
单点登录拦截器:HandlerInterceptorAdapter拦截器配置,只在刷新和登录时拦截
日志记录,可以记录请求信息的日志,以便进行信息监控、信息统计等。
权限检查:如登陆检测,进入处理器检测是否登陆,如果没有直接返回到登陆页面。
性能监控:典型的是慢日志。
在HandlerInterceptorAdapter中主要提供了以下的方法:
preHandle:在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行,也就是说我们想调用的方法 不会被执行,但是你可以修改response为你想要的响应。
postHandle:在方法执行后调用。
afterCompletion:在整个请求处理完毕后进行回调,也就是说视图渲染完毕或者调用方已经拿到响应
package com.teradata.zuul.interceptor;
import com.google.gson.Gson;
import com.teradata.common.bean.ErrorEnum;
import com.teradata.common.util.JWTUtil;
import com.teradata.zuul.service.RedisService;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* 单点登录拦截器
*/
public class SsoInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Object handler) throws Exception { //HttpServletRequest 请求和响应
//获取请求头中的Authorization属性
String authorization = servletRequest.getHeader("Authorization");
//BeanFactory 中获取redis,redis已经配置好了
BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(servletRequest.getServletContext());
RedisService redisService = (RedisService) factory.getBean("redisService");
boolean flag= false;
if (authorization != null && JWTUtil.verifyToken(authorization)){
//校验登陆的token是否与缓存中的token保持一致
String userId = JWTUtil.getUsername(authorization); //根据auth解析出userid
if (((String)redisService.get(userId)).equalsIgnoreCase(authorization)){ //从redis里面获取userid看是否和传入的相等
//如果成功返回true
flag = true;
return flag;
}
}
//否则返回false和servletResponse(内容是没有登陆)
servletResponse.setCharacterEncoding("UTF-8");
//Subject subject = getSubject(request, response);
PrintWriter printWriter = servletResponse.getWriter();
servletResponse.setContentType("application/json;charset=UTF-8");
servletResponse.setHeader("Access-Control-Allow-Origin", servletRequest.getHeader("Origin"));
servletResponse.setHeader("Access-Control-Allow-Credentials", "true");
servletResponse.setHeader("Vary", "Origin");
String respStr;
respStr = "you have not right to access";//返回的内容
printWriter.write(new Gson().toJson(new com.teradata.common.bean.ResponseBean(ErrorEnum.ERROR_10002.getErrorCode(),ErrorEnum.ERROR_10002.getErrorMsg(),"")));
printWriter.flush();
servletResponse.setHeader("content-Length", respStr.getBytes().length + "");//设置响应头
return flag;
}
}
yuml配置数据库文件
datasource:
master:
url: jdbc:mysql://localhost:3306/aa?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
driverClassName: com.mysql.jdbc.Driver
username: root
password: root
配置数据库代码
package com.teradata.zuul.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* @author xuyaohui
* @date 2018/7/27 0027 上午 10:32
*/
@Configuration
@MapperScan(basePackages = "com.teradata.zuul.repository",sqlSessionFactoryRef = "masterSqlSessionFactory")
public class DataSourceConfig { //类名 数据库配置
@Bean(name = "masterDataSource") //数据源的创建
@ConfigurationProperties("datasource.master") //找到yuml文件的字段自动注入参数
public DataSource masterDataSource(){
return DataSourceBuilder.create().build();//调用DataSourceBuilder来创建
}
@Bean(name = "masterSqlSessionFactory")//注入masterSqlSessionFactory工厂
public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {//调用masterDataSource创建数据源
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);//添加数据源
sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/*.xml"));//设置map.xml的位置
return sessionFactoryBean.getObject(); //返回工厂
}
}
``
3.点击登录:先去校验auth。。。
.登录的服务层同时获取(User.login/auth方法)、用户对象和权限菜单的数据、设置token到cookie
控件
<el-input type="password" @keyup.enter.native="handleLogin" v-model="loginForm.password"
autoComplete="on"></el-input>
JS
export default {
name: 'login',
data() {
return {
loginForm: { //参数
username: 'admin',
password: '123456'
},
loginRules: {
username: [{required: true, trigger: 'blur', message: "请输入用户名"}],
password: [{required: true, trigger: 'blur', message: "请输入密码"}]
},
loading: false
}
},
methods: {
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
//请求auth,loginForm参数
this.$store.dispatch('Login', this.loginForm).then(data => { //跳转autn后台的 js
console.log('获取到的data: '+data.msg);
this.loading = false
if ("success" === data.msg) {
//成功直接跳转主页面layout
this.$router.push({path: '/'})
} else {
this.$message.error("账号/密码错误");
}
}).catch(() => {
this.loading = false
})
} else {
this.$message.error("请输入所有字段");
return false
}
})
}
}
}
</script>
user.js里的方法
actions: {
// 登录
Login({commit, state}, loginForm) {
return new Promise((resolve, reject) => {
api({
url: "login/auth", //后台
method: "post",
data: loginForm
}).then(data => {
if (data.msg === "success") { //成功后
//使用sessionStorage存储token值
//前台存在session的属性cloud-ida-token中,存在服务器中
sessionStorage.setItem('cloud-ida-token',data.data);
//cookie中保存前端登录状态,设置了已经登录字段
setToken();
}
resolve(data);
}).catch(err => {
reject(err)
})
})
},
export function setToken() {
return Cookies.set(LoginKey, "1")
}
后台auth
@PostMapping("/login/auth")
public ResponseBean login(@RequestBody JSONObject jsonObject){
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");//传参
try {
User user=userService.getUserByAccount(username);//用id查到user
if (user.getPassword().equalsIgnoreCase(password)){//对比密码是否正确
Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
String token= JWT.create() //产生token
.withClaim("username", username)
.withClaim("roles",user.getRoles())
.withExpiresAt(date)
.sign(algorithm);
redisService.remove(username);
//redisService去掉以前的token,新创建token
System.out.println("before: "+token);
redisService.set(username,token);
System.out.println("after: "+ redisService.get(username));
// 附带username信息
return new ResponseBean(200,"success",token);//返回数据的第三个是数据
}
return new ResponseBean(200,"false","用户名或密码错误");
} catch (UnsupportedEncodingException e) {
return new ResponseBean(200,"false","异常抛出");
}
}
//根据userid(admin名字)查user
package com.teradata.zuul.service;
import com.teradata.zuul.bean.PermissionRole;
import com.teradata.zuul.bean.User;
import com.teradata.zuul.bean.UserPermission;
import com.teradata.zuul.repository.UserDao;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
@Service
public class UserService {
@Autowired
private UserDao userDao;
public User getUserByAccount(String userAccount){
User user=userDao.getUserByName(userAccount);
if (user!=null){
Set<String> permissions = userDao.getUserPermissions(userAccount);
user.setPermission(permissions);
//获取菜单
Set<String> s=userDao.getMenuListByName(userAccount);
user.setMenuList(userDao.getMenuListByName(userAccount));
}
return user;
}
User bean实体属性
public class User implements Serializable{
//用户标识
private String userAccount;
private String password;
//用户角色(多角色)
private String roles;
private Set<String> permission;
/**
* 该用户所有菜单
*/
private Set<String> menuList;
获取到了user拥有的的菜单栏目和操作权限
4.登陆成功后getinfo:跳转路由页面 layout主页下的dashboat。
会被前台拦截器拦截,已经登录但是没有信息:获取信息getinfo.
登陆成功跳转
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
//请求
this.$store.dispatch('Login', this.loginForm).then(data => {
console.log('获取到的data: '+data.msg);
this.loading = false
if ("success" === data.msg) {
//成功直接跳转主页面layout
this.$router.push({path: '/'})
} else {
this.$message.error("账号/密码错误");
}
}).catch(() => {
this.loading = false
})
} else {
this.$message.error("请输入所有字段");
return false
}
})
前台拦截器:::跳转页面前被拦截下来,是src下面的permission.js,
import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css' // Progress 进度条样式
import {getToken} from '@/utils/auth' // 验权
const whiteList = ['/login', '/404'] //白名单直接跳转
//每次使用路由都会判断
router.beforeEach((to, from, next) => {
NProgress.start()
//从cookie中取出token,,,getToken()
if (getToken()) {
//如果是重新登录的情况
if (to.path === '/login') {
next({path: '/'})
NProgress.done() // 结束Progress
//如果store.getters.role为null,则GetInfo
} else if (!store.getters.role) {
//如果是没获取state信息的情况,跳转后台GetInfo
store.dispatch('GetInfo').then(() => {
next({...to})
})
} else {
//其他情况:也就是信息都有了的情况,直接跳转
next()
}
} else if (whiteList.indexOf(to.path) !== -1) {
//如果前往的路径是白名单内的,就可以直接前往
next()
} else {
//如果路径不是白名单内的,而且又没有登录,就跳转登录页面
next('/login')
NProgress.done() // 结束Progress
}
})
router.afterEach(() => {
//跳转之后,进度条关闭
NProgress.done() // 结束Progress
})
从user类里的GetInfo方法
前端debug
GetInfo({commit, state}) {
return new Promise((resolve, reject) => {
api({
url: '/login/getInfo',
method: 'get'
}).then(data => {
//设置用户
commit('SET_USER', data.data);
//cookie保存登录状态,仅靠vuex保存的话,页面刷新就会丢失登录状态
setToken();
//生成路由
let userPermission = data.data ;
console.log("userPermission"+userPermission);
store.dispatch('GenerateRoutes', userPermission).then(() => {
//生成该用户的新路由json操作完毕之后,调用vue-router的动态新增路由方法,将新路由添加
router.addRoutes(store.getters.addRouters)
})
resolve(data)
}).catch(error => {
reject(error)
})
})
},
前台拦截器api.js:每次请求axios 的时候设置请求头——加auth(token)
vue+vue-resource设置请求头(带上token)
知乎http请求原理
import axios from 'axios'
import {Message, MessageBox} from 'element-ui'
import {getToken} from '@/utils/auth'
import store from '../store'
// 创建axios实例service
const service = axios.create({
baseURL: process.env.BASE_URL, // api的base_url
timeout: 15000 // 请求超时时间2
})
// service 对象调用了request拦截器
service.interceptors.request.use(config => {
//从sessionStorage获取token给赋值到headers
config.headers.Authorization=sessionStorage.getItem('cloud-ida-token');
return config
}, error => {
// Do something with request error
console.error(error) // for debug
Promise.reject(error)
})
// service 对象调用了 respone拦截器
service.interceptors.response.use(
response => {
const res = response.data;
if (res.code == "200") {
//如果返回200
return res
}
//如果返回10002
if (res.code == '10002') {
Message({
//弹出错误消息
showClose: true,
//使用返回的错误信息
message: res.msg,
type: 'error',
duration: 3 * 1000,
onClose: () => {
store.dispatch('FedLogOut').then(() => {
location.reload()// 为了重新实例化vue-router对象 避免bug
})
}
});
return Promise.reject(res.msg)
}else{
//如果返回其他错误
Message({
message: res.msg,
type: 'error',
duration: 3 * 1000
})
return res
}
},
error => {
console.error('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 3 * 1000
})
return Promise.reject(error)
}
)
export default service
getInfo后台:获取token对比,成功才给userinfo信息。
JWTUtil代替了权限认证接口,到时候请求接口获取user,再去getuserinfo就行。
服务器没法通过session来验证你的身份,所以服务器的过滤器(或拦截器)会过滤掉你的请求,让你返回登陆界面重新登录,使用户体验变差。
Authorization里面放的就是token,就相当于每次发送请求的时候,拦截器都会拦截一次你的请求,
把你请求头部的Authorization拿出来,与当前存在服务器上的token做对比。
* @return
*/
@GetMapping("/login/getInfo")
public ResponseBean getInfo(ServletRequest req, ServletResponse resp) {
//获取参数Authorization
HttpServletRequest httpServletRequest = (HttpServletRequest) req;
String authorization = httpServletRequest.getHeader("Authorization");
//token获取id来获取用户信息
System.out.println("获取的auth: "+authorization);
//再次获取user类 ,getUserByAccount
if (authorization!=null){
String userId=JWTUtil.getUsername(authorization);
return new ResponseBean(200,"success",userService.getUserByAccount(userId));
}
return new ResponseBean(401,"false","获取用户信息出错");
}
成功返回到js
GetInfo({commit, state}) {
return new Promise((resolve, reject) => {
api({
url: '/login/getInfo',
method: 'get'
}).then(data => {
//设置用户user
commit('SET_USER', data.data);
//cookie保存登录状态,仅靠vuex保存的话,页面刷新就会丢失登录状态
setToken();
//生成路由
let userPermission = data.data ;
console.log("userPermission"+userPermission);
store.dispatch('GenerateRoutes', userPermission).then(() => {
//生成该用户的新路由json操作完毕之后,调用vue-router的动态新增路由方法,将新路由添加
router.addRoutes(store.getters.addRouters)
})
resolve(data)
}).catch(error => {
reject(error)
})
})
},
返回后赋值给user对象:
前台的user.js,接收user:就相当于一个类, state、mutations是构造方法,actions是方法
import {getInfo, login, logout} from '@/api/login'
import {getToken, removeToken, setToken} from '@/utils/auth'
import {default as api} from '../../utils/api'
import store from '../../store'
import router from '../../router'
const user = {
state: {
nickname: "",
userId: "",
avatar: 'https://www.gravatar.com/avatar',
role: '',
menus: [],
permissions: [],
},
mutations: {
SET_USER: (state, userInfo) => {
state.nickname = userInfo.userAccount;
state.userId = userInfo.userAccount;
state.role = userInfo.roles;
state.menus = userInfo.menuList;
state.permissions = userInfo.permission;
},
RESET_USER: (state) => {
state.nickname = "";
state.userId = "";
state.role = '';
state.menus = [];
state.permissions = [];
}
},
actions: {
// 登录
Login({commit, state}, loginForm) {
getinfo(){。。。。。。}
前台的store相当于类工厂
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
import permission from './modules/permission'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app, //app.js类
user, //user.js类
permission //permission .js类
},
getters
})
export default store
返回后生成路由
permission是权限类,里面有创建路由的方法,判断是否有权限,过滤路由:
import {asyncRouterMap, constantRouterMap} from '@/router/index'
/**
* 判断用户是否拥有此菜单
* @param menus
* @param route
*/
function hasPermission(menus, route) {
if (route.menu) {
/*
* 如果这个路由有menu属性,就需要判断用户是否拥有此menu权限
*/
return menus.indexOf(route.menu) > -1;
} else {
return true
}
}
/**
* 递归过滤异步路由表,返回符合用户菜单权限的路由表
* @param asyncRouterMap
* @param menus
*/
function filterAsyncRouter(asyncRouterMap, menus) {
const accessedRouters = asyncRouterMap.filter(route => {
//filter,js语法里数组的过滤筛选方法
if (hasPermission(menus, route)) {
if (route.children && route.children.length) {
//如果这个路由下面还有下一级的话,就递归调用
route.children = filterAsyncRouter(route.children, menus)
//如果过滤一圈后,没有子元素了,这个父级菜单就也不显示了
return (route.children && route.children.length)
}
return true
}
return false
})
return accessedRouters
}
//permission 类
const permission = {
state: {
routers: constantRouterMap, //本用户所有的路由,包括了固定的路由和下面的addRouters
addRouters: [] //本用户的角色赋予的新增的动态路由
},
mutations: {
SET_ROUTERS: (state, routers) => {
//设置路由的方法
state.addRouters = routers
state.routers = constantRouterMap.concat(routers) //将固定路由和新增路由进行合并, 成为本用户最终的全部路由信息
console.log(state.routers);
}
},
actions: {
GenerateRoutes({commit}, userPermission) {
//生成路由
return new Promise(resolve => {
const role = userPermission.roles;//角色
const menus = userPermission.menuList;//菜单
//声明 该角色可用的路由
let accessedRouters
if (role === '管理员') {
//如果角色里包含'管理员',那么所有的路由都可以用
//其实管理员也拥有全部菜单,这里主要是利用角色判断,节省加载时间
accessedRouters = asyncRouterMap
} else {
//否则需要通过以下方法来筛选出本角色可用的路由
console.log("过滤前"+menus)
accessedRouters = filterAsyncRouter(asyncRouterMap, menus)
console.log("过滤后"+accessedRouters)
}
//调用执行设置路由的方法
commit('SET_ROUTERS', accessedRouters)
resolve()
})
}
}
}
export default permission
7.服务层方法调用mapper映射sql语句 到dao层、
8.Token、用户信息和用户权限菜单 写入本地sessionStorage缓存和user用户对象(commit('SET_USER', data.data)方法;) (vuexx,store仓库)
9.全路由对比登录后得到的路由许可,得到用户的菜单路由并暴露(js)、
10登录成功直接跳转主页面layout、
11侧边栏去找路由store定义(用户的菜单)、
12点击侧边栏跳转路由占位符main那里;
12也可以是跳到home页面再请求菜单数据再对比再渲染。
遇到的Bug:请求不到新加的权限菜单,是因为获取菜单那里写死了,父id必须15。Debug找问题源头
/