实现JWT单点登录功能
单点登录:直观意义就是对于一个系统,只需登录一次就可以处处访问
两种单点登录的设计方案
方案一:redis+token
登录阶段
登录开始->校验用户名和密码->生成随机的token,每次都不一样->将token放入redis->结束
校验阶段
从header中获取token->根据token到redis获取数据->是否有数据->有数据登录校验成功,反之校验失败
方案二:JWT
登录阶段
登陆开始->校验用户名和密码->生成JWT Token,每次都不一样->结束
校验阶段
校验开始->从header获取token->使用工具包校验token->校验是否成功
JWT原理
JWT原理及其用法
JWT存在的问题
问题一:token被解密破解
给密钥加盐值,每个项目盐值不一样,减小被破解风险
问题二:token被第三方使用
背景:自己的产品,被第三方包装成一个界面,做成他们自己的收费产品
此类问题无好的解决办法,可以通过限流进行一定程度缓解,如果某个相同的token有大量请求,则可能被第三方利用
生成JWT单点登录token
本项目中,我们利用Hutool工具包提供的JWT模块进行单点登录的开发
将JWT  token生成和校验功能封装为一个工具类
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 
 | package com.bang.train.common.util;
 import cn.hutool.core.date.DateField;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.crypto.GlobalBouncyCastleProvider;
 import cn.hutool.json.JSONObject;
 import cn.hutool.jwt.JWT;
 import cn.hutool.jwt.JWTPayload;
 import cn.hutool.jwt.JWTUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.HashMap;
 import java.util.Map;
 
 public class JwtUtil {
 private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);
 
 
 
 
 private static final String key = "train-12306";
 
 public static String createToken(Long id, String mobile) {
 LOG.info("开始生成JWT token,id:{},mobile:{}", id, mobile);
 GlobalBouncyCastleProvider.setUseBouncyCastle(false);
 DateTime now = DateTime.now();
 DateTime expTime = now.offsetNew(DateField.HOUR, 24);
 Map<String, Object> payload = new HashMap<>();
 
 payload.put(JWTPayload.ISSUED_AT, now);
 
 payload.put(JWTPayload.EXPIRES_AT, expTime);
 
 payload.put(JWTPayload.NOT_BEFORE, now);
 
 payload.put("id", id);
 payload.put("mobile", mobile);
 String token = JWTUtil.createToken(payload, key.getBytes());
 LOG.info("生成JWT token:{}", token);
 return token;
 }
 
 public static boolean validate(String token) {
 LOG.info("开始JWT token校验,token:{}", token);
 GlobalBouncyCastleProvider.setUseBouncyCastle(false);
 JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
 
 boolean validate = jwt.validate(0);
 LOG.info("JWT token校验结果:{}", validate);
 return validate;
 }
 
 public static JSONObject getJSONObject(String token) {
 GlobalBouncyCastleProvider.setUseBouncyCastle(false);
 JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
 JSONObject payloads = jwt.getPayloads();
 payloads.remove(JWTPayload.ISSUED_AT);
 payloads.remove(JWTPayload.EXPIRES_AT);
 payloads.remove(JWTPayload.NOT_BEFORE);
 LOG.info("根据token获取原始内容:{}", payloads);
 return payloads;
 }
 
 public static void main(String[] args) {
 String token = createToken(1L, "123");
 
 
 validate(token);
 
 getJSONObject(token);
 }
 }
 
 
 | 
修改登录功能service层代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | @Override
 public MemberLoginResp login(MemberLoginReq req) {
 String mobile = req.getMobile();
 String code = req.getCode();
 
 Member member = getMembersByMobile(mobile);
 if(ObjectUtil.isNull(member)){
 throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);
 }
 
 
 if(!"8888".equals(code)){
 throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);
 }
 
 
 
 MemberLoginResp memberLoginResp = BeanUtil.copyProperties(member,MemberLoginResp.class);
 
 
 
 String token = JwtUtil.createToken(memberLoginResp.getId(),memberLoginResp.getMobile());
 memberLoginResp.setToken(token);
 return memberLoginResp;
 }
 
 | 
使用vuex保存登录信息
在store index.js中定义登录信息对应的全局变量
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | import { createStore } from 'vuex'
 export default createStore({
 state: {
 member:{}
 },
 getters: {
 },
 mutations: {
 setMember(state,_member){
 state.member = _member;
 }
 },
 actions: {
 },
 modules: {
 }
 })
 
 
 | 
修改login.vue
登录成功之后,将后端数据存储至全部变量
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | const login = () => {axios.post("/member/login", loginForm).then((response) => {
 let data = response.data;
 if (data.success) {
 notification.success({ description: '登录成功!' });
 // 登录成功,跳到控台主页
 router.push("/");
 //使用vuex保存会员登录信息,里面包含JWT TOKEN
 store.commit("setMember", data.content);
 } else {
 notification.error({ description: data.message });
 }
 })
 };
 
 | 
vuex配置后的session解决浏览器刷新问题
gateway拦截器的简单使用使用
- 自定义过滤器
- 继承接口GlobalFilter,实现filter方法
 
- 多个过滤器存在时,如何确定执行的先后顺序
- 继承Ordered接口,实现getOrder方法,按照返回数值由小到大执行
 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 
 | package com.bang.train.gateway.filter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
 import org.springframework.cloud.gateway.filter.GlobalFilter;
 import org.springframework.core.Ordered;
 import org.springframework.stereotype.Component;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 
 
 @Component
 public class AuthGlobalFilter implements GlobalFilter, Ordered {
 private static final Logger LOG = LoggerFactory.getLogger(AuthGlobalFilter.class);
 @Override
 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 LOG.info("网关登录校验拦截器:{}","AuthGlobalFilter");
 
 return chain.filter(exchange);
 }
 
 
 @Override
 public int getOrder() {
 return 0;
 }
 }
 
 | 
编写会员登录校验拦截器
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 
 | package com.bang.train.gateway.filter;
 import com.bang.train.gateway.util.JwtUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
 import org.springframework.cloud.gateway.filter.GlobalFilter;
 import org.springframework.core.Ordered;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Component;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 
 
 @Component
 public class LoginMemberFilter implements GlobalFilter, Ordered {
 private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class);
 @Override
 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 
 String path = exchange.getRequest().getURI().getPath();
 
 if (path.contains("/admin")
 || path.contains("/hello")
 || path.contains("/member/login")
 || path.contains("/member/send-code")) {
 LOG.info("不需要登录验证:{}", path);
 return chain.filter(exchange);
 } else {
 LOG.info("需要登录验证:{}", path);
 }
 
 String token = exchange.getRequest().getHeaders().getFirst("token");
 LOG.info("会员登录验证开始,token:{}", token);
 if (token == null || token.isEmpty()) {
 LOG.info( "token为空,请求被拦截" );
 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
 return exchange.getResponse().setComplete();
 }
 
 
 boolean validate = JwtUtil.validate(token);
 if (validate) {
 LOG.info("token有效,放行该请求");
 return chain.filter(exchange);
 } else {
 LOG.warn( "token无效,请求被拦截" );
 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
 return exchange.getResponse().setComplete();
 }
 }
 
 @Override
 public int getOrder() {
 return 0;
 }
 }
 
 | 
为axios请求增加统一拦截器
在main.js中修改拦截器代码,为所有的axios请求头加上token参数
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | axios.interceptors.request.use(function (config) {
 console.log('请求参数:', config);
 
 const token = store.state.member.token;
 if(token){
 config.headers.token = token;
 console.log("为请求header增加token:",token);
 }
 return config;
 }, error => {
 return Promise.reject(error);
 });
 
 | 
为了增加用户请求,在token失效之后(后端返货401)时,应该让页面自动跳转到登录页面,并提示用户登录超时
修改main.js中响应拦截器代码,对401做出特定动作
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | axios.interceptors.response.use(function (response) {
 console.log('返回结果:', response);
 return response;
 }, error => {
 console.log('返回错误:', error);
 const response = error.response;
 const status = response.status;
 if(status === 401){
 
 console.log("未登录或者登录超时,跳转至登录页面");
 
 store.commit("setMember",{});
 
 notification.error({description: "未登录或登录超时"});
 
 router.push("/login");
 }
 return Promise.reject(error);
 });
 
 | 
为路由页面添加拦截器
背景
通过axios发送请求,可以通过后端jwt校验验证用户权限,未登录或者登录超时会直接跳转至登录页面
但是系统中可能存在一些其他静态页面,比如帮助文档等页面,这类页面不用于后端进行交互,无法根据后端响应进行拦截,所以需要增加路由跳转拦截器
在router/index.js中增加拦截器代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 
 | import { createRouter, createWebHistory } from 'vue-router'import store from "@/store";
 import {notification} from "ant-design-vue";
 
 const routes = [
 {
 path: '/login',
 name: 'login',
 component: () => import('../views/LoginView.vue')
 },
 {
 path: '/',
 component: () => import('../views/main.vue'),
 meta: {
 loginRequire: true
 },
 }
 ];
 
 const router = createRouter({
 history: createWebHistory(process.env.BASE_URL),
 routes
 })
 
 
 
 router.beforeEach((to, from, next) => {
 
 if (to.matched.some(function (item) {
 console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);
 return item.meta.loginRequire;
 })) {
 const _member = store.state.member;
 console.log("页面登录校验开始:", _member);
 if (!_member.token) {
 console.log("用户未登录或登录超时!");
 notification.error({ description: "未登录或登录超时" });
 next('/login');
 } else {
 next();
 }
 } else {
 next();
 }
 });
 export default router
 
 |