目录
一、用户权限设置
只有管理员才能对后台进行登录,在用户表中设置管理员属性admin,设置默认值为0,当这个值大于0时,表示为管理员权限,其中将一个账号设置为超级管理员,管理其他的普通管理员,所以在账号进行登录时,判断他的admin属性值,是否能够进行登录
前端逻辑
先获取当前登录用户的token,以及要修改的这一行数据,作为参数传递给后端,根据返回值判断是否允许修改
后端逻辑
- 先使用JWT工具类解析当前的token值中的用户信息
- 判断token中的用户信息是否存在防止NPE
- 判断当前token中的用户是否是超级管理员,不是超级管理员返回fail
- 是超级管理员则对admin值进行修改
二、登录界面逻辑
1、账号密码登录实现
前端逻辑
使用element表单获取输入的数据,单击登录,将表单提交给后端,提交给后端后,后端会根据当前登录的账号创建一个token返回给前端,前端接收到token 将token存储进 Cookie中,接收到结果为真直接跳转到首页
在Cookie中存储token的方法
- 在Vue项目中安装 js-cookie npm i js-cookie
- 在项目中引入Cookie import Cookie from 'js-cookie'
- 设置token存储进Cookie Cookie.set('token',后端返回的token值)
在Cookie中存储和获取的token方法
- 在script中引入Cookie
- Cookie.get('token')
后端逻辑
根据登录的账户放入Redis中存储并设置过期事件再生成token并返回
@Service
public class LoginServiceImpl implements LoginService {
//加密盐 防止md5被解密
private static final String slat = "mszlu!@#";
//登录功能需要查询用户表,将用户操作的service注入
@Autowired
private UserService userService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public Result login(LoginParam loginParam) {
/**
* 1、检查参数是否合法
* 2、根据用户名和密码在 user表中查询是否存在
* 3、如果不存在 登陆失败
* 4、登陆成功 使用JWT生成token 返回给前端
* 5、将token放入redis 设置:(tokenuser 信息 设置过期时间)
* (登录认证的时候 先认证token字符串是否合法 再去Redis认证是否存在)
*/
//获取登陆的用户名和密码
String username = loginParam.getUsername();
String password = loginParam.getPassword();
//判断账户对象或者密码是否为空
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){
return Result.fail(1200,"账户对象或密码为空");
}
//给密码加密
password = DigestUtils.md5Hex(password + slat);
//查询用户表中有没有对应的用户对象
SysUser sysUser = userService.findUserInLog(username,password);
if (sysUser == null){
return Result.fail(1201,"用户名或密码错误!");
}
//如果是管理员登录才允许进入
Integer admin = sysUser.getAdmin();
if (admin >= 1){
//用户存在生成对应的token
String token = JWTUtils.createToken(sysUser.getId());
//将对象信息也放入redis中
//如果用户登录那么一定会在redis中找到这个对象信息
//将token放入redis中 设置过期时间1天
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser), 1, TimeUnit.DAYS);
return Result.success(token);
}else {
return Result.fail(3020,"对不起,当前用户权限不足!");
}
}
}
生成token工具类
public class JWTUtils {
//JWT密钥
private static final String jwtToken = "123456!@#$$";
//创建一个token
public static String createToken(Long userId){
//B部分
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
//A部分
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
//设置B部分
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
//设置token完成
String token = jwtBuilder.compact();
return token;
}
public static Map<String, Object> checkToken(String token){
try {
//JWT 通过密钥 解析token
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
补充:根据token获取当前登录对象信息
@Override
public Result getUserByToken(String token) {
//先解析token
Map<String, Object> map = JWTUtils.checkToken(token);
//查看是否为空
if (map == null){
return Result.fail(5000,"当前未登录");
}
//查看redis中是否存在token token中存放了对象
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
if (StringUtils.isBlank(userJson)){
return Result.fail(5000,"当前未登录");
}
//存在token 解析这个JSON
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
return Result.success(copy(sysUser));
}
2、手机号登录
前台逻辑
同样使用表单构建 将当前用户输入的手机号发送给后端 当点击发送验证码时,将手机号发送给后端校验,校验通过后再申请发送验证码的接口,接收到后端发送的验证码先进行解密,在根据用户输入的验证码进行登录校验,校验成功将后端返回的token存储进Cookie中,跳转到首页。
后台逻辑
先接收前端发送的手机号查询到当前用户对象,先判断账号是否存在,如果不存在则提示先注册,在判断当前用户的权限是否允许登录,权限允许 则调用发送验证码接口,返回给前端加密的验证码,前端用户输入接收到的验证码点击登录后,再将当前的手机号发送给后端,生成token返回。
先在阿里云短信服务中申请签名和短信模板,获取阿里云accessKeyID的账号和密码,以及注册好的短信模板代码,调用接口
@Component
public class CodeUtils {
//生成手机验证码工具类
public boolean sendCode(String phoneNum,String templateCode,Map<String,Object> code) {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "LTAI5tJX8MHTLx4mPGw5hNyN", "BTI4MSNNYq8Bl0KZ3mv449lmeBJhFl");
IAcsClient client = new DefaultAcsClient(profile);
//构建请求
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysAction("SendSms");
request.setSysVersion("2017-05-25");
//自定义的参数(手机号、验证码、签名、模板)
request.putQueryParameter("PhoneNumbers", phoneNum);
//签名
request.putQueryParameter("SignName", "猿客栈后台登录");
//模板
request.putQueryParameter("TemplateCode", templateCode);
//构建一个短信验证码
request.putQueryParameter("TemplateParam", JSONObject.toJSONString(code));
try {
CommonResponse response = client.getCommonResponse(request);
System.out.println(response.getData());
//返回是否发送成功
return response.getHttpResponse().isSuccess();
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
}
return false;
}
/**
* 随机生成6位验证码
* @return
*/
public String getCode(){
Random random = new Random();
StringBuffer builder = new StringBuffer();
//循环6次生成验证码
for (int i=0;i<6;i++){
int code = random.nextInt(10);
builder.append(code);
}
return builder.toString();
}
/**
* 将验证码加密
*/
public String md5String(String code){
//将验证码加密
StringBuffer reverse = new StringBuffer(code).reverse();
String s = reverse.toString();
Long i = Long.valueOf(s);
i += 123456;
return String.valueOf(i);
}
}
LoginService
@Override
public Result loginByPhone(String phone) {
//根据手机号查找用户对象并判断权限
SysUser userByPhone = userService.getUserByPhone(phone);
if (Objects.isNull(userByPhone)){
return Result.fail(5000,"该手机号未注册!请先注册");
}
//创建token返回
//用户存在生成对应的token
String token = JWTUtils.createToken(userByPhone.getId());
//将token放入redis中 设置过期时间1天
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(userByPhone), 1, TimeUnit.DAYS);
return Result.success(token);
}
UserService查看权限
@Override
public Result isLoginByPhone(String phoneNum) {
SysUser sysUser = getUserByPhone(phoneNum);
if (Objects.isNull(sysUser)){
return Result.fail(2500,"手机号不存在! 请先注册!");
}else if (sysUser.getAdmin() >= 1){
//只允许管理员登录
return Result.success("允许登录");
}
return Result.fail(500,"对不起,权限不足无法登陆!");
}
/**
* 通过手机号查询用户对象
* @param phoneNum
* @return
*/
@Override
public SysUser getUserByPhone(String phoneNum) {
QueryWrapper<SysUser> qw = new QueryWrapper<>();
qw.eq("mobile_phone_number",phoneNum);
SysUser sysUser = userMapper.selectOne(qw);
return sysUser;
}
补充:实现点击发送验证码120s倒计时功能
在表单中添加一个span 添加button 发送验证码 并且实现动态显示按钮内容
绑定实体类
动态文字显示
//验证码提示信息动态显示
getCodeText: function() {
if (this.wait_timer > 0) {
return "已发送";
}
if (this.wait_timer === 0) {
this.showNum = false;
return "重新发送验证码!";
}
if (this.wait_timer === false) {
return "发送验证码";
}
if (this.userForm.tele === '') {
return "手机号不能为空"
}
},
实现倒计时120s
//验证码等待
if (this.wait_timer > 0) {
return false;
}
this.showNum = true;
this.wait_timer = 120;
var that = this;
var timer_interval = setInterval(function() {
if (that.wait_timer > 0) {
that.wait_timer--;
} else {
learInterval(timer_interval);
}
}, 1000);
三、文章管理功能
1、实现批量删除
前端逻辑
要实现批量删除功能首先要在表单中添加selection类型的表格列,通过this.$refs.table.selection获取当前选中的整行数据,并对这个数据进行遍历,获取所有选中数据的id,存放在实例对象的数组中,请求后端携带这个id数组
后端逻辑
后端通过List集合接收到前端传递的json数组 ,对集合遍历删除
2、数据编辑实现表格数据回显
对当前行数据进行深拷贝即可获得回显数据
3、实现表格中动态显示不同数据的图片信息
四、图片上传功能
前端逻辑
使用elementUI中的上传标签,将当期选中的图片上传到服务器中,并通过方法接收返回的图片地址对象。
- action 图片提交地址
- :on-exceed="handleExceed" 图片上传超过限制的提示
- :auto-upload="false" 设置手动提交
- :before-upload 图片上传前的方法,对图片格式校验
- :on-success="handleAvatarSuccess" 图片上传成功的方法
//图片上传
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg';
const isPNG = file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG && !isPNG) {
this.$message.error('上传头像图片只能是 JPG或者PNG 格式!');
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!');
}
return (isJPG || isPNG) && isLt2M;
},
handleAvatarSuccess(res, file) {
// 上传图片成功后返回图片地址
this.imgUrl = res.data
},
handleExceed() {
this.$message.warning(`当前限制只能选择 1 张图片!`);
},
upload() {
//手动提交
this.$refs.upload.submit();
},
后端逻辑
在七牛云配置好存储空间,保留accessKey 账号,后端引入七牛云工具类,并调用上传方法baseUpload(),返回图片地址
@Component
public class QiniuUtils {
public static final String url = "http://cdn.zxlsc.top/";
//配置文件中写的accessKey账号
@Value("${qiniu.accessKey}")
private String accessKey;
@Value("${qiniu.accessSecretKey}")
private String accessSecretKey;
public boolean upload(MultipartFile file,String fileName){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.huabei());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
String bucket = "myblogssr";
//默认不指定key的情况下,以文件内容的hash值作为文件名
try {
byte[] uploadBytes = file.getBytes();
Auth auth = Auth.create(accessKey, accessSecretKey);
String upToken = auth.uploadToken(bucket);
Response response = uploadManager.put(uploadBytes, fileName, upToken);
//解析上传成功的结果
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
//防止冗余 直接写在工具类 Controller中调用
public Result baseUpload(MultipartFile file, String path){
//获取到文件名称 如a.jpg
String originalFilename = file.getOriginalFilename();
//唯一的文件名称
String fileName = path + UUID.randomUUID().toString() + "." + StringUtils.substringAfterLast(originalFilename,".");
//上传文件到哪呢? 上传到七牛云 将图片发放到离用户最近的服务器上,降低自身服务器的带宽消耗
boolean upload = upload(file, fileName);
if (upload){
return Result.success(QiniuUtils.url + fileName);
}
return Result.fail(20001,"上传失败");
}
}
五、在Vue中封装axios请求
1、在src中创建utils目录 创建request.js
在request中对axios进行二次封装,直接粘贴即可
//对axios二次封装
import axios from 'axios'
//对封装的axios方法起个名字
const request = axios.create({
//通用请求地址前缀
baseURL:'http://localhost:8989/api',
//请求的超时时间
timeout: 10000,
})
//添加请求拦截器
request.interceptors.request.use(function (config){
//在请求之前做什么
return config;
},function(error){
//对错误做点什么
return Promise.reject(error);
});
//添加响应拦截器
request.interceptors.response.use(function (response){
//对响应数据做点什么
return response;
},function (error){
//对响应错误做点什么
return Promise.reject(error);
});
//将这个常量暴露
export default request
2、在src下再创建不同接口请求的js文件
//从request中引入
import request from "../utils/request.js"
//将请求方法暴露
//获取所有文章列表
export function getArticle(data){
return request({
method:"post",
url:"/articles/list",
data:data
})
}
//获取文章总数
export function getCount(){
return request({
method:"get",
url:"/article"
})
}
3、在src下再创建一个index.js
此文件负责将所有的接口文件统一暴露出去直接使用
//按格式引入
import * as article from './article.js'
import * as user from'./user.js'
import * as tag from './tag.js'
import * as log from './log.js'
//负责将写好的请求暴露出去使用
export default {
article,
user,
tag,
log,
}
4、在main.js中引入这个api中的index.js
给暴露出去的方法设置全局属性 使得方法能够在全局调用
import api from './api/index.js'
//给这个api中的index起别名 后序调用直接使用this.$api
Vue.prototype.$api = api
5、调用方式
//原本使用axios方式
axios({
method:"post",
url:"http://localhost:8989/api/user"
data:data
}).then(res => {
//返回值
})
//使用封装的axios
//使用user.js中的getUser()请求
this.$api.user.getUser().then(res => {})
六、Echarts的基本引入
1、在Vue中导入Echarts
- 使用npm安装 echarts npm i echarts
- 在vue文件中导入echarts
- 在需要的标签内容设置 ref ---- vue提供的获取dom节点的方式
- 配置图形对象
- 将图形对象暴露出去
官网提供的Demo 描述了图形的基本属性设置
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
//当有多条数据时使用遍历的方式设置series属性
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
2、不同图形的属性
折线: series->type: "line"
柱状图: series->type: "bar"
柱状图与折线图只有type不同其他相同
饼状图:series->type: "pie" radius:"50%" 其中对于饼状图不需要设置x,y轴 他也可以设置其他属性
3、补充 Echarts取数据
对于Echarts的使用难点在于如何对后端返回的数据进行筛选,将不同的数据提取出来对图形图像进行设置,只要将数据取出并赋值就可以完成Echarts的使用
所以在这里提供几个取数据的方法
数据类型 let obj = {"www":23,"ww":78}
数据类型为Array可以直接通过[0]的方式 通过索引位置获取
1、在js对象中获取所有的key值
Object.keys(obj)
2、在js对象中获取所有的value值
Object.values(obj)
3、将js对象转为集合类型使其能够通过索引取数据
Object.entries(obj2)
4、Echarts问题记录
1、x轴数据与y轴数据的位置不匹配
在x轴中添加一个属性
lineOption.xAxis = {
data:Object.keys(orderData[0]) ,
//x轴数据自动调节大小
axisLabel:{
interval:0
}
}
2、当x轴数据超过指定长度时变为省略号
lineOption.xAxis = {
data: xAxis,
axisLabel: {
formatter: function(value) {
if (value.length > 6) {
return `${value.slice(0,5)}...`;
}
return value
}
}
}
七、使用SpringBoot的拦截器拦截特定url
使用SpringBoot拦截器将特定url请求拦截 实现一个简单的 记录网站访问次数的功能,
1、创建Iterceptor
@Component
@SuppressWarnings({"all"})
public class URLInterceptor implements HandlerInterceptor {
/**
* 请求前置处理(后置处理同理)
*
* @param request
* @param response
* @param handler
* @return boolean
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,@Nullable ModelAndView modelAndView) throws Exception {
//获取前端请求的接口地址
String path = request.getServletPath();
//对特定的接口进行设置
if ("/articles/list".equals(path)) {
//后置处理
//对访问这个路径后执行的操作
}
}
2、在WebMvc中注入并添加
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
//注入
@Autowired
private URLInterceptor urlInterceptor;
//重写方法 将拦截器注入
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(urlInterceptor);
}
}
八、实现账户退出并清空token
注意退出登录并清空token后应该返回登录页面
1、在main.js中添加导航路由守卫
当不存在token时会立马跳转到登录页
//添加路由守卫
router.beforeEach((to,from,next) => {
//判断token是否存在 从Cookie读取
const token = Cookie.get('token')
if(!token && to.name !== 'login'){
//token不存在 说明当前用户未登录 应该跳转未登录页面
//name 是路由配置中指定路由的名称
next({ name: 'login' })
}else if(token && to.name === 'login'){
//token存在 说明登录 此时跳转首页
// next({ name:from.name })
next({ name:'home' })
}else{
next()
}
})
2、退出逻辑
前端逻辑
后端逻辑
九、对前后端的数据请求进行拦截
对前端发送的请求统一封装数据,将当前登录的账户的token放在请求头中,在发送给后端的时候使用拦截器对请求进行拦截,执响应前拦截,在请求之前判断,当前的请求 请求头中是否含有token,并且校验这个token是否合法,如果不合法说明,有用户直接使用浏览器中设置token试图发送请求,所以需要进行安全校验
后端拦截器逻辑
前端逻辑
在 request.js中设置响应拦截器 对后端返回的错误状态码进行拦截
后端逻辑
@Component
public class HeaderInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//先判断是否在登录界面 防止登录拦截
//获取请求路径
String path = request.getServletPath();
//对登录界面的请求进行放行
if ("/login".equals(path) || "/code".equals(path) || "/users/judgephone".equals(path) || "/users/token".equals(path)){
return true;
}
//这个一定要加否则报错 对预检请求放行
//查看是否是预检请求
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
System.out.println("OPTIONS请求,放行");
return true;
}
//获取请求头中的token 前端设置存储在Authorization
String token = request.getHeader("Authorization");
//当前没有token
if (token == ""){
//给前端响应
//设置响应码
response.setStatus(438);
return false;
}
Result result = null;
try {
result = userService.getUserByToken(token);
} catch (Exception e) {
//报错说明token不合法
//给前端响应
response.setStatus(438);
return false;
}
if (!result.isSuccess()){
response.setStatus(438);
return false;
}
return true;
}
}
在WebMVCConfig中注册
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private HeaderInterceptor headerInterceptor;
//登录验证
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(headerInterceptor).addPathPatterns("/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
//进行跨域配置
//前端占用8080 后端占用8888
//两个端口之间的访问就是跨域
//防止跨域报错
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}