简单实现
token 可用于登录验证和权限管理.
大致步骤分为:
前端登录, post 用户名和密码到后端.
后端验证用户名和密码, 若通过, 生成一个 token 返回给前端.
前端拿到 token 用 vuex 和 localStorage 管理, 登录成功进入首页.
之后前端每一次权限操作如跳转路由, 都需要判断是否存在 token, 若不存在, 跳转至登录页.
前端之后的每一个对后端的请求都要在请求头上带上 token, 后端查看请求头是否有 token, 拿到 token 检查是否过期, 返回对应状态给前端.
若 token 已过期, 清除 token 信息, 跳转至登录页.
具体代码如下:
前端
登录页
- <template>
- <div class="signin-form">
- <h3 class="sign-title">ticket-sys 登录 </h3>
- <div>
- <el-form :model="loginForm" :rules="rules" status-icon ref="ruleForm" class="demo-ruleForm">
- <el-form-item prop="username">
- <el-input
- v-model="loginForm.username"
- autocomplete="off"
- placeholder="用户名"
- prefix-icon="el-icon-user-solid"
- ></el-input>
- </el-form-item>
- <el-form-item prop="password">
- <el-input
- type="password"
- v-model="loginForm.password"
- autocomplete="off"
- placeholder="请输入密码"
- prefix-icon="el-icon-lock"
- ></el-input>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="submitForm" id="login-btn"> 登录 </el-button>
- </el-form-item>
- </el-form>
- </div>
- </div>
- </template>
- <script>
- import API from '../constant/api';
- import {mapMutations} from "vuex";
- export default {
- name: 'login',
- data() {
- return {
- loginForm:{
- username:'',
- password:''
- },
- userToken:'',
- rules:{
- username:[
- { required: true, message: '请输入用户名', trigger: 'blur' },
- ],
- password:[
- { required: true, message: '请输入密码', trigger: 'blur' },
- ]
- }
- }
- },
- methods: {
- ...mapMutations(['changeLogin']),
- submitForm() {
- let v=this;
- this.$axios({
- method: 'post',
- url: API.base_url+'/user/login',
- data:{
- 'username':v.loginForm.username,
- 'password':v.loginForm.password
- }
- }).then(function(res){
- console.log(res.data);
- v.userToken = 'Bearer' + res.data.token;
- // 将用户 token 保存到 vuex 中
- v.changeLogin({ Authorization:v.userToken });
- v.$router.push('/home');
- v.$message('登录成功');
- }).catch(function(err){
- console.log("err",err);
- v.$message('密码或用户名错误');
- })
- }
- }
- }
- </script>
- <style scoped>...</style>
vuex 状态管理
- /store/index.JS
- import Vue from 'vue';
- import Vuex from 'vuex';
- Vue.use(Vuex);
- const store = new Vuex.Store({
- state: {
- // 存储 token
- Authorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : ''
- },
- mutations: {
- // 修改 token, 并将 token 存入 localStorage
- changeLogin (state, user) {
- state.Authorization = user.Authorization;
- localStorage.setItem('Authorization', user.Authorization);
- }
- }
- });
- export default store;
路由守卫
- /router/index.JS
- import Vue from 'vue'
- import VueRouter from 'vue-router'
- import Home from '../views/Home.vue'
- Vue.use(VueRouter)
- const routes = [
- {
- path: '/home',
- name: 'home',
- component: Home,
- }
- ,
- {
- path: '/about',
- name: 'about',
- // route level code-splitting
- // this generates a separate chunk (about.[hash].JS) for this route
- // which is lazy-loaded when the route is visited.
- component: function () {
- return import(/* webpackChunkName: "about" */ '../views/About.vue')
- }
- },
- {
- path:'/login',
- name:'login',
- component:function () {
- return import('../views/Login.vue');
- }
- }
- ]
- const router = new VueRouter({
- <template>
- <div class="home">
- <el-container>
- <!-- <Header/>-->
- <!-- <el-main > 首页 </el-main>-->
- <el-button @click="exit"> 退出登录 </el-button>
- <el-button @click="test"> 携带 token 的测试请求 </el-button>
- </el-container>
- </div>
- </template>
- <script>
- // @ is an alias to /src
- import HelloWorld from '@/components/HelloWorld.vue'
- import Header from "../components/Header";
- import API from "../constant/api";
- export default {
- name: 'home',
- components: {
- Header,
- HelloWorld
- },
- methods:{
- exit(){
- localStorage.removeItem('Authorization');
- this.$router.push('/login');
- },
- test(){
- this.$axios({
- method: 'get',
- url: API.base_url+'/user/test',
- }).then(function(res){
- console.log("res",res);
- }).catch(function(err){
- console.log("err",err);
- })
- }
- }
- }
- </script>
- <style>...</style>
- mode: 'history',
- base: process.env.BASE_URL,
- routes
- });
- // 导航守卫
- // 使用 router.beforeEach 注册一个全局前置守卫, 判断用户是否登陆
- router.beforeEach((to, from, next) => {
- if (to.path === '/login') {
- next();
- } else {
- let token = localStorage.getItem('Authorization');
- if (token === 'null' || token === '') {
- next('/login');
- } else {
- next();
- }
- }
- });
- export default router
主文件中注册并添加拦截器
/main.JS
- import Vue from 'vue'
- import App from './App.vue'
- import router from './router'
- import './plugins/element.js'
- import ElementUI from 'element-ui';
- import 'element-ui/lib/theme-chalk/index.css';
- import axios from 'axios' ;
- import Vuex from 'vuex' // 引入状态管理
- Vue.prototype.$axios= axios ;
- Vue.use(Vuex) ;
- Vue.config.productionTip = false
- Vue.use(ElementUI)
- // 这里要导入 store
- import store from "./store";
- // 添加请求拦截器, 在请求头中加 token
- axios.interceptors.request.use(
- config => {
- if (localStorage.getItem('Authorization')) {
- config.headers.Authorization = localStorage.getItem('Authorization');
- }
- return config;
- },
- error => {
- return Promise.reject(error);
- });
- new Vue({
- // 这里要配置 store
- router, store:store,
- render: function (h) { return h(App) }
- }).$mount('#app')
home 页面
- <template>
- <div class="home">
- <el-container>
- <!-- <Header/>-->
- <!-- <el-main > 首页 </el-main>-->
- <el-button @click="exit"> 退出登录 </el-button>
- <el-button @click="test"> 携带 token 的测试请求 </el-button>
- </el-container>
- </div>
- </template>
- <script>
- // @ is an alias to /src
- import HelloWorld from '@/components/HelloWorld.vue'
- import Header from "../components/Header";
- import API from "../constant/api";
- export default {
- name: 'home',
- components: {
- Header,
- HelloWorld
- },
- methods:{
- exit(){
- // 退出登录, 清空 token
- localStorage.removeItem('Authorization');
- this.$router.push('/login');
- },
- test(){
- this.$axios({
- method: 'get',
- url: API.base_url+'/user/test',
- }).then(function(res){
- console.log("res",res);
- }).catch(function(err){
- console.log("err",err);
- })
- }
- }
- }
- </script>
- <style>...</style>
后端
登录 controller
- package com.zxc.ticketsys.controller;
- import com.fasterxml.jackson.core.JsonProcessingException;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.Web.bind.annotation.*;
- import java.util.HashMap;
- import java.util.Map;
- @RestController
- @RequestMapping("/user")
- public class UserController{
- @RequestMapping(value = "/login",method = RequestMethod.POST)
- @ResponseBody
- public String login(@RequestHeader Map<String,Object> he,@RequestBody Map<String,Object> para) throws JsonProcessingException {
- System.out.println(he);
- String username=(String)para.get("username");
- String password=(String)para.get("password");
- HashMap<String,Object> hs=new HashMap<>();
- hs.put("token","token"+username+password);
- ObjectMapper objectMapper=new ObjectMapper();
- return objectMapper.writeValueAsString(hs);
- }
- @RequestMapping(value = "/test",method = RequestMethod.GET)
- @ResponseBody
- public String test(@RequestHeader Map<String,Object> he) throws JsonProcessingException {
- System.out.println(he);
- HashMap<String,Object> hs=new HashMap<>();
- ObjectMapper objectMapper=new ObjectMapper();
- return objectMapper.writeValueAsString(hs);
- }
- }
测试
登录
此时后台的请求头:
{host=localhost:8088, connection=keep-alive, accept=application/JSON, text/plain, */*, origin=http://localhost:8080, user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36, sec-fetch-site=same-site, sec-fetch-mode=cors, referer=http://localhost:8080/home, accept-encoding=gzip, deflate, br, accept-language=zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7}
登录成功, 进入 home 页面
后端返回的 token:
前端持有 token, 访问测试接口
后端收到请求头:
{host=localhost:8088, connection=keep-alive, accept=application/JSON, text/plain, */*, origin=http://localhost:8080, authorization=Bearer tokenadminadmin, user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36, sec-fetch-site=same-site, sec-fetch-mode=cors, referer=http://localhost:8080/home, accept-encoding=gzip, deflate, br, accept-language=zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7}
带有 token, 可以进行验证
退出登录, 清空 token
后台实际应用
在实际的应用中, 一般需要一个生成 token 的工具类和一个拦截器对请求进行拦截.
token 生成工具类
/utils/TokenUtil.java package com.zxc.ticketsys.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.zxc.ticketsys.model.User; import java.util.Date; public class TokenUtil { private static final long EXPIRE_TIME= 10*60*60*1000; private static final String TOKEN_SECRET="txdy"; // 密钥盐 /** * 签名生成 * @param user * @return */ public static String sign(User user){ String token = null; try { Date expiresAt = new Date(System.currentTimeMillis() + EXPIRE_TIME); token = JWT.create() .withIssuer("auth0") .withClaim("username", user.getUsername()) .withExpiresAt(expiresAt) // 使用了 HMAC256 加密算法. .sign(Algorithm.HMAC256(TOKEN_SECRET)); } catch (Exception e){ e.printStackTrace(); } return token; } /** * 签名验证 * @param token * @return */ public static boolean verify(String token){ try { JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build(); DecodedJWT jwt = verifier.verify(token); System.out.println("认证通过:"); System.out.println("username:" + jwt.getClaim("username").asString()); System.out.println("过期时间:" + jwt.getExpiresAt()); return true; } catch (Exception e){ return false; } } }
拦截器类
/interceptor/TokenInterceptor.java package com.zxc.ticketsys.interceptor; import com.alibaba.fastjson.JSONObject; import com.zxc.ticketsys.utils.TokenUtil; import org.springframework.stereotype.Component; import org.springframework.Web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class TokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{ if(request.getMethod().equals("OPTIONS")){ response.setStatus(HttpServletResponse.SC_OK); return true; } response.setCharacterEncoding("utf-8"); String token = request.getHeader("token"); if(token != null){ boolean result = TokenUtil.verify(token); if(result){ System.out.println("通过拦截器"); return true; } } response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); try{ JSONObject JSON = new JSONObject(); JSON.put("msg","token verify fail"); JSON.put("code","50000"); response.getWriter().append(JSON.toJSONString()); System.out.println("认证失败, 未通过拦截器"); }catch (Exception e){ e.printStackTrace(); response.sendError(500); return false; } return false; } }
配置拦截器
/config/WebConfiguration.java
注意最好写在一个配置类里, 且 WebMvcConfigurationSupport 和 WebMvcConfigurerAdapter 不要同时存在
这里包括处理跨域的配置, 而且全部改为 implements WebMvcConfigurer 接口
package com.zxc.ticketsys.config; import com.zxc.ticketsys.interceptor.TokenInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import org.springframework.Web.servlet.config.annotation.*; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import org.springframework.Web.servlet.config.annotation.WebMvcConfigurer; /** * 跨域请求支持 / token 拦截 * tip: 只能写在一个配置类里 */ @Configuration public class WebConfiguration implements WebMvcConfigurer { private TokenInterceptor tokenInterceptor; // 构造方法 public WebConfiguration(TokenInterceptor tokenInterceptor){ this.tokenInterceptor = tokenInterceptor; } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowCredentials(true) .allowedHeaders("*") .allowedMethods("*") .allowedOrigins("*"); } @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer){ configurer.setTaskExecutor(new ConcurrentTaskExecutor(Executors.newFixedThreadPool(3))); configurer.setDefaultTimeout(30000); } @Override public void addInterceptors(InterceptorRegistry registry){ List<String> excludePath = new ArrayList<>(); // 排除拦截, 除了注册登录 (此时还没 token), 其他都拦截 excludePath.add("/user/register"); // 登录 excludePath.add("/user/login"); // 注册 excludePath.add("/static/**"); // 静态资源 excludePath.add("/assets/**"); // 静态资源 registry.addInterceptor(tokenInterceptor) .addPathPatterns("/**") .excludePathPatterns(excludePath); WebMvcConfigurer.super.addInterceptors(registry); } }
控制器类
package com.zxc.ticketsys.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.zxc.ticketsys.model.User; import com.zxc.ticketsys.utils.TokenUtil; import org.springframework.Web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/user") public class UserController{ @RequestMapping(value = "/login",method = RequestMethod.POST) @ResponseBody public String login(@RequestBody Map<String,Object> para) throws JsonProcessingException { String username=(String)para.get("username"); String password=(String)para.get("password"); String token= TokenUtil.sign(new User(username,password)); HashMap<String,Object> hs=new HashMap<>(); hs.put("token",token); ObjectMapper objectMapper=new ObjectMapper(); return objectMapper.writeValueAsString(hs); } @RequestMapping(value = "/test",method = RequestMethod.POST) @ResponseBody public String test(@RequestBody Map<String,Object> para) throws JsonProcessingException { HashMap<String,Object> hs=new HashMap<>(); hs.put("data","data"); ObjectMapper objectMapper=new ObjectMapper(); return objectMapper.writeValueAsString(hs); } }
测试
登录
成功登录, 获得 token.
不带 token 访问 test 接口
被拦截, 访问失败.
带有效 token 访问 test 接口
访问成功, 获得数据.
完整项目代码
https://github.com/Zeng1998/sb_cors_token_demo
来源: https://www.cnblogs.com/zxcoder/p/11964433.html