记录一下前后端分离开发时使用token进行登录退出以及未登录的拦截功能,以供参考

依赖

使用token是JWT生成的,需要导入相关依赖

		<dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

登录

后端

Util工具类

这里需要定义一个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();
    }

}

Model层

实体类自己定义一个就行啦,有id,name,password字段即可
我这里用的是Root,后面方法中的Root实体类和字段名记得换成自己的

Mapper层

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>

Service层

服务层接口

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;
    }
    
}

Controller层

控制层使用注解@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;
    }

前端

HTML

界面添加一个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 v-if="!loading">登 录</span>-->
<!--                    <span v-else>登 录 中...</span>-->
                    <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();
    }
 }

效果图

可以看到登录后成功存入token
登录页面
存入token

登出

为了保持前后端一致,登出部分也需要后端写一个接口用于前端登出时调用。
直接在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: '已取消!'
         });
    });
}

在这里插入图片描述

拦截

后端

定义annotation

定义两个注解,用于配置拦截器

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;
}

Intercepter拦截器

新建一个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配置类

新建一个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过期自动删除后则进不去界面
在这里插入图片描述

记录到此结束,希望这篇文章对兄弟姐妹们有作用!

Logo

前往低代码交流专区

更多推荐