一步步带你用react+spring boot搭建后台之二(登录与首页篇)
创始人
2024-04-15 19:03:50
0
  • 前言


最近半年一直在重庆忙于项目上的事情,博客停更了好久,一直想写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) {  //返回tokenconst action = {type: 'login_token',login_token: _token}//存放到cookiesetToken(_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 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());@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();return info;}@Overrideprotected 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

    @Beanpublic 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  userRoles=(List) 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查询functionList icRoleFunctions=(  List)cacheHelp.getObjectInCache(IcRoleFunction.class,new IcRoleFunction_by_roleId_Key(false),roleId);List functions=new ArrayList();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> trees = new ArrayList>();//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 tree = new Tree();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> 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 ({flex:1}}>{/*
*/}{flex:1}}>
{ padding: 0 }}>{React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {className: 'trigger',onClick: this.toggle,})}
{margin: '24px 16px',padding: 24,minHeight: 280,flex: 1,display: 'flex',flexDirection: 'column'}}>{/*内容区域*/}{/**/}
);} }

左边是菜单,右边为内容区,右边路由 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 (}>{text})}// 处理子级菜单栏renderSubMnenu = ({text,key,children}) => {return ({children && children.map(item => {return item.children && item.children.length > 0 ? this.renderSubMnenu(item) : this.renderMenu(item)})})}render() {let Router2= this.props.menus || [];const { selectedKeys,openKeys } = this.state//debuggerreturn ({ height: '100%', borderRight: 0 }}>{Router2 && Router2.map(firstItem => {return firstItem.children && firstItem.children.length > 0 ? this.renderSubMnenu(firstItem) : this.renderMenu(firstItem)})})}
}
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 {/**/}{/**/}
}export default router;

App.js当代代码:

import React from 'react';
import Router from './Router'
class App extends React.Component {render(){return ();}
}
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 (待续)

相关内容

热门资讯

保存时出现了1个错误,导致这篇... 当保存文章时出现错误时,可以通过以下步骤解决问题:查看错误信息:查看错误提示信息可以帮助我们了解具体...
汇川伺服电机位置控制模式参数配... 1. 基本控制参数设置 1)设置位置控制模式   2)绝对值位置线性模...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
不一致的条件格式 要解决不一致的条件格式问题,可以按照以下步骤进行:确定条件格式的规则:首先,需要明确条件格式的规则是...
本地主机上的图像未显示 问题描述:在本地主机上显示图像时,图像未能正常显示。解决方法:以下是一些可能的解决方法,具体取决于问...
表格列调整大小出现问题 问题描述:表格列调整大小出现问题,无法正常调整列宽。解决方法:检查表格的布局方式是否正确。确保表格使...
表格中数据未显示 当表格中的数据未显示时,可能是由于以下几个原因导致的:HTML代码问题:检查表格的HTML代码是否正...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...