1. 回顾
1. Vue通过脚手架创建Vue工程。
1.组件【网页】--->组件【父组件】可以引用另一个组件[子组件].父组件怎么传参给子组件
2.路由:
[1]路由跳转
(<router-link to="/路由路径"></router-link>) this.$router.push("/路由路径")
路由配置: {path:"",component:""}
路由渲染: <router-view/> 理解:<iframe/>
2. 权限系统
权限系统:
1.前端使用: vue + elementui + axios + css + html
2.后端使用: springboot+mybatis-plus +mybatis+druid+shiro+swagger2+redis
3. 完成登录
3.1. 前端布局
(1)Login.vue组件
<template>
<!--这里必须使用一个双标签-->
<div id="building">
<div class="login-container">
<el-form :model="ruleForm" :rules="rules"
status-icon
width="200px"
ref="ruleForm"
class="demo-ruleForm login-page">
<div class="block"><el-avatar :size="100" :src="circleUrl"></el-avatar></div>
<p class="denglu" style="font-family: 幼圆;font-size: 30px ">用户登录</p>
<hr style="background-color: mintcream; overflow:hidden; "/>
<p style="font-family: 幼圆;font-size: 16px; color:white">User login</p>
<el-form-item label="" prop="username">
<el-input type="text"
v-model="ruleForm.username"
auto-complete="off"
placeholder="请输入账号"
></el-input>
</el-form-item>
<el-form-item label="" prop="password">
<el-input type="password"
v-model="ruleForm.password"
auto-complete="off"
placeholder="请输入密码"
@keyup.enter.native="handleSubmit"
></el-input>
</el-form-item>
<el-form-item style="width:100%;">
<el-button type="primary" style="width:100px;background-color: #348C77" @click="handleSubmit" :loading="logining">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data(){
return{
logining: false,
//绑定表单数据
ruleForm: {
username:'',
},
//表单验证规则
rules: {
username: [
{required: true, message: '请输入账号,账号不能为空', trigger: 'blur'},
//{ type: 'number', message: '账号必须为数字值'},
],
password: [
{required: true, message: '请输入密码,密码不能为空', trigger: 'blur'},
{min: 6, max: 12, message: "密码的长度必须{6~12}", trigger: "blur"},
]
},
checked: false,
//头像绑定
circleUrl:'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
}
},
methods:{
handleSubmit(){
//表单校验
this.$refs['ruleForm'].validate((valid) => {
if (valid) {
this.$router.push("/home")
}
})
}
}
}
</script>
<style>
body{
padding:0px;
margin:0px;
}
/* 登录文字 */
.denglu {
color: white;
font-size: 20px;
}
.login-container {
width: 450px;
height: 200px;
margin:auto auto;
}
.login-page {
/* 背景图片 */
/*background-image: url(/imgs/);*/
background-size: 100% 100%;
/*background-color: #3CA993; */
margin-top: 10rem;
border-radius: 12px;
/* 内边距 */
padding: 20px;
text-align: center;
/*box-shadow: 0 8px 5px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);*/
}
label.el-checkbox.rememberme {
margin: 0px 0px 25px;
text-align: left;
}
#building {
background: url("../assets/login.jpg");
width: 100%;
height: 100%;
position: fixed;
background-size: 100% 100%;
}
</style>
(2)配置路由
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '../views/Login'
Vue.use(VueRouter)
const routes = [
{
/*路由路径为/则重定向到/login路由*/
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: Login
//component: ()=>import("../views/Login.vue")
},
]
const router = new VueRouter({
//删除地址栏的#
mode: 'history',
routes
})
export default router
(3)登录按钮事件
如果想在vue工程中使用axios进行异步请求,则需要在main.js中导入axios
[1]//导入axiosimport axios from "axios";
[2]//把axios挂载到vue对象中,以后在vue中如果使用axios直接可以用$http名称,可以随意起名
Vue.prototype.$http=axios
methods:{
handleSubmit(){
//表单校验
this.$refs['ruleForm'].validate((valid) => {
if (valid) {
//this.$http就是自己刚刚定义的axios
//url:后端登录接口的路径 //es=>函数
this.$http.post("http://localhost:8081/system/login",this.ruleForm).then(result=>{
})
}
})
}
}
3.2. 完成后端登录接口
(1) 创建springboot项目
pox.xml文件依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wzh</groupId>
<artifactId>springboot-vue0808</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-vue0808</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.9.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
(2) 使用mybatis-plus的代码生成器
package com.wzh;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.Collections;
/**
* @ProjectName: springboot-vue0808
* @Package: com.wzh
* @ClassName: Generator
* @Author: 王振华
* @Description: mp代码生成器
* @Date: 2022/8/8 18:33
* @Version: 1.0
*/
public class Generator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/shiro_permission?serverTimezone=Asia/Shanghai", "root", "123456")
.globalConfig(builder -> {
builder.author("王振华") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir(".\\src\\main\\java\\"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.wzh") // 设置父包名
.moduleName("system") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "src\\main\\resources\\mapper\\")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("acl_user","acl_role","acl_permission")// 设置需要生成的表名
.addTablePrefix("acl_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
(3)配置application文件
#端口号
server.port=8081
#druid数据源
spring.datasource.druid.url=jdbc:mysql://localhost:3306/shiro_permission?serverTimezone=Asia/Shanghai
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
#初始化的个数
spring.datasource.druid.initial-size=5
# 最大活跃数
spring.datasource.druid.max-active=10
# 最大等待时间
spring.datasource.druid.max-wait=3000
# 最小的闲置个数
spring.datasource.druid.min-idle=5
#mybatis-plus日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
(4)登录接口
package com.wzh.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.User;
import com.wzh.system.service.IUserService;
import com.wzh.system.vo.CommonResult;
import com.wzh.system.vo.LoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @ProjectName: springboot-vue0808
* @Package: com.wzh.system.controller
* @ClassName: LoginController
* @Author: 王振华
* @Description:登录接口
* @Date: 2022/8/8 18:47
* @Version: 1.0
*/
@RestController
@RequestMapping("system")
@Api(tags = "登录的接口类")
//@CrossOrigin //解决跨域问题
public class LoginController {
@Autowired
private IUserService userService;
@PostMapping("/login")
@ApiOperation(value = "登录接口")
public CommonResult login(@RequestBody LoginVo loginVo){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",loginVo.getUsername());
wrapper.eq("password",loginVo.getPassword());
wrapper.eq("is_deleted",0);
User user = userService.getOne(wrapper);
if(user!=null){
return new CommonResult(2000,"登录成功",null);
}else{
return CommonResult.LOGIN_ERROR;
}
}
}
vo包:它也是实体类的一种; view obeject 视图对象。 作用:接受和响应网页的对象,
如果用map接收的话,无法使用swagger注解
前端调用后端登录接口时出现如下的错误
当使用异步请求从一个网址访问另一个网址时可能会出现跨域问题。
前提:
1. 必须为异步请求
2. 当端口号或协议或ip不同时则会出现跨域
出现两个请求: 有一个请求的方式为: OPTIONS 和真实的请求方式
理解: OPTIONS先头部队。---探视后台有没有解决跨域。
如何解决跨域:
1.前端解决
2.后端解决---->这里也有几种方式:
【1】可以借助nginx.
【2】在代码中解决 (重点)
在控制层接口上添加@CrossOrigin
@CrossOrigin(origins = {"192.168.1.14:8081","192.168.2.34:8080"},allowedHeaders="运行哪些请求头跨域",methods={RequestMethod.GET,RequestMethod.POST})
(origins = {"192.168.0.111:8080","192.168.0.120:8081"},allowedHeaders="运行哪些请求头跨域",methods={"GET","POST"})
origins: 允许哪些域可以跨域访问我这个接口
allowedHeaders:允许哪些请求头信息跨域
methods: 允许哪些请求方式跨域
上面在控制层接口处加上注解的方式解决跨域,麻烦的地方就需要对每个控制类都加该注解。 设置一个全局跨域配置类。
package com.wzh.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
// 当前跨域请求最大有效时长。这里默认1天
private static final long MAX_AGE = 24 * 60 * 60;
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
corsConfiguration.setMaxAge(MAX_AGE);
source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
return new CorsFilter(source);
}
}
(5)加入swagger配置类
package com.wzh.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.VendorExtension;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
/**
* @Author 闫克起
* @Date 2021/4/29 16:37
* @Version 1.0
*/
@Configuration
public class SwaggerConfig {
//获取swagger2的实例对象docket
@Bean
public Docket getDocket() {
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("QY151")
.apiInfo(apiInfo())
.select()//设置哪些包下的类生产api接口文档
.apis(RequestHandlerSelectors.basePackage("com.wzh.system.controller"))
//设置哪些请求路径生产接口文档
.paths(PathSelectors.any())
.build();
return docket;
}
private ApiInfo apiInfo() {
Contact DEFAULT_CONTACT = new Contact("王振华", "http://www.bing.com", "1430930278@qq.com");
ApiInfo apiInfo = new ApiInfo("员工管理系统API接口文档", "员工管理系统API接口文档", "1.0", "http://www.bing.com",
DEFAULT_CONTACT, "Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0", new
ArrayList<VendorExtension>());
return apiInfo;
}
}
(6)登录成功后前端路由跳转
4. 登录的bug
上面咱们写的登录,后端没有保存数据 前端也没有拿到数据进行保存
要用redis的原因是因为如果高并发访问数据库数据库会崩溃,造成系统瘫痪
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application配置文件
#redis的配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=20000
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5
(1)修改登录的接口
package com.wzh.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.User;
import com.wzh.system.service.IUserService;
import com.wzh.system.vo.CommonResult;
import com.wzh.system.vo.LoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @ProjectName: springboot-vue0808
* @Package: com.wzh.system.controller
* @ClassName: LoginController
* @Author: 王振华
* @Description:登录接口
* @Date: 2022/8/8 18:47
* @Version: 1.0
*/
@RestController
@RequestMapping("system")
@Api(tags = "登录的接口类")
//@CrossOrigin(origins = {"192.168.1.14:8081","192.168.2.34:8080"},allowedHeaders="运行哪些请求头跨域",methods={RequestMethod.GET,RequestMethod.POST}) //解决跨域问题
public class LoginController {
@Autowired
private IUserService userService;
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/login")
@ApiOperation(value = "登录接口")
public CommonResult login(@RequestBody LoginVo loginVo){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",loginVo.getUsername());
wrapper.eq("password",loginVo.getPassword());
wrapper.eq("is_deleted",0);
User user = userService.getOne(wrapper);
if(user!=null){
//随机生成一个唯一字符串。
String token = UUID.randomUUID().toString();
//把该token作为redis的key value为当前登录用户信息
ValueOperations forValue = redisTemplate.opsForValue();
forValue.set(token,user,24, TimeUnit.HOURS);
return new CommonResult(2000,"登录成功",token);
}else{
return CommonResult.LOGIN_ERROR;
}
}
}
RedisTemplate类需要序列化,加入一个配置类
package com.wzh.system.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* @program: qy151-redis-springboot
* @description:
* @author: 王振华
* @create: 2022-08-08 15:16
**/
@Configuration
public class RedisConfig {
//这个是用来自己创建的RedisTemplate
/* @Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化 filed value
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(redisSerializer);
return template;
}
}
(2)修改前端登录方法
后面每次请求都可以携带该token,
每次请求都得要人为添加参数token. 我们可以使用axios的请求拦截器。
验证token有没有被使用
<template>
<div>
<el-button type="primary" @click="getInfo">获取用户信息</el-button>
</div>
</template>
<script>
export default {
name: "User",
methods:{
getInfo(){
this.$http.get("http://localhost:8081/system/user/getInfo").then(result=>{
console.log(result)
})
}
}
}
</script>
<style scoped>
</style>
接口:
package com.wzh.system.controller;
import com.wzh.system.entity.User;
import com.wzh.system.vo.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* <p>
* 用户表 前端控制器
* </p>
*
* @author 王振华
* @since 2022-08-08
*/
@RestController
@RequestMapping("/system/user")
public class UserController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("getInfo")
public CommonResult getInfo(HttpServletRequest request) {
String token = request.getHeader("token"); //获取请求头
System.out.println(token);
//根据token从redis中获取用户信息
ValueOperations forValue = redisTemplate.opsForValue();
User o = (User) forValue.get(token);
return new CommonResult(2000, "获取用户信息成功", o);
}
}
这时发现报500 原因我们的实体类有两个时间类型
解决办法:
<!--日期序列化-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.3</version>
</dependency>
类属性上加入注解
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
运行程序成功
5.前置路由守卫
前置路由守卫:就是在路由跳转前加上自己得一些业务代码,在main.js中配置。类似于拦截器
//设置前置路由守卫 to:到哪个路由 from:从哪个路由来 next():放行到指定路由
router.beforeEach((to,from,next)=>{
//获取跳转得路径
var path = to.path;
//判断是否为登录路由路径
if(path==="/login"){
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
//放行
return next();
}
//其他路由路径 判断是否登录过
var token = sessionStorage.getItem("token");
if(token){
return next();
}
//跳转登录
return next("/login");
})
6.整合shiro
(1)添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
(2)shiro的配置类
package com.wzh.system.config;
import com.wzh.system.filter.LoginFilter;
import com.wzh.system.realm.MyRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.Filter;
import java.util.HashMap;
/**
* @ProjectName: springboot-shiro-swagger
* @Package: com.wzh.config
* @ClassName: ShiroConfig
* @Author: 王振华
* @Description:
* @Date: 2022/8/5 17:32
* @Version: 1.0
*/
@Configuration
public class ShiroConfig {
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
return securityManager;
}
@Bean
public Realm realm() {
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(credentialsMatcher());
return myRealm;
}
@Bean
public CredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1024);
return credentialsMatcher;
}
@Autowired
private RedisTemplate redisTemplate;
@Bean(value = "shiroFilter")
public ShiroFilterFactoryBean filterFactoryBean() {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager());
//设置拦截规则
HashMap<String, String> map = new HashMap<>();
//放行路径
map.put("/system/login", "anon");
map.put("/doc.html", "anon");
map.put("/swagger-ui.html", "anon");
map.put("/swagger/**", "anon");
map.put("/webjars/**", "anon");
map.put("/swagger-resources/**", "anon");
map.put("/v2/**", "anon");
map.put("/static/**", "anon");
//拦截路径
map.put("/**", "authc");
map.put("/login/logout", "logout");
filterFactoryBean.setFilterChainDefinitionMap(map);
//设置自定义认证过滤器
HashMap<String, Filter> filterMap = new HashMap<String, Filter>();
filterMap.put("authc", new LoginFilter(redisTemplate));
filterFactoryBean.setFilters(filterMap);
return filterFactoryBean;
}
@Bean //注册filter
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setName("shiroFilter");
filterRegistrationBean.setFilter(new DelegatingFilterProxy());
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
//开始shiro注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public FilterRegistrationBean vv() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
//设置字符编码
characterEncodingFilter.setEncoding("UTF-8");
//设置强制使用指定字符编码
characterEncodingFilter.setForceEncoding(true);
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(characterEncodingFilter);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
(3)增加一个realm类对象
package com.wzh.system.realm;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.Permission;
import com.wzh.system.entity.User;
import com.wzh.system.service.IPermissionService;
import com.wzh.system.service.IRoleService;
import com.wzh.system.service.IUserService;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* @ProjectName: springboot-vue0808
* @Package: com.wzh.system.realm
* @ClassName: MyRealm
* @Author: 王振华
* @Description:
* @Date: 2022/8/9 9:47
* @Version: 1.0
*/
public class MyRealm extends AuthorizingRealm {
@Autowired
private IUserService userService;
@Autowired
private IPermissionService permissionService;
@Autowired
private IRoleService roleService;
//当你进行权限校验时会执行该方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/* User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//根据账号查找该用户具有哪些权限
QueryWrapper<Permission>
List<String> list = permissionService.getOne(user.get());
if(list!=null&&list.size()>0){
info.addStringPermissions(list);
}
List<String> roles = roleService.findRolesById(user.getUserid());
if(roles!=null&&roles.size()>0){
info.addRoles(roles);
}*/
return null;
}
//该方法用于完成认证的功能
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.根据token获取账号
String username = (String) token.getPrincipal();
/**
* 以前登陆的逻辑是 把用户和密码全部发到数据库 去匹配
* 在shrio里面是先根据用户名把用户对象查询出来,再来做密码匹配
*/
//2.根据账号查询用户信息
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
wrapper.eq("is_deleted",0);
User user = userService.getOne(wrapper);
//表示该用户名在数据库中存在
if(user!=null){
/**
* 参数说明
* 参数1:可以传到任意对象
* 参数2:从数据库里面查询出来的密码
* 参数3:盐
* 参数4:当前类名
*/
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),credentialsSalt,this.getName());
return info;
}
return null;
}
}
(4) 修改controller代码
package com.wzh.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.User;
import com.wzh.system.service.IUserService;
import com.wzh.system.vo.CommonResult;
import com.wzh.system.vo.LoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @ProjectName: springboot-vue0808
* @Package: com.wzh.system.controller
* @ClassName: LoginController
* @Author: 王振华
* @Description:登录接口
* @Date: 2022/8/8 18:47
* @Version: 1.0
*/
@RestController
@RequestMapping("system")
@Api(tags = "登录的接口类")
//@CrossOrigin(origins = {"192.168.1.14:8081","192.168.2.34:8080"},allowedHeaders="运行哪些请求头跨域",methods={RequestMethod.GET,RequestMethod.POST}) //解决跨域问题
public class LoginController {
@Autowired
private IUserService userService;
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/login")
@ApiOperation(value = "登录接口")
public CommonResult login(@RequestBody LoginVo loginVo){
try{
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginVo.getUsername() ,loginVo.getPassword());
subject.login(usernamePasswordToken);
Object user = subject.getPrincipal();
//随机生成一个唯一字符串。
String token = UUID.randomUUID().toString();
//把该token作为redis的key value为当前登录用户信息
ValueOperations forValue = redisTemplate.opsForValue();
forValue.set(token,user,24, TimeUnit.HOURS);
return new CommonResult(2000,"登录成功",token);
}catch (Exception e){
e.printStackTrace();
return CommonResult.LOGIN_ERROR;
}
}
}
测试登录
登录成功后获取用户信息时出现如下得错误
被shiro的拦截器给拦截了。
package com.wzh.system.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wzh.system.vo.CommonResult;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
/**
* @ProjectName: springboot-vue0808
* @Package: com.wzh.system.filter
* @ClassName: LoginFilter
* @Author: 王振华
* @Description:
* @Date: 2022/8/9 9:46
* @Version: 1.0
*/
//如果类没有交于spring容器来管理 那么该类中得属性也不能让spring帮你注入
public class LoginFilter extends FormAuthenticationFilter {
private RedisTemplate redisTemplate; //LoginFilter必须交于spring容器来管理。
public LoginFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
//当登录成功后执行得方法,如果该方法返回false,则执行onAccessDenied
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
//1.请求方式是否为OPTIONS
String options = req.getMethod();
if(options!=null && options.equals("OPTIONS")){
return true;
}
String token = req.getHeader("token");
//token可以伪造,所以也要查询redis里有没有该记录
if(token!=null && redisTemplate.hasKey(token)){
return true;
}
return false;
}
//未登录时调用该方法? 为什么进入没有登录方法:
// --->第一个请求是OPTIONS,没有携带token 第二个因为前端和后端不是用得同一个session.默认shiro以sessionId为是否登录得标准
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
CommonResult commonResult = CommonResult.UNLOGIN;
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(commonResult);
writer.print(json);
writer.flush();
writer.close();
return false;
}
}
7.主页的布局
<template>
<el-container>
<el-header>
<span id="logo" style="display: inline-block;width: 30%;height: 100%;float: left" >
<a href="https://www.bilibili.com/video/BV14g41197PY/"><img src="../assets/logo.png" height="100%" width="180px"></a>
</span>
<span id="avatar" style="float: right">
<el-dropdown @command="handleCommand">
<span class="el-dropdown-link" style="margin-top: 10px; display: inline-block;">
<el-avatar ></el-avatar>
</span>
<el-dropdown-menu slot="dropdown" style="margin-top: -10px">
<el-dropdown-item command="info">个人信息</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
</el-header>
<el-container>
<el-aside width="200px">
</el-aside>
<el-main>
</el-main>
</el-container>
<el-footer>Footer</el-footer>
</el-container>
</template>
<script>
export default {
name: "Home",
methods:{
getInfo(){
this.$http.get("/system/user/getInfo").then(result=>{
console.log(result)
})
},
}
}
</script>
<!--当前vue有效-->
<style>
html,body,#app{
height: 100%;
}
body,#app{
padding: 0px;
margin:0px;
}
.el-container{
height: 100%;
}
.el-header, .el-footer {
background-color: #1F272F;
color: #333;
line-height: 60px;
}
.el-aside {
background-color: #545c64;
color: #333;
line-height: 560px;
}
.el-aside>.el-menu{
border: none;
}
.el-main {
background-color: #E9EEF3;
color: #333;
line-height: 560px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
</style>
7.1. 退出
前端:
由于每个跳转都需要写ip和端口,我们可以在main.js中设置axios的基础路径
//设置axios基础路径 axios.defaults.baseURL="http://localhost:8082"
后端:
@GetMapping("/logout")
public CommonResult logout(HttpServletRequest request){
String token = request.getHeader("token");
if(redisTemplate.hasKey(token)){
redisTemplate.delete(token);
return new CommonResult(2000,"退出成功",null);
}
return new CommonResult(5000,"请先登录",null);
}
7.2. 查询左侧菜单
(1)前端
<template>
<el-container>
<el-header>
<span id="logo" style="display: inline-block;width: 30%;height: 100%;float: left" >
<a href="https://www.bilibili.com/video/BV14g41197PY/"><img src="../assets/logo.png" height="100%" width="180px"></a>
</span>
<span id="avatar" style="float: right">
<el-dropdown @command="handleCommand">
<span class="el-dropdown-link" style="margin-top: 10px; display: inline-block;">
<el-avatar ></el-avatar>
</span>
<el-dropdown-menu slot="dropdown" style="margin-top: -10px">
<el-dropdown-item command="info">个人信息</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
</el-header>
<el-container>
<el-aside width="200px">
<el-menu
default-active="2"
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b">
<el-submenu :index="menu.id+''" v-for="menu in leftMenus">
<template slot="title">
<i :class="menu.icon"></i>
<span>{{menu.name}}</span>
</template>
<el-menu-item :index="second.id+''" v-for="second in menu.children">
<i :class="second.icon"></i>
<span><a style="color: white;text-decoration: none"
>{{second.name}}</a></span>
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<el-main>
</el-main>
</el-container>
<el-footer>Footer</el-footer>
</el-container>
</template>
<script>
export default {
name: "Home",
data(){
return{
leftMenus:[]
}
},
methods:{
initLeftMenu(){
this.$http.get("/system/permission/leftMenu").then(result=>{
if(result.data.code===2000){
this.leftMenus=result.data.data;
}
})
},
getInfo(){
this.$http.get("/system/user/getInfo").then(result=>{
console.log(result)
})
},
//下拉的触发事件
handleCommand(command) {
if (command === 'logout') {
this.$http.get("/system/logout").then(result => {
if (result.data.code === 2000) {
console.log("11111")
sessionStorage.clear();
//sessionStorage.removeItem("token");
this.$router.push("/login")
}
})
}
}
}
}
</script>
<!--当前vue有效-->
<style>
html,body,#app{
height: 100%;
}
body,#app{
padding: 0px;
margin:0px;
}
.el-container{
height: 100%;
}
.el-header, .el-footer {
background-color: #1F272F;
color: #333;
line-height: 60px;
}
.el-aside {
background-color: #545c64;
color: #333;
line-height: 560px;
}
.el-aside>.el-menu{
border: none;
}
.el-main {
background-color: #E9EEF3;
color: #333;
line-height: 560px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
</style>
(2)后端:
@RestController
@RequestMapping("/system/permission")
public class PermissionController {
@Autowired
private IPermissionService permissionService;
@GetMapping("leftMenu")
public CommonResult leftMenu(HttpServletRequest request){
String token = request.getHeader("token");
return permissionService.findPermissionByUserId(token);
}
}
service
package com.wzh.system.service.impl;
import com.wzh.system.entity.Permission;
import com.wzh.system.entity.User;
import com.wzh.system.mapper.PermissionMapper;
import com.wzh.system.service.IPermissionService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wzh.system.vo.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 权限 服务实现类
* </p>
*
* @author 王振华
* @since 2022-08-09
*/
@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements IPermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private RedisTemplate redisTemplate;
@Override
public CommonResult findPermissionByUserId(String token) {
//根据token获取用户信息
ValueOperations forValue = redisTemplate.opsForValue();
User user = (User) forValue.get(token);
//根据用户id查询该用户具有的权限。
List<Permission> permissionList = permissionMapper.selectByUserId(user.getId());
//设置层级关系
List<Permission> firstMenus = new ArrayList<>();
for (Permission firstMenu : permissionList){
if(firstMenu.getPid().equals("1")){
firstMenus.add(firstMenu);
}
}
//为一级菜单设置二级菜单
for (Permission first : firstMenus){
//根据一级菜单id 查询 该菜单得二级菜单。如果出现不确定有几级菜单 那么我们可以使用方法得递归调用
first.setChildren(findChildren(permissionList, first.getId()));
}
return new CommonResult(2000,"查询成功",firstMenus);
}
//方法递归
private List<Permission> findChildren(List<Permission> permissionList, String pid) {
List<Permission> children = new ArrayList<>();
for (Permission p : permissionList){
if(p.getPid().equals(pid)){
children.add(p);
}
}
for(Permission child : children){
child.setChildren(findChildren(permissionList,child.getId()));
}
return children;
}
}
实体类添加列
推荐使用下一篇文章的,用到了递归,不管几级菜单都可以