记录一下前后端分离开发时使用token进行登录退出以及未登录的拦截功能,以供参考
使用token是JWT生成的,需要导入相关依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
这里需要定义一个token工具类调用JWT包的方法
import com.auth0.jwt.JWT;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* token工具类
* @author 阿楠
*/
public class TokenUtils {
public static String getTokenRootId() {
// 从 http 请求头中取出 token
String token = Objects.requireNonNull(getRequest()).getHeader("token");
return JWT.decode(token).getAudience().get(0);
}
/**
* 获取request
* @return
*/
public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
return requestAttributes == null ? null : requestAttributes.getRequest();
}
}
实体类自己定义一个就行啦,有id,name,password字段即可
我这里用的是Root,后面方法中的Root实体类和字段名记得换成自己的
Mapper接口定义findById,findByName两个方法用于登录验证
Root findRootById(@Param("root_id") Integer root_id);
//验证
Root findByName(@Param("root_name") String root_name);
XML映射用sql语句写两个select where即可,resultType对应实体类路径
<select id="findRootById" resultType="实体类路径">
select root_name,root_password
from root
where root_id = #{root_id}
</select>
<select id="findByName" resultType="实体类路径">
select *
from root
where root_name=#{root_name}
</select>
服务层接口
public interface RootService {
Root findRootById(Integer root_id);
//登录校验
Root checkRoot(String root_name, String root_password);
//获取token
String getToken(Root root);
}
服务层实现类
@Service
public class RootServiceImpl implements RootService {
@Autowired
private RootDao rootDao;
// @Autowired
// private RedisUtils redisUtils;
@Override
public Root findRootById(Integer root_id) {
return rootDao.findRootById(root_id);
}
/**
* 登录验证
* @param root_name
* @param root_password
* @return
*/
@Override
public Root checkRoot(String root_name,String root_password) {
Root root = rootDao.findByName(root_name);
if (root != null){
if (root.getRoot_password().equals(root_password)) {
return root;
}
}
return null;
}
/**
* 生成token
* @param root
* @return
*/
@Override
public String getToken(Root root) {
Date start = new Date();
//6小时有效时间
long currentTime = System.currentTimeMillis() + 1000 * 60 * 60 * 6;
Date end = new Date(currentTime);
String token = JWT.create().withAudience(root.getRoot_name())
.withIssuedAt(start)
.withExpiresAt(end)
.sign(Algorithm.HMAC256(root.getRoot_password()));
return token;
}
}
控制层使用注解@RestController
我的login接口多了一个验证码code的操作,这里不需要可以直接删掉两个注释之间的代码,记得把第二个判断的else if改成if
需要的话可以参考一下文章:SpringBoot+Vue实现验证码登录
@Autowired
private RootService rootService;
/**
* 登录实现token,并验证code
* @param param
* @param request
* @return
*/
@PostMapping(value = "/login")
public Object login(@RequestBody Map<String,String> param,
HttpServletRequest request,
HttpServletResponse response){
JSONObject jsonObject = new JSONObject();
String root_name = param.get("root_name");
String root_password = param.get("root_password");
Root root = rootService.checkRoot(root_name,root_password);
//判断code是否正确
if(request.getSession(true)
.getAttribute("code")==null
||
!param.get("code").toUpperCase()
.equals(request.getSession(true)
.getAttribute("code")
.toString().toUpperCase())){
jsonObject.put("message", "code error!");
return jsonObject;
}
//code判断结束
//登录判断
else if (root == null){
jsonObject.put("message", "login error!");
}else {
//登陆成功利用session存储账号密码
HttpSession session =request.getSession(true);
session.setAttribute("root",root);
String token = rootService.getToken(root);
jsonObject.put("message", "login success!");
jsonObject.put("token", token);
jsonObject.put("root_img", root.getRoot_img());
Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
response.addCookie(cookie);
}
return jsonObject;
}
界面添加一个form表单用于登录,这里的code部分同样可以直接删除,这里的样式如果需要的话可以直接在本文后面抬走
<el-form ref="loginForm" :model="loginForm" :rules="loginRules"
class="login_container">
<h2 class="login_title">后台登录h2>
<el-form-item prop="root_name">
<span><i class="el-icon-user" style="font-size: 20px">i>span>
<el-input type="text" auto-complete="false" v-model="loginForm.root_name"
placeholder="请输入用户名" style="width: 90%;float: right">el-input>
el-form-item>
<el-form-item prop="root_password">
<span @click="showPassword()"><i class="el-icon-lock" style="font-size: 20px">i>span>
<el-input :type="pwdType" auto-complete="false" v-model="loginForm.root_password"
placeholder="请输入密码" style="width: 90%;float: right"
@keyup.enter.native="submitLogin('loginForm')">
el-input>
el-form-item>
<el-form-item prop="code">
<span><i class="iconfont icon-icon_anquan" style="font-size: 25px">i>span>
<el-input type="text" auto-complete="false" v-model="loginForm.code"
placeholder="点击图片更换验证码"
style="width: 60%;margin-left: 10px"
@keyup.enter.native="submitLogin('loginForm')">el-input>
<el-image class="codeImg" :src="imgUrl" @click="resetImg">el-image>
el-form-item>
<el-form-item>
<el-checkbox v-model="checked" class="login_remember">记住我el-checkbox>
el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%" @click="submitLogin('loginForm')">
<span>登 录span>
el-button>
el-form-item>
el-form>
登录表单可以设置显示默认值
关于code可以删掉的有loginForm.code,imgUrl,loginRules.code
data() {
return {
loginForm: {
root_name: 'anan',
root_password: '123456',
code:''
},
//密码显示与否
pwdType:'password',
rootToken:'',
checked: true,
//生成code图片
imgUrl:'http://localhost:8181/root/code?time='+new Date(),
loginRules: {
root_name: [
{required: true, message: '用户名不能为空!', trigger: 'blur'}
],
root_password: [
{required: true, message: '密码不能为空!', trigger: 'blur'}
],
code: [
{required: true, message: '验证码不能为空!', trigger: 'change'}
]
}
}
},
这里调用的接口是被我config处理过的,同时也需要跨域才能调用,需要的话可以参考一下文章:前后端分离开发之Vue跨域
methods: {
/*
* 登录表单的提交
*/
submitLogin(formName) {
const _this = this;
this.$refs[formName].validate((valid) => {
if (valid) {
_this.axios.post('login后端接口',
{root_name: this.loginForm.root_name,
root_password: this.loginForm.root_password,
code: this.loginForm.code},
{'Content-Type': 'application/json; charset=utf-8'}
).then(res => {
console.log(res);
//验证码部分
if (res.data.message === 'code error!'){
_this.$message.error('验证码错误!');
_this.imgUrl= "http://localhost:8181/root/code?time="+new Date();
}
else if (res.data.message === 'login error!'){
_this.$message.error('用户名或密码错误,请重新登录!');
}else {
//存session
sessionStorage.setItem('root_name', _this.loginForm.root_name);
sessionStorage.setItem('root_img', res.data.root_img);
sessionStorage.setItem("root_token",res.data.token);
_this.$message({
message: '登录成功!',
type: 'success',
});
_this.$router.push({path:'/home'});
}
}).catch(failResponse => {
})
} else {
this.$message.error('请输入所有字段!');
return false;
}
});
},
showPassword() {
if (this.pwdType === 'password') {
this.pwdType = 'text'
} else {
this.pwdType = 'password'
}
},
/*
刷新验证码图片
*/
resetImg(){
this.imgUrl = "http://localhost:8181/root/code?time="+new Date();
}
}
为了保持前后端一致,登出部分也需要后端写一个接口用于前端登出时调用。
直接在controller层定义一个logout接口
/**
* 退出登录
* @param request
* @return
*/
@PostMapping(value = "logout")
public String unLogin(HttpServletRequest request){
HttpSession session = request.getSession();
session.removeAttribute("root");
return "logout success!";
}
前端在主页界面在方法区定义一个logout方法,同时remove掉对应的session缓存,html页面设置一个button调用logout方法,logout方法调用后端接口即可
logout() {
// sessionStorage.setItem("token", 'false');
this.$confirm('即将退出登录, 是否继续?', 'whether', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.axios.post("logout后端接口").then(res=>{
// console.log(res);s
sessionStorage.removeItem("root_name");
sessionStorage.removeItem("root_token");
if (res.data === 'logout success!'){
this.$message({
type: 'success',
message: '退出成功!'
});
this.$router.push("/");
}
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消!'
});
});
}
定义两个注解,用于配置拦截器
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @PassToken
* 用于跳过验证
* @author 阿楠
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @LoginToken
* 用于登录后才能操作
* @author 阿楠
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required() default true;
}
新建一个LoginIntercepter.class拦截器实现HandlerIntervepter
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.nan.house.annotation.LoginToken;
import com.nan.house.annotation.PassToken;
import com.nan.house.domain.Root;
import com.nan.house.service.RootService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* 拦截器
* @author 阿楠
*/
public class LoginInterceptor implements HandlerInterceptor{
@Autowired
RootService rootService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object object) {
// 从 http 请求头中取出 token
String token = httpServletRequest.getHeader("token");
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//检查是否有passToken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken loginToken = method.getAnnotation(LoginToken.class);
if (loginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 root id
int rootId;
try {
rootId = Integer.parseInt(JWT.decode(token).getAudience().get(0));
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
Root root = rootService.findRootById(rootId);
if (root == null) {
throw new RuntimeException("账号不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(root.getRoot_password())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o,
ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o,
Exception e) {
}
}
新建一个WebConfig.class配置类实现WebMvcConfigurer
import com.nan.house.interceptor.LoginInterceptor;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.*;
import java.util.List;
/**
* token拦截器
* @author 阿楠
*/
@SpringBootConfiguration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
registry.addInterceptor(loginInterceptor())
.addPathPatterns("/**");
}
@Bean
public LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> arg0) {
// TODO Auto-generated method stub
}
@Override
public void addCorsMappings(CorsRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void addFormatters(FormatterRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> arg0) {
// TODO Auto-generated method stub
}
@Override
public void addViewControllers(ViewControllerRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> arg0) {
// TODO Auto-generated method stub
}
@Override
public void configurePathMatch(PathMatchConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureViewResolvers(ViewResolverRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) {
// TODO Auto-generated method stub
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> arg0) {
// TODO Auto-generated method stub
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
// TODO Auto-generated method stub
return null;
}
@Override
public Validator getValidator() {
// TODO Auto-generated method stub
return null;
}
}
router/index.js为需要拦截的路由添加requireAuth属性判断
meta:{
requireAuth: true
}
main.js中添加导航守卫配置,根据缓存token判断是否未登录或者登录已过期,如果token存在则通过,否则重定向到登录页面
// 导航守卫,判断是否登录
router.beforeEach((to, from, next) => {
console.log(to);
if (to.meta.requireAuth) {
if (sessionStorage.getItem("root_token") !== null) {
next()
}
else {
next(
{
path: '/login',
query: {redirect: to.fullPath}
},
Message({
showClose: true,
message: '未登录或登录已过期,无法获取管理员权限,请登录!',
type: 'error'
})
)
}
} else {
next()
}
});
地址栏输入其他地址,会重定向到login页面,并提示登录
token过期自动删除后则进不去界面
记录到此结束,希望这篇文章对兄弟姐妹们有作用!