功能介绍:登录成功后,用户页面1分钟无操作返回登录页面,包括新增等按钮
效果:
vue登录
首先导jar包:
<!-- jwt jar 包-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
<!-- aop jar-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties配置文件:
#用户
spring.datasource.username=root
#密码
spring.datasource.password=123123
#连接是数据库
spring.datasource.url=jdbc:mysql://localhost:3306/jin1?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#打印Sql
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#扫描.xml文件
mybatis-plus.mapper-locations=classpath*:xml/*.xml
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端?
spring.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=PT10S
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=30
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=1
#链接超时时间
spring.redis.timeout=PT10S
定义登录的controller:
package com.ff.controller;
import com.ff.common.ResultCode;
import com.ff.common.ResultObj;
import com.ff.entity.User;
import com.ff.service.UserService;
import com.ff.util.JWTUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/login")
@CrossOrigin //跨域
public class LoginController {
@Autowired
private UserService userService;
private static final String ACCESS_TOKEN="ACCESS_TOKEN";
@Autowired
private RedisTemplate redisTemplate;
//登录
@PostMapping("login")
public ResultObj login(String username,String password) {
//判断用户名密码为空
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
//ResultObj统一返回值
return ResultObj.error(ResultCode.USERNAME_PASSWORD_ISNULL);
}
//判断用户名是否存在
User user = userService.queryUserByUsername(username);
if (user == null) {
return ResultObj.error(ResultCode.USER_NOEXIST);
}
//判断密码是否正确
if (!password.equals(user.getPassword())) {
return ResultObj.error(ResultCode.PASSWORD_ERROR);
}
//登录成功,生成token
String token = JWTUtil.createToken(user);
//获取当前时间的毫秒值
String currentTime = String.valueOf(System.currentTimeMillis());
//拼接存到redis中的Key中
String accessKey=ACCESS_TOKEN+user.getId()+":"+token;
//往拼接存到redis中的Key存value值
redisTemplate.opsForValue().set(accessKey,currentTime);
//设置redis key的过期时间 2分钟
redisTemplate.expire(accessKey,2, TimeUnit.MINUTES);
return ResultObj.success(token);
}
}
定义统一返回值类:
package com.ff.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultObj {
//状态码
private Integer code;
//信息
private String msg;
//数据
private Object data;
public static ResultObj success(Object data){
return new ResultObj(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMsg(),data);
}
public static ResultObj error(ResultCode resultCode){
return new ResultObj(resultCode.getCode(),resultCode.getMsg(),null);
}
}
定义一个enum类:
package com.ff.common;
public enum ResultCode {
SUCCESS(200,"操作成功"),
ERROR(500,"操作失败"),
USERNAME_PASSWORD_ISNULL(1001,"用户名或密码为空"),
USER_NOEXIST(1002,"用户不存在"),
PASSWORD_ERROR(1003,"密码错误"),
TOKEN_ERROR(1004,"登录失败")
;
private Integer code;
private String msg;
ResultCode(Integer code,String msg){
this.code=code;
this.msg=msg;
}
public Integer getCode(){
return code;
}
public void setCode(Integer code){
this.code=code;
}
public String getMsg(){
return msg;
}
public void setMsg(String msg){
this.msg=msg;
}
}
定义一个切面类:
package com.ff.common;
import com.ff.annotation.LoginAnnotation;
import com.ff.util.JWTUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@Component
@Aspect
public class LoginAop {
@Resource
private HttpServletRequest request;
private static final String ACCESS_TOKEN="ACCESS_TOKEN";
@Autowired
private RedisTemplate redisTemplate;
//扫描controller层所有自定义注解
//loginAnnotation自定义注解
@Around(value = "execution(* com.ff.controller.*.*(..)) && @annotation(loginAnnotation)")
public Object loginAround(ProceedingJoinPoint joinPoint, LoginAnnotation loginAnnotation) {
Object proceed=null;
//验证token的值
String token = request.getHeader("Authorization-token");
ResultObj resultObj = JWTUtil.verifToken(token);
//失败的情况
if(resultObj.getCode()!= 200){
return resultObj;
}
//获取data中数据
Claims claims = (Claims)resultObj.getData();
//拼接存到redis中的Key中
String accessKey=ACCESS_TOKEN+claims.get("id")+":"+token;
//验证redis中的token得值是否存在
if(!redisTemplate.hasKey(accessKey)){
//登录失败
return ResultObj.error(ResultCode.TOKEN_ERROR);
}
//如果redis中的token得值存在,续签
//获取当前时间的毫秒值
String currentTime = String.valueOf(System.currentTimeMillis());
//往拼接存到redis中的Key存value值
redisTemplate.opsForValue().set(accessKey,currentTime);
//设置redis key的过期时间 2分钟
redisTemplate.expire(accessKey,2, TimeUnit.MINUTES);
try {
//执行目标方法
proceed = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
定义自定义注解:
package com.ff.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginAnnotation {
}
定义自定义注解:JWTUtil工具类:
package com.ff.util;
import com.ff.common.ResultCode;
import com.ff.common.ResultObj;
import com.ff.entity.User;
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtil {
//生成Toke的方法
public static String createToken(User user) {
//token分为3部分
//第一部分我们称它为头部(header),
// 第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),
// 第三部分是签证(signature).
//头部
Map<String, Object> headerMap = new HashMap<String, Object>();
//HS256加密方式
headerMap.put("alg", "HS256");
headerMap.put("type", "JWT");
//有效载荷
Map<String, Object> payloadMap = new HashMap<String, Object>();
payloadMap.put("username", user.getUsername());
payloadMap.put("userid", user.getPassword());
//失效时间 s设置不过期的token
long timeMillis = System.currentTimeMillis();
//设置token时间 999999999 毫秒=11.5740740625 天
long endTime = timeMillis + 999999999 ;
//签证,签名
String token = null;
try {
token = Jwts.builder()
.setHeader(headerMap)
.setClaims(payloadMap)
.setExpiration(new Date(endTime))
.signWith(SignatureAlgorithm.HS256, "rtet")
.compact();
System.out.println(token);
} catch (ExpiredJwtException e) {
e.getClaims();
}
return token;
}
//解密token
public static ResultObj verifToken(String token) {
try {
//通过TWTs的parser 方法 接受
//根据人名key解密
Claims claims = Jwts.parser().setSigningKey("rtet")
.parseClaimsJws(token)
.getBody();
return ResultObj.success(claims);
} catch (Exception e) {
return ResultObj.error(ResultCode.TOKEN_ERROR);
}
}
}
最后使用俩个类序列化Redis参数,不然乱码
package com.ff.cache;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (null == t) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (null == bytes || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return (T) JSON.parseObject(str, clazz);
}
}
package com.ff.cache;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
com.ff.cache.FastJsonRedisSerializer fastJsonRedisSerializer = new com.ff.cache.FastJsonRedisSerializer(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
//除了登录方法和上传图片方法,其他请求方法上都加自定义注解,
//注=登录方法不要加,不然登录不是直接拦截
前端=======================================
Login.Vue页面代码:
<template >
<div class="login">
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm aaa" >
<el-form-item label="用户名" prop="username">
<el-input type="text" v-model="ruleForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="password">
<el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @keyup.enter="keyDown" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "Login",
data(){
return {
ruleForm: {
password: '',
username: ''
},
rules: {
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
],
username: [
{ required: true, message: '请输入用户名称', trigger: 'blur' },
{ min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
]
}
}
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
var _this=this;
this.$axios.post("http://localhost:8080/login/login",this.$qs.stringify(this.ruleForm)).then(function (result) {
if(result.data.code!=200){
_this.$message.error(result.data.msg)
}else{
//将token放到localStorage
localStorage.setItem("token",result.data.data);
//跳转首页路径
_this.$router.push("/main");
}
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
},
keyDown(e){
//如果是回车则执行登录方法
if(e.keyCode == 13){
//需要执行的方法
this.submitForm('ruleForm');
}
}
},
mounted () {
//绑定事件
window.addEventListener('keydown',this.keyDown);
},
//销毁事件
destroyed(){
window.removeEventListener('keydown',this.keyDown,false);
}
}
</script>
<style scoped>
/*设置样式*/
.demo-ruleForm{
border: 1px solid #DCDFE6;
width: 350px;
margin: 180px auto;
padding: 35px 120px 15px 25px;
border-radius: 100px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
/* 背景图片*/
.login{
background:url("../assets/55.jpg");
width: 100%;
height: 100%;
position:fixed;
margin-top: -65px;/*上边距*/
margin-left: -10px;/*左边距*/
background-size:100% 100%;
}
/* 背景图片*/
.aaa{
background:url("../assets/4.jpg");
width: 26%;
height: 24%;
position:fixed;
margin-top: 260px;/*上边距*/
margin-left:460px;/*左边距*/
background-size:100% 100%;
}
</style>
main.js页面:设置拦截和请求
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import VueRouter from 'vue-router'
import router from './router'
import './router/directives.js';
import ElemrntUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElemrntUI)
Vue.use(VueRouter)
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios,axios)
Vue.config.productionTip = false
import 'element-ui/lib/theme-chalk/index.css';
import QS from 'qs'
Vue.config.productionTip = false
//发起请求axios请求之前给请求的头设置token的值
axios.interceptors.request.use(config=>{
config.headers.common['Authorization-token']=localStorage.getItem("token");
return config;
})
//响应时的拦截
axios.interceptors.response.use(response=>{
/*
登录错误时
*/
if(response.data.code == 1004){
//通过路由进行跳转
router.push("/");
}
return response;
})
Vue.prototype.$axios= axios
Vue.prototype.$qs= QS
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
index.js页面:
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import Main from '@/components/Main'
Vue.use(Router)
export default new Router({
routes: [
{
//跳转登录页面
path: '/',
name: 'Login',
component: Login
},
{
//跳转首页
path: '/main',
name: 'Main',
component: Main
}
]
})