-
前言
最近半年一直在重庆忙于项目上的事情,博客停更了好久,一直想写2个开源项目:
一个是入门级:一步步带你用react+spring boot搭建后台
一个是olap应用系列:一步步构建olap分析平台
今天开始写第一个系列,完整代码随后上传github
-
登录
登录界面:
click事件触发登录操作:
// //将store.dispatch方法挂载到props上
const mapDispatchToProps = (dispatch) => {
return {
login_prop(loginName, password) {
let r= login(loginName, password).then(
(res) => {
console.log("get article response:", res);
if (res.code === "200") {
let _token = res.data.token;
if (_token != null && _token.length > 0) { //返回token
const action = {
type: 'login_token',
login_token: _token
}
//存放到cookie
setToken(_token)
dispatch(action)
//继续跳转......
this.history.push('/main')
return;
}
else{
console.log("get token failed!");
return -2;
}
}else{ //登录失败 用户名 密码错误 主要走这个
return -1;
}
},
(error) => {
console.log("get response failed!");
return -3;
}
);
return r;
}
}
}
登录成功后,服务端会返回一个 token(该token的是一个能唯一标示用户身份的一个key),之后我们将token存储在本地cookie之中,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,不用再去登录页面重新登录了。
- 登录服务端
服务端我们用shiro实现:
shiro的结构图如下:
项目引入shiro网上教程很多,这里就不重复。
@PostMapping("/loginPost")
public R login(@RequestBody() IcUser user) {
HashMap<Object, Object> map = new HashMap<>();
Subject subject = SecurityUtils.getSubject();
try {
PersonnelPasswordToken token = new PersonnelPasswordToken(user);
subject.login(token);
} catch (Exception e) {
return R.error(e.getCause().getMessage());
}
map.put("token", subject.getSession().getId());
//basPersonnelService.clearResourceCache(UserUtil.getBasPersonnel().getGuid());
icSecurityModule.clearResourceCache(UserUtil.getIcUser().getUserId());
return R.ok(map);
}
这里我写了个UserRealm
import com.comm.cache.CacheException;
import com.comm.cache.CacheHelp;
import com.comm.cachecite.keydef.IcUserLoginName_Key;
import com.comm.common.exception.ReturnException;
import com.comm.common.utils.MD5Utils;
import com.comm.f_olap.entity.IcUser;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class UserRealm extends AuthorizingRealm {
CacheHelp cacheHelp=new CacheHelp();
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
PersonnelPasswordToken token = (PersonnelPasswordToken) authenticationToken;
String loginUserName = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
IcUser user = null;
try {
List cs=cacheHelp.getAllObjectInCache(IcUser.class);
user=(IcUser)cacheHelp.getObjectInCache(IcUser.class,new IcUserLoginName_Key(true),loginUserName);
} catch (CacheException e) {
e.printStackTrace();
}
if(user==null) throw new ReturnException("无此用户!");
if (user == null) {
throw new ReturnException("无此用户!");
}
String md5Psw = MD5Utils.encrypt(password).toUpperCase();
if (!user.getPassword().toUpperCase().equals(md5Psw)) {
throw new ReturnException("密码错误!");
} else {
logger.info("用户:{},登录成功!", loginUserName);
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
}
spring boot 通过 shiroconfig 认识相关realm
@Bean
public Realm realm() {
UserRealm userRealm = new UserRealm();
userRealm.setAuthenticationTokenClass(PersonnelPasswordToken.class);
return userRealm;
}
vue中用户登录成功之后,一般会在全局钩子router.beforeEach
中拦截路由,判断是否已获得token,在获得token之后再去获取用户的基本信息。
react实现路由拦截一般是靠鉴权组件去实现的,在特定的模块或者最上层主模块建立一个鉴权组件,在获取到当前路由信息时,可以先判断权限是否通过,不通过则不渲染children,用路由重定向至特定页面,否则渲染children。
componentDidMount() {
getMenus().then(
(res) => {
if( res.code == 501 ){
//没有登录 跳转到登录页面
this.props.history.push("/login")
return
}
console.log(res.data[0].children);
this.setState({menus: res.data[0].children});
},
(error) => {
console.log("get getMenus failed!");
}
);
}
就如前面所说的,我只在本地存储了一个用户的token,并没有存储别的用户信息(如用户权限,用户名,用户头像等)。有些人会问为什么不把一些其它的用户信息也存一下?主要出于如下的考虑:
假设我把用户权限和用户名也存在了本地,但我这时候用另一台电脑登录修改了自己的用户名,之后再用这台存有之前用户信息的电脑登录,它默认会去读取本地 cookie 中的名字,并不会去拉去新的用户信息。
正常情况下 获取用户信息和获取角色权限信息应该分为2个办法,我这里都是通过getMenus方法实现的。
@GetMapping("/getMemus")
public R getMemus(){
IcUser user=UserUtil.getIcUser();
Integer userId=user.getUserId();
//Integer userId=21001;
//根据userId 查找对应的role
//IcUserRole userRole=null;
try {
List<IcUserRole> userRoles=(List<IcUserRole>) cacheHelp.getObjectInCache(IcUserRole.class,new IcRole_by_userId_Key(false),userId);
if(userRoles!=null && userRoles.size()>0){
IcUserRole userRole=userRoles.get(0);
Integer roleId=userRole.getId().getRoleId();
//根据roleId 查找角色
// IcRole role=(IcRole)cacheHelp.getObjectInCache(IcRole.class,new IcRole_PK(true),roleId);
//根据role查询function
List<IcRoleFunction> icRoleFunctions=( List<IcRoleFunction>)cacheHelp.getObjectInCache(IcRoleFunction.class,new IcRoleFunction_by_roleId_Key(false),roleId);
List<SysResource> functions=new ArrayList<SysResource>();
if(icRoleFunctions!=null && icRoleFunctions.size()>0){
icRoleFunctions.forEach((IcRoleFunction rf)->{
Integer functionId= rf.getFunctionId();
SysResource function=null;
try {
function=(SysResource)cacheHelp.getObjectInCache(SysResource.class,new IcSysResource_Key(true),functionId);
functions.add(function);
} catch (CacheException e) {
e.printStackTrace();
}
});
}
List<Tree<SysResource>> trees = new ArrayList<Tree<SysResource>>();
//resourceOrder
//functions.sort();
//Collections.sort(functions,(s1, s2) ->);
functions.sort(Comparator.comparing(SysResource::getPosition));
for (int i = 0; i < functions.size(); i++) {
SysResource sysResource = functions.get(i);
Tree<SysResource> tree = new Tree<SysResource>();
tree.setKey(sysResource.getId());
tree.setId(sysResource.getId()+"");
tree.setParentId(sysResource.getParentId()+"");
tree.setText(sysResource.getResourceName());
tree.setTitle(sysResource.getResourceName());
tree.setPath(sysResource.getResourceUrl());
tree.setIcon(sysResource.getIcon());
trees.add(tree);
}
List<Tree<SysResource>> functionTrees = BuildTree.buildList(trees,"-1");
return R.ok(functionTrees);
}
} catch (CacheException e) {
e.printStackTrace();
}
//role 获取functions
//组装成数据
return R.ok();
}
-
首页
登录后跳转到的首页如下:
import React from 'react';
import ReactDOM from 'react-dom';
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useParams,
useRouteMatch
} from "react-router-dom";
import 'antd/dist/antd.css';
import './base.css';
import {Layout, Menu, Row, Col, Button} from 'antd';
import {
MenuUnfoldOutlined,
MenuFoldOutlined,
UserOutlined,
VideoCameraOutlined,
UploadOutlined,
TagOutlined
} from '@ant-design/icons';
import ReportType from '../views/reportType.js';
import UserList from '../views/userList.js';
import RoleList from '../views/roleList.js';
import UserRole from '../views/userRole.js';
import Resource from '../views/resource.js'
import ManageReport from '../views/manageReport.js';
import MainConntent from '../views/mainConntent.js';
import DimManagement from "../views/DimManagement";
import FormManagement from "../views/formManagement";
import ProcessManage from "../views/processManage.js"
import ProcessDef from "../views/processDef.js"
import ProcessDefView from "../views/ProcDefView.js"
import HeaderBar from "./headerBar"
import AsideMenu from "./AsideMenu";
import {getMenus} from "../api/Security";
import WorkflowDesign from '../views/workflowDesign.js'
import ProcDefView from '../views/ProcDefView.js'
import DataSource from "../views/dataSource";
import CodeMapping from '../views/codeMapping.js'
import ReadExcel from '../views/readExcel.js'
import QueryAccount from '../views/queryAccount.js'
import RunSql from '../views/runSql.js'
import Dynamic_Form_Designer from '../views/dynamic_form_designer.js'
import Dynamic_Form_Designer2 from '../views/dynamic_form_designer2.js'
import Account_report from '../views/Account_report.js'
import CaDataObject from '../views/caDataObject.js'
import AnalyzerFolder from '../views/analyzerFolder.js'
import CaAnalysisManage from '../views/caAnalysisManage.js'
const { Header, Sider, Content } = Layout;
export default class MainContent extends React.Component {
state = {
collapsed: false,
menus:[]
};
toggle = () => {
this.setState({
collapsed: !this.state.collapsed,
});
};
componentDidMount() {
getMenus().then(
(res) => {
if( res.code == 501 ){
//没有登录 跳转到登录页面
this.props.history.push("/login")
return
}
console.log(res.data[0].children);
this.setState({menus: res.data[0].children});
},
(error) => {
console.log("get getMenus failed!");
}
);
}
render() {
console.log(this.state.menus);
return (
<Layout id="mainLayout" className="main">
<Sider trigger={null} collapsible collapsed={this.state.collapsed}
style={{flex:1}}
>
{/*<div className="logo" />*/}
<AsideMenu menus={this.state.menus}/>
</Sider>
<Layout className="site-layout" style={{flex:1}}>
<Header className="site-layout-background" style={{ padding: 0 }}>
<Row>
<Col span={22}>
{React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: this.toggle,
})}
</Col>
<Col span={2}>
<HeaderBar/>
</Col>
</Row>
</Header>
<Content
className="site-layout-background"
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
flex: 1,
display: 'flex',
flexDirection: 'column'
}}
>
{/*内容区域*/}
{/*<Route exact path="/reportTypes" component={ReportType} />*/}
<Switch>
<Route exact path="/main/base/reportType">
<ReportType />
</Route>
<Route exact path="/main/base/datasource">
<DataSource />
</Route>
<Route path="/main/report/finance">
<ManageReport/>
</Route>
<Route path="/main/olap/dim">
<DimManagement/>
</Route>
<Route path="/main/olap/form">
<FormManagement/>
</Route>
<Route path="/main/secrity/userList">
<UserList/>
</Route>
<Route path="/main/secrity/role">
<RoleList/>
</Route>
<Route path="/main/secrity/resource">
<Resource/>
</Route>
<Route path="/main/secrity/userRole">
<UserRole/>
</Route>
<Route path="/main/COLLECTION_DATA/RUN_SQL">
<RunSql/>
</Route>
<Route path="/COLLECTION_DATA/RUN_SQL">
<UserList/>
</Route>
<Route path="/main/tool/processManage">
<ProcessManage/>
</Route>
<Route path="/main/tool/processDef">
<ProcessDef/>
</Route>
<Route exact path="/main/tool/viewProcDef/:procId" component={ProcessDefView}/>
<Route path="/main/tool/newDesigner">
<WorkflowDesign/>
</Route>
<Route path="/main/dataTool/codeMapping">
<CodeMapping/>
</Route>
<Route path="/main/dataTool/readExcel">
<ReadExcel/>
</Route>
<Route path="/main/dataTool/queryAccount">
<QueryAccount/>
</Route>
<Route path="/main/dynamicForm/formDesigner">
<Dynamic_Form_Designer/>
</Route>
<Route path="/main/dynamicForm/formDesigner2">
<Dynamic_Form_Designer2/>
</Route>
<Route path="/main/account_report">
<Account_report/>
</Route>
<Route path="/main/analyzer/DataObject">
<CaDataObject/>
</Route>
<Route path="/main/analyzer/analyzerFolder">
<AnalyzerFolder/>
</Route>
<Route path="/main/analyzer/DataObject">
<CaDataObject/>
</Route>
<Route path="/main/analyzer/AnalysisManage">
<CaAnalysisManage/>
</Route>
</Switch>
</Content>
</Layout>
</Layout>
);
}
}
左边是菜单,右边为内容区,右边路由 switch,是否应该根据后台数据循环构建呢?现在这种写法,后台动态增加菜单栏目的时候,还需要配套修改这个switch的内容啊。
路由体系,由路由地图、link和对应的组件三部分组成,下面我们描述link的构建,动态渲染菜单。
- 动态渲染菜单
当前,我们从后端根据用户角色查询到可以访问的菜单数据,传递到前端,前端根据后台的菜单数据,动态渲染左边导航菜单,
从后端返回的菜单信息,包括菜单名称、key等,;另外包括path信息,这个和路由是对应的,当用户点击菜单的时候,即link关联的路由,
{
"key":0,
"id":"0",
"text":"全部",
"title":"全部",
"alias":null,
"path":"/main",
"position":0,
"state":null,
"checked":false,
"attributes":null,
"children":[
{
"key":1,
"id":"1",
"text":"基础资料",
"title":"基础资料",
"alias":null,
"path":"/main/base",
"position":0,
"state":null,
"checked":false,
"attributes":null,
"children":[
Object{...},
{
"key":12,
"id":"12",
"text":"数据源",
"title":"数据源",
"alias":null,
"path":"/main/base/datasource",
"position":0,
"state":null,
"checked":false,
"attributes":null,
"children":null,
"parentId":"1",
"hasParent":true,
"hasChildren":false,
"icon":null,
"btn":false,
"menuId":null,
"type":null,
"ftype":0,
"levelnum":0
}
],
"parentId":"0",
"hasParent":true,
"hasChildren":true,
"icon":null,
"btn":false,
"menuId":null,
"type":null,
"ftype":0,
"levelnum":0
},
{
"key":3,
"id":"3",
"text":"财务报表",
"title":"财务报表",
"alias":null,
"path":"/main/report/",
"position":0,
"state":null,
"checked":false,
"attributes":null,
"children":[
{
"key":31,
"id":"31",
"text":"报表模板",
"title":"报表模板",
"alias":null,
"path":"/main/report/finance",
"position":0,
"state":null,
"checked":false,
"attributes":null,
"children":null,
"parentId":"3",
"hasParent":true,
"hasChildren":false,
"icon":null,
"btn":false,
"menuId":null,
"type":null,
"ftype":0,
"levelnum":0
},
......
],
"parentId":"-1",
"hasParent":false,
"hasChildren":true,
"icon":null,
"btn":false,
"menuId":null,
"type":null,
"ftype":0,
"levelnum":0
}
我们根据后台取得的菜单数据渲染前端菜单,代码如下:
import React, { Component,Fragment } from 'react'
import {Link,withRouter} from 'react-router-dom'
import { Menu } from 'antd';
import {
TagOutlined
} from '@ant-design/icons';
const { SubMenu } = Menu;
class AsideMenu extends Component {
constructor(props) {
super(props);
this.state= {
selectedKeys:[], //selectedKeys 当前选中的菜单项 key 数组
openKeys:[], //openKeys, 当前展开的 SubMenu 菜单项 key 数组
menus:this.props.menus
}
}
componentDidMount(){
const pathname = this.props.location.pathname;
const menukey = pathname.split("/").slice(0,3).join('/');
const menuHigh = {
selectedKeys: pathname,
openKeys: menukey
}
this.selectMenuHigh(menuHigh)
}
selectMenu =({item,key,keyPath}) => {
// 选中菜单
const menuHigh = {
selectedKeys: key,
openKeys: keyPath[keyPath.length - 1]
}
this.selectMenuHigh(menuHigh)
}
openMenu = (openKeys) => {
// 展开
this.setState({
openKeys: [openKeys[openKeys.length - 1]]
})
}
selectMenuHigh = ({selectedKeys,openKeys}) => {
// 菜单高亮
this.setState({
selectedKeys: [selectedKeys],
openKeys: [openKeys]
})
}
// 处理一级菜单栏
renderMenu =({title,key,path, text}) => {
return (
<Menu.Item key={key} icon={<TagOutlined/>}>
<Link to={path}>
<span>{text}</span>
</Link>
</Menu.Item>
)
}
// 处理子级菜单栏
renderSubMnenu = ({text,key,children}) => {
return (
<SubMenu key={key} title={text}>
{
children && children.map(item => {
return item.children && item.children.length > 0 ? this.renderSubMnenu(item) : this.renderMenu(item)
})
}
</SubMenu>
)
}
render() {
let Router2= this.props.menus || [];
const { selectedKeys,openKeys } = this.state
//debugger
return (
<Fragment>
<Menu
onOpenChange={this.openMenu}
onClick={this.selectMenu}
theme="dark"
mode="inline"
selectedKeys={selectedKeys}
openKeys={openKeys}
style={{ height: '100%', borderRight: 0 }}
>
{
Router2 && Router2.map(firstItem => {
return firstItem.children && firstItem.children.length > 0 ? this.renderSubMnenu(firstItem) : this.renderMenu(firstItem)
})
}
</Menu>
</Fragment>
)
}
}
export default withRouter(AsideMenu)
- 路由
//引入react jsx写法的必须
import React from 'react';
//引入需要用到的页面组件
import Home from './pages/home';
import About from './pages/about';
import Designer from './pages/designer';
import Login from './base/login'
import MainContent from './base/main'
import EditReport from './views/editReport'
import EditReport_Grid from './views/editReport_Grid'
import ViewReport from './views/viewReport'
import DimManagement from './views/DimManagement'
import FormManagement from './views/formManagement'
import ViewForm from './views/ViewForm'
import TestDiv from './views/testDiv'
//引入一些模块
import { BrowserRouter as Router, Route,Switch} from "react-router-dom";
function router(){
return <Router>
<Switch>
<Route key="/home" path="/home" component={Home} />
<Route key="/designer" path="/designer" component={Designer} />
<Route key="/about" path="/about" component={About} />
<Route key="/login" path="/login" component={Login} />
<Route key="/mainKey" path="/main" component={MainContent} />
{/*<Route key="/main2" component={SiderDemo2} />*/}
<Route key="/eidtReport/id" path="/eidtReport/:id" component={EditReport} />
<Route key="/eidtReportGrid/id" path="/eidtReportGrid/:id" component={EditReport_Grid} />
<Route key="/viewReport/id" path="/viewReport/:id/:accountSetId/:year/:month/:orgName/:reportDate/:reportNo/:lister/:auditor/:way/:reportInstanceId" component={ViewReport} />
{/*<Route path="/login" component={LoginForm} />*/}
<Route key="/dim" path="/dim" component={DimManagement} />
<Route key="/formManagement" path="/formManagement" component={FormManagement} />
<Route key="/viewForm/formId" path="/viewForm/:formId" component={ViewForm} />
<Route key="/testdiv" path="/testdiv" component={TestDiv} />
</Switch>
</Router>
}
export default router;
App.js当代代码:
import React from 'react';
import Router from './Router'
class App extends React.Component {
render(){
return (
<Router />
);
}
}
export default App;
- 每次请求带token
我们对每一个请求都会验证权限,所以这里我们针对业务封装了一下请求。首先我们通过request拦截器在每个请求头里面塞入token,好让后端对请求进行权限验证。
/**
* 网络请求配置
*/
import axios from "axios";
import store from '../store/store'
import { getToken } from './auth'
//store.getState()
axios.defaults.timeout = 200000;
axios.defaults.baseURL = "/reportapi/report";
/**
* http request 拦截器
*/
axios.interceptors.request.use(
(config) => {
if (store.getState().login_token) {
//为什么从cookie当中取这个值呢?从state当中取得不好吗
config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// showFullScreenLoading()
// startLoading()
console.log("getToken():"+getToken());
console.log("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$:"+store.getState().login_token)
// config.data = JSON.stringify(config.data);
// config.headers = {
// "Content-Type": "application/json",
// };
return config;
},
(error) => {
return Promise.reject(error);
}
);
/**
* http response 拦截器
*/
axios.interceptors.response.use(
(response) => {
if (response.data.errCode === 2) {
console.log("过期");
}
return response;
},
(error) => {
console.log("请求出错:", error);
}
);
/**
* 封装get方法
* @param url 请求url
* @param params 请求参数
* @returns {Promise}
*/
export function get(url, params = {}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params,
}).then((response) => {
//landing(url, params, response.data);
console.log("http response in axios:"+response)
resolve(response.data);
})
.catch((error) => {
console.log("http error in axios:"+error)
reject(error);
});
});
}
/**
* 封装post请求
* @param url
* @param data
* @returns {Promise}
*/
export function post(url, data) {
return new Promise((resolve, reject) => {
axios.post(url, data).then(
(response) => {
//关闭进度条
resolve(response.data);
},
(err) => {
reject(err);
}
);
});
}
/**
* 封装patch请求
* @param url
* @param data
* @returns {Promise}
*/
export function patch(url, data = {}) {
return new Promise((resolve, reject) => {
axios.patch(url, data).then(
(response) => {
resolve(response.data);
},
(err) => {
msag(err);
reject(err);
}
);
});
}
/**
* 封装put请求
* @param url
* @param data
* @returns {Promise}
*/
export function put(url, data = {}) {
return new Promise((resolve, reject) => {
axios.put(url, data).then(
(response) => {
resolve(response.data);
},
(err) => {
msag(err);
reject(err);
}
);
});
}
//统一接口处理,返回数据
export default function (fecth, url, param) {
let _data = "";
return new Promise((resolve, reject) => {
switch (fecth) {
case "get":
console.log("begin a get request,and url:", url);
get(url, param)
.then(function (response) {
resolve(response);
})
.catch(function (error) {
console.log("get request GET failed.", error);
reject(error);
});
break;
case "post":
post(url, param)
.then(function (response) {
resolve(response);
})
.catch(function (error) {
console.log("get request POST failed.", error);
reject(error);
});
break;
default:
break;
}
});
}
//失败提示
function msag(err) {
if (err && err.response) {
switch (err.response.status) {
case 400:
alert(err.response.data.error.details);
break;
case 401:
alert("未授权,请登录");
break;
case 403:
alert("拒绝访问");
break;
case 404:
alert("请求地址出错");
break;
case 408:
alert("请求超时");
break;
case 500:
alert("服务器内部错误");
break;
case 501:
alert("服务未实现");
break;
case 502:
alert("网关错误");
break;
case 503:
alert("服务不可用");
break;
case 504:
alert("网关超时");
break;
case 505:
alert("HTTP版本不受支持");
break;
default:
}
}
}
/**
* 查看返回的数据
* @param url
* @param params
* @param data
*/
function landing(url, params, data) {
if (data.code === -1) {
}
}
上面对后端请求也进行了简单封装。
- 首页布局(待续)
-
permission (待续)