(原创)springboot+springsecurity+redis+jwt+vue+iframe 做一个前后端分离的SSO单点登陆鉴权系统 类似淘宝天猫单点登陆

1 前言

单点登陆系统,简单说就是用户登陆了www.aaa.com之后,再登陆www.bbb.com,就不需要重新输入用户名密码进行登陆,中间会有一个www.sso.com的验证中心。这点类似于淘宝 ,天猫,当然淘宝的验证中心域名是login.taobao.com,和www.taobao.com是同一个域名。要做到无缝登陆,就需要iframe这个技术,淘宝天猫也是用的iframe,iframe还是挺简单的,我做这个项目参考许多资料,网上的资料大部分是半成品,细节还是要自己琢磨,所以也没有什么规划,代码有冗余的地方,也有需要优化的地方,进来看的人可以自己修改。

这个项目用spring security做鉴权,网上有些资料都是把需要鉴权的路径写死在代码中,像这样

http.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                .authorizeRequests()  定义哪些URL需要被保护、哪些不需要被保护
                .antMatchers("/**").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/createPass").permitAll()
                .antMatchers("/checkTokenCookie").permitAll()
                .antMatchers("/logout").permitAll()
                .antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
                .anyRequest().authenticated()// 剩下所有的验证都需要验证
                .and()
                .csrf().disable()                      // 禁用 Spring Security 自带的跨域处理
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

这样不好,当然好的都是做成基于JDBC的数据库鉴权,我做的就是基于数据库鉴权,jwt的令牌刷新机制也做了。也有做不好的地方,就是把一些工具类做成了静态类,这样在注入spring的IOC容器时就要多写一个步骤,这个地方要优化,你们自己优化吧,也挺容易的。还有那个数据库变更时同步更新redis缓存的,没有做,因为电脑配置不高,开VM虚拟机会卡,用的工具是mysql-udf-http,这个工具没有windows版的,这个工具看资料也是挺简单,你们自己试试吧,同时也可以结合redis的消息订阅发布机制更新redis中的用户权限缓存。这在下面代码有测试。还有在验证jwt令牌时,本来是要验证ip地址和客户端设备是否一致的,做到一半时,看了一些资料,发现不稳妥,客户端还好,ip地址经过公司路由,电信服务器,是会变动,如果验证ip不对就判断用户非法登陆,明显不合理,所以换成在cookie中加入一些密钥进入验证,把ip验证和客户端设备验证去掉了,看到一些资料说客户端验证可以加appId进入验证,这个没有深究。下面进入代码,同时也会把思路说一说,就当参考吧,我也不是什么技术大佬。

2.1 mysql数据库

DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `pid` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES ('1', '/aaab', 'ROLE_AAA', null, '0');
INSERT INTO `permission` VALUES ('2', '/bbba', 'ROLE_BBB', null, '0');
INSERT INTO `permission` VALUES ('3', '/pureCheckToken', 'ROLE_USER', null, '0');
INSERT INTO `permission` VALUES ('4', '/annym/**', 'ROLE_ANNYM', '可以匿名访问', '0');

-- ----------------------------
-- Table structure for `role`
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'USER');
INSERT INTO `role` VALUES ('2', 'ADMIN');
INSERT INTO `role` VALUES ('3', 'BBB');

-- ----------------------------
-- Table structure for `role_permission`
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
  `role_id` bigint(11) NOT NULL,
  `permission_id` bigint(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES ('2', '1');
INSERT INTO `role_permission` VALUES ('2', '2');
INSERT INTO `role_permission` VALUES ('1', '2');
INSERT INTO `role_permission` VALUES ('1', '3');

INSERT INTO `role_permission` VALUES ('1', '4');

-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'mm', '$2a$10$1fYMjHDhY2iKB32szSxur.bE/fh9a3su.j8OxwOY0fAPIOpKK8uG6');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$1fYMjHDhY2iKB32szSxur.bE/fh9a3su.j8OxwOY0fAPIOpKK8uG6');

-- ----------------------------
-- Table structure for `user_role`
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `user_id` bigint(11) NOT NULL,
  `role_id` bigint(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('2', '1');
INSERT INTO `user_role` VALUES ('2', '2');
INSERT INTO `user_role` VALUES ('1', '3');

-- ----------------------------
-- Table structure for `version`
-- ----------------------------
DROP TABLE IF EXISTS `version`;
CREATE TABLE `version` (
  `name` varchar(255) DEFAULT '0',
  `versionNum` int(11) unsigned DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of version
-- ----------------------------
INSERT INTO `version` VALUES ('role_permission_version', '7');

2.2 pom.xml  -下面是SSO服务端的JAVA代码,不是SSO前端代码



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.5.RELEASE
         
    
    com.tuu
    springjw
    0.0.1-SNAPSHOT
    springjw
    Demo project for Spring Boot
    
        1.8
    
    
        
            org.springframework.boot
            spring-boot-starter
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-logging
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.1.1
        
        
            org.projectlombok
            lombok
            1.16.10
            provided
        
        
            mysql
            mysql-connector-java
            5.1.10
        
        
            com.alibaba
            druid
            1.1.10
        
        
            io.jsonwebtoken
            jjwt
            0.7.0
        
        
            eu.bitwalker
            UserAgentUtils
            1.21
        
        
            org.apache.commons
            commons-pool2
            2.4.3
        
        
            org.apache.commons
            commons-lang3
            3.8.1
        
        
            commons-codec
            commons-codec
            1.11
        
        
            org.bouncycastle
            bcprov-jdk16
            1.45
        
        
            com.alibaba
            fastjson
            1.2.28
        
        
        
            org.springframework.boot
            spring-boot-starter-security
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

2.2.2  application.yml

server:
  port: 8878
# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver

    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/myjwt?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
  redis:
    database: 0
    host: 127.0.0.1
    password: ''

    port: 6379
    timeout: 1000ms
    lettuce:
      pool:
        max-active: 200
        max-wait: -1ms
        max-idle: 10
        min-idle: 0

  # 配置sql打印日志
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:/mapper/*.xml
#日志的方式打印sql
logging:
  level:
    com.tuu.mapper: DEBUG
# 自定义  常量
jwt:
  token: UserToken
  secretKey: 7786df7fc3a34e26a61c034d5ec8245d
  expiration: 1200000
  userVersionExpiration: 1260000
  loginPage: /login2
  AllAuths: AllAuths
  pVersionName: role_permission_version
  userIdentifier: tmcb
  userIdentFakes: nZia,G7bRLLD,mQHT,b6h5r,Y7PAR,AKAKAM

2.3 Permission.java    -----用户的权限映射类 一定要序列化,不然存不进redis

@Data的idea插件要自己安装  网上有资料


import lombok.Data;
import java.io.Serializable;
@Data
public class Permission implements Serializable {
    private int id;
    //权限名称
    private String name;
    //权限描述
    private String descritpion;
    //授权链接
    private String url;
    //父节点id
    private int pid;
}

2.4  JwtClainsObject.java   -----这个类用来存储生成jwt的要素


import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class JwtClainsObject {   //这个类用来存储生成jwt的要素
    String username;
    String randomId;
    String ip;
    String subjectUid;
    //String browerName;
    //String systemName;
    //String browerVersion;
    //jwt过期时间 设置为20分钟
    long ttlMillis;
}

2.5  SysUser.java    ----用于security的userDetails

import lombok.Data;
@Data
public class SysUser {
    private String username;
    private String password;
    private boolean isAccountNonExpired;
    private boolean isEnabled;
    private boolean isSingle;
    private boolean isAccountLock;
}

2.6  RPVersion.java   ----当permission表,role_permission表变动时,这个表的字段加1

import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class RPVersion {
    private String name;
    private Integer versionNum;
}

2.7  JwtUser.java  ----映射user表

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
@TableName("user")   //映射user表
public class JwtUser {
    private String id;
    private String username;
    private String password;
}

2.8   Role.java  ----映射role角色表

import lombok.Data;
import java.io.Serializable;
@Data
public class Role implements Serializable {
    private String id;
    private String name;
    //@TableField(exist = false)
    //private Set permissions=new HashSet<>();
}

3.1  MyUtils.java --生成传给前端的无用的cookie,把验证用的cookie隐藏在其中,淘宝网cookie中也有很多这样的混淆视听cookie

import org.springframework.stereotype.Component;
@Component
public class MyUtils {
    //随机字符串
    public String randomString(int len) {
        int len2 =  len>0?len:46;
        String chars ="ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprswxyz2345678";    /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
        //1.隐式转换
        double maxPos = chars.length();
        String pwd ="";
        for (int i = 0; i < len; i++) {
            pwd += chars.charAt((int)Math.floor(Math.random() * maxPos));
        }
        return pwd;
    }
    //得到随机整数
    public int GetRandomNum(int Min,int Max) {
        int Range = Max - Min;
        double Rand = Math.random();
        //round +0.5  再取整
        return (int)(Min + Math.round(Rand * Range));
    }
}

3.2  RedisUtil.java  

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.PoolException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {
    @Autowired
    private RedisTemplate redisTemplate;
    // =============================common============================

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();

            throw new PoolException("redis utils连接不行了");

        }

    }
    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    // ================================Map=================================
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    // ============================set=============================
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

 
  

3.3 RedisConfig.java

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        JdkSerializationRedisSerializer jdkserlier=new JdkSerializationRedisSerializer();
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jdk自带的序列化
        template.setValueSerializer(jdkserlier);
        // hash的value序列化方式jdk自带的序列化
        template.setHashValueSerializer(jdkserlier);
        template.afterPropertiesSet();
        return template;
    }
}

3.4 RedisReceiver.java  ----redis消息订阅  消息接收器

import org.springframework.stereotype.Service;

@Service
public class RedisReceiver {
//消息处理器
    public void rolePerMessage(String message) {
        System.out.println("rolePer执行:"+message);
        //这里是收到通道的消息之后执行的方法 更新redis中用户的权限
    }
    public void userRoleMessage(String message) {
        System.out.println("userRole消息来了:"+message);
        //这里是收到通道的消息之后执行的方法
    }
}

3.5  RedisListenerConfig.java  ---redis消息监听器容器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisListenerConfig {
    /**
     * redis消息监听器容器
     * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理

     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter userRoleListenerAdapter,
                                            MessageListenerAdapter rolePerListenerAdapter
    ) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);

        //可以添加多个 messageListener   监听主题
        container.addMessageListener(userRoleListenerAdapter, new PatternTopic("user_role"));
        container.addMessageListener(rolePerListenerAdapter, new PatternTopic("role_permission"));
        return container;
    }


    /**
     * 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
     *
     * @param redisReceiver
     * @return
     */
    @Bean
    MessageListenerAdapter rolePerListenerAdapter(RedisReceiver redisReceiver) {
        System.out.println("rolePer消息适配器进来了");
        return new MessageListenerAdapter(redisReceiver, "rolePerMessage");
    }
    @Bean
    MessageListenerAdapter userRoleListenerAdapter(RedisReceiver redisReceiver) {
        System.out.println("userRole消息适配器进来了");
        return new MessageListenerAdapter(redisReceiver, "userRoleMessage");
    }

}

3.6  JwtTokenUtil.java  --核心工具类,参照网上资料复制的大杂烩,需要优化,不应该有静态方法,而且有冗余代码,但也有一些很优雅的代码,像这样Claims::getExpiration函数式接口的写法

被我改造过程中不用了,你们自己优化,不难,只是费些时间

import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultClock;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
@Slf4j
public class JwtTokenUtil  {
    @Autowired
    private  RedisUtil redisUtil;
    @Value("${jwt.secretKey}")
    private  String secret;
    @Value("${jwt.expiration}")
    private  Long expiration;
    @Value("${jwt.userVersionExpiration}")
    private  Long userExpiration;
    @Value("${jwt.userIdentifier}")
    private  String userIdent;
    private  Clock clock = DefaultClock.INSTANCE;

    public  String generateToken2(JwtClainsObject jco) {

        return doGenerateToken2(jco);
    }
//生成jwt
    private  String doGenerateToken2(JwtClainsObject jco) {
        Map claims = new HashMap();//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        claims.put("uid", jco.getSubjectUid());
        claims.put("user_name", jco.getUsername());
        claims.put(userIdent, jco.getRandomId());
       final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(jco.getSubjectUid())
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS256, generalKey())
                .compact();
    }

    private  Date calculateExpirationDate(Date createdDate) {
        return new Date(createdDate.getTime() + expiration);
    }

    public Map validateTokenAndCookie(String jwt, HttpServletRequest request) throws Exception {
        Map map = new HashMap<>();
        if (pureValidateToken(jwt, request)) {
            Claims claims = getClaims(jwt);
            map.put("nickname", claims.get("user_name"));
            System.out.println("验证成功。。。返回cookies");
            return map;
        }
        return null;
    }
    //不返回cookie  单纯检验token
    public Boolean pureValidateToken(String jwt, HttpServletRequest request) {
        //设置需要解析的jwt
        //Date exp = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy‐MM‐dd hh:mm:ss");
        
        Map map = new HashMap<>();
        Claims claims = null;
       String uiden= CookieUtils.getCookie(request,userIdent);
        System.out.println("uident cookie---"+uiden);
       try {
            System.out.println("进入token验证。。。");
            claims = getClaims(jwt);
            //exp = claims.getExpiration();
            System.out.println("过期时间" + sdf.format(claims.getExpiration()));
            if (!uiden.equals(claims.get(userIdent))) {
                System.out.println("userIdent不正确。。。"+claims.get(userIdent));
                return false;
            }
            System.out.println("userIdent正确..."+claims.get(userIdent));
            String uidContact = "uid" + claims.getSubject() + "token";
            try {
                getRedisAndDelayToken(uidContact, jwt);
                return true;
            } catch (Exception ee) {
                throw new BizException("500", "缓存服务器错误,请稍后重试");
            }
        } catch (ExpiredJwtException e) {
            System.out.println("抛出token过期错误");
            System.out.println("过期时间" + sdf.format(e.getClaims().getExpiration()));
            String uidContact = "uid" + e.getClaims().getSubject() + "token";
            try {
                getRedisAndDelayToken(uidContact, jwt);
                return true;
            } catch (Exception ee) {
                throw new BizException("500", "缓存服务器错误,请稍后重试");
            }

        } catch (Exception e) {
            return false;
        }
    }
    /*
    public Boolean validateToken(String token, UserDetails userDetails) {
        SecurityUserDetails user = (SecurityUserDetails) userDetails;
        final String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername())
                && !isTokenExpired(token)
        );
    }
     */
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    //从token的解析出claims  主体
    public Claims getClaims(String token) {
        Claims claims = null;
        try {
            claims = getAllClaimsFromToken(token);
        } catch (ExpiredJwtException e) {
            log.info("getClaimFromToken进入过期错误 使用e.getClaims()");
            claims = e.getClaims();
        } catch (Exception ee) {
            log.error("getClaims错误:" + ee.getMessage());
        }
        return claims;
    }
    public  T getClaimFromToken(String token, Function claimsResolver) {
        Claims claims = null;
        try {
            claims = getAllClaimsFromToken(token);
        } catch (ExpiredJwtException e) {
            log.info("getClaimFromToken进入过期错误 使用e.getClaims()");
            claims = e.getClaims();
        } catch (Exception ee) {
            log.error(ee.getMessage());
        }
        //Function接口 jdk8新特性 优雅的写法
        return claimsResolver.apply(claims);
    }
    private Claims getAllClaimsFromToken(String token) {
        System.out.println("进入getAllClaimsFromToken");
        return Jwts.parser()
                .setSigningKey(generalKey())
                .parseClaimsJws(token)
                .getBody();
    }

   
    //token没过期 就重新刷新redis token的时间
    public  void getRedisAndDelayToken(String uidContact, String jwt) throws Exception {
        try {
            String redisToken = (String) redisUtil.get(uidContact);
            if (redisToken != null && redisToken.equals(jwt)) {
                //重新设置过期时间20分钟
                
                redisUtil.expire(uidContact, Long.parseLong("" + expiration / 1000L));
                uidContact= uidContact.replace("uid","");
//延长自己的版本号时间 redis中有个mysqlVersion版本号 
//生成token时把mysqlVersion复制到自己版本号中,自己版本号与mysqlVersion不同
//,说明数据库Permission表有变动,检验token时会重新从数据库拿自己的权限,
//再将新的mysqlVersion赋值给自己的版本号
                String uid=uidContact.replace("token","");
                redisUtil.expire("uid"+uid+"pVersion",Long.parseLong("" + userExpiration / 1000L));
                System.out.println("刷新redisToken成功");
            } else {
                System.out.println("token失效,请......");
                throw new BizException("-1", "token不正确,请重新登陆");
            }
        } catch (BizException e) {
            throw new BizException(e.getErrorCode(), e.getErrorMsg());
        } catch (Exception e) {
            throw new BizException("500", "缓存服务器错误,请稍后重试getRedisAndDelayToken");
        }
    }
    public  boolean getRedisToken(String uidContact, String jwt) {
        try {
            String redisToken = (String) redisUtil.get(uidContact);
            if (redisToken != null && redisToken.equals(jwt)) {
                System.out.println("token在redis可以拿到");
                return true;
            } else {
                System.out.println("token在redis未拿到");
                return false;
            }
        } catch (Exception e) {
            System.out.println("token在redis未拿到");
            return false;
        }
    }
    public  SecretKey generalKey() {  //生成jwt要用到密钥 用这个方法生成
        System.out.println("SecretKey中的key是" + secret);
       
        
        byte[] encodedKey = Base64.decodeBase64(secret);//本地的密码解码[B@152f6e2
       
        
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");// 根据给定的字节数组使用AES加密算法构造一个密钥,使用 encodedKey中的始于且包含 0 到前 leng 个字节这是当然是所有。(后面的文章中马上回推出讲解Java加密和解密的一些算法)
        return key;
    }
    //删除redis中的用户token
    public void logoutJWT(String jwt, HttpServletRequest request) {
        //pureValidateToken(jwt,request)?redisUtil.del("uid"+getUsernameFromToken(jwt)+"token"):"2";
        if (pureValidateToken(jwt, request)) {
            log.info("logoutJWT 有效");
            String uid = getUsernameFromToken(jwt);
            try {
                redisUtil.del("uid" + uid + "token");
                redisUtil.del("uid" + uid + "auths");
                redisUtil.del("uid" + uid + "pVersion");
            } catch (Exception e) {
                throw new BizException("500", "缓存服务器错误,请稍后重试logoutJWT");
            }
        }
    }
}

4.1 BizException.java   ---自定义异常类

public class BizException extends RuntimeException {
    /**
     * 错误码
     */
    protected String errorCode;
    /**
     * 错误信息
     */
    protected String errorMsg;
    public BizException() {
        super();
    }
    public BizException(BaseErrorInfoInterface errorInfoInterface) {
        super(errorInfoInterface.getResultCode());
        this.errorCode = errorInfoInterface.getResultCode();
        this.errorMsg = errorInfoInterface.getResultMsg();
    }
    public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
        super(errorInfoInterface.getResultCode(), cause);
        this.errorCode = errorInfoInterface.getResultCode();
        this.errorMsg = errorInfoInterface.getResultMsg();
    }
    public BizException(String errorMsg) {
        super(errorMsg);
        this.errorMsg = errorMsg;
    }
    public BizException(String errorCode, String errorMsg) {
        super(errorCode);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
    public BizException(String errorCode, String errorMsg, Throwable cause) {
        super(errorCode, cause);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
    public String getErrorCode() {
        return errorCode;
    }
    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }
    public String getErrorMsg() {
        return errorMsg;
    }
    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
    public String getMessage() {
        return errorMsg;
    }
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

4.2 BaseErrorInfoInterface   --异常接口,用于下面的枚举类

public interface BaseErrorInfoInterface {
    /** 错误码*/
    String getResultCode();

    /** 错误描述*/
    String getResultMsg();
}

4.3  CommonEnum

public enum CommonEnum implements BaseErrorInfoInterface {
    // 数据操作错误定义
    SUCCESS("200", "成功!"),
    BODY_NOT_MATCH("400","请求的数据格式不符!"),
    SIGNATURE_NOT_MATCH("401","请求的数字签名不匹配!"),
    NOT_FOUND("404", "未找到该资源!"),
    INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
    SERVER_BUSY("503","服务器正忙,请稍后再试!")
    ;
    /** 错误码 */
    private String resultCode;
    /** 错误描述 */
    private String resultMsg;
    CommonEnum(String resultCode, String resultMsg) {
        this.resultCode = resultCode;
        this.resultMsg = resultMsg;
    }
    @Override
    public String getResultCode() {
        return resultCode;
    }
    @Override
    public String getResultMsg() {
        return resultMsg;
    }
}

4.4  GlobalExceptionHandler.java  --全局异常处理类  核心

import org.springframework.data.redis.connection.PoolException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@ControllerAdvice
public class GlobalExceptionHandler {
    //private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    /**
     * 处理自定义的业务异常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value = BizException.class)
    @ResponseBody
    public  ResultBody bizExceptionHandler(HttpServletRequest req, BizException e){
        //logger.error("发生业务异常!原因是:{}",e.getErrorMsg());
        System.out.println("进入Biz异常"+e.getErrorCode()+e.getErrorMsg());
        return ResultBody.error(e.getErrorCode(),e.getErrorMsg());
    }
    /**
     * 处理空指针的异常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =NullPointerException.class)
    @ResponseBody
    public ResultBody exceptionHandler(HttpServletRequest req, NullPointerException e){
        //logger.error("发生空指针异常!原因是:",e);
        return ResultBody.error(CommonEnum.BODY_NOT_MATCH);
    }
    @ExceptionHandler(value =PoolException.class)
    @ResponseBody
    public ResultBody poolException(HttpServletRequest req, PoolException e){
        //logger.error("发生空指针异常!原因是:",e);
        return ResultBody.error("500","redis连接池异常");
    }
    @ExceptionHandler(value =UsernameNotFoundException.class)
    @ResponseBody
    public ResultBody userNotException(UsernameNotFoundException e){
        //logger.error("发生空指针异常!原因是:",e);
        return ResultBody.error("500",e.getMessage());
    }
    @ExceptionHandler(value =AccessDeniedException.class)
    @ResponseBody
    public ResultBody noRightException(AccessDeniedException e){
        //logger.error("发生空指针异常!原因是:",e);
        return ResultBody.error("500",e.getMessage());
    }
    @ExceptionHandler(value =IOException.class)
    @ResponseBody
    public ResultBody ioException(IOException e){
        //logger.error("发生空指针异常!原因是:",e);
        return ResultBody.error("500",e.getMessage());
    }
    /**
     * 处理其他异常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =Exception.class)
    @ResponseBody
    public ResultBody exceptionHandler(HttpServletRequest req, Exception e){
        //logger.error("未知异常!原因是:",e);
        return ResultBody.error(CommonEnum.INTERNAL_SERVER_ERROR.getResultCode(),e.getMessage());
    }
}

4.5  ResultBody.java   ---视图对象  VO 前后端分离全部用这个返回数据给前面

import com.alibaba.fastjson.JSONObject;

public class ResultBody {
    /**
     * 响应代码
     */
    private String code;
    /**
     * 响应消息
     */
    private String message;
    /**
     * 响应结果
     */
    private Object result;
    private String url;
    public ResultBody() {
    }
    public ResultBody(BaseErrorInfoInterface errorInfo) {
        this.code = errorInfo.getResultCode();
        this.message = errorInfo.getResultMsg();
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public Object getResult() {
        return result;
    }
    public void setResult(Object result) {
        this.result = result;
    }
    /**
     * 成功
     *
     * @return
     */
    public static ResultBody success() {
        return success(null);
    }
    /**
     * 成功
     * @param data
     * @return
     */
    public static ResultBody success(Object data) {
        ResultBody rb = new ResultBody();
        rb.setCode(CommonEnum.SUCCESS.getResultCode());
        rb.setMessage(CommonEnum.SUCCESS.getResultMsg());
        rb.setResult(data);
        return rb;
    }
    /**
     * 失败
     */
    public static ResultBody error(BaseErrorInfoInterface errorInfo) {
        ResultBody rb = new ResultBody();
        rb.setCode(errorInfo.getResultCode());
        rb.setMessage(errorInfo.getResultMsg());
        rb.setResult(null);
        return rb;
    }
    /**
     * 失败
     */
    public static ResultBody error(String code, String message) {
        ResultBody rb = new ResultBody();
        rb.setCode(code);
        rb.setMessage(message);
        rb.setResult(null);
        return rb;
    }
    /**
     * 失败
     */
    public static ResultBody error( String message) {
        ResultBody rb = new ResultBody();
        rb.setCode("-1");
        rb.setMessage(message);
        rb.setResult(null);
        return rb;
    }
    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }
}

5.1  JwtUserMapper    ---mapper接口类 Role Permission JwtUser上面的代码有 ,引入即可

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
@Service
public interface JwtUserMapper extends BaseMapper {
    //第一个sql为获取用户所拥有角色
    @Select("select * from  role where id in(select role_id from user_role where user_id = #{uid})")
    Set getUserRoles(@Param("uid") Long uid);
    Set getUserAuths(@Param("uid") Long uid);
    @Select("select * from permission")
    HashSet findAllPermissons();
    @Select("select * from version")
    ArrayList findVersion();
}

5.2  userMapper  --注意修改namespace为你们自己的路径





    
    

6.1 UserService  用户业务类接口

import javax.servlet.http.HttpServletRequest;
import java.util.Map;


public interface UserService {
    
    //使用的是JwtTokenUtil
    public Map login2(Map device,JwtUser user) throws Exception ;
    void logout(String token, HttpServletRequest request)throws Exception;
}

6.2  UserServiceImpl      ---实现类,有冗余重复代码,需要优化,注释里面有思路,可以看看



import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.PoolException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
public class UserServiceImpl implements UserService{


    @Autowired
    private JwtUserMapper jwtUserMapper;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private MyUtils myUtils;
    //token的有效时间 20分钟 可以自定义
    @Value("${jwt.expiration}")
    String exp;
    //生成token会同时生成 用户版本号 此用户版本号和 redis中的
    //权限版本号相同 ,当检测到这两个版本号不同时,说明数据库权限表有变化,
    //这时需要重新从数据库拿最新的用户权限,放到redis
    @Value("${jwt.userVersionExpiration}")
    String userExp;
    //权限版本号名称 在redis中的key
    @Value("${jwt.pVersionName}")
    String pVersionName;
    //用户标识  传给前端生成cookie 检验token时从request中拿到
    //这个cookie,看和token中的claims中的是否相等,
    // 不相等说明这个token可能黑客从用户那里盗取的
    //这个cookie的名字可以在application.yml中配置
    @Value("${jwt.userIdentifier}")
    String userIdent;
    //假的用户标识, 将真的混在其中,也可以在application.yml配置
    //个数,名字都可以自定义
    @Value("${jwt.userIdentFakes}")
    String userIdentFakes;
    @Override
    public Map login2(Map deviceInfo,JwtUser user) throws Exception {
        Map map=new HashMap<>();
        Map mapCookies=new HashMap<>();
        JwtUser user1 =new JwtUser();
        user1.setUsername(user.getUsername());
        //user1.setPass(user.getPassword());
        System.out.println(user.getUsername()+"-----"+user.getPassword());
        QueryWrapper qw = new QueryWrapper();
        qw.setEntity(user1);
        //qw.select("name","pass");//只查询age和name字段
        //从数据库查询用户  之后再比对密码
        JwtUser b = jwtUserMapper.selectOne(qw);
        String token=null;
        if(b!=null){  用的是security的BCryte加密  加密解密 比md5加密安全
            if(!passwordEncoder.matches(user.getPassword(),b.getPassword())){
                System.out.println("密码不匹配。。。");
                return null;
            }
            //将token的生成要素放在jco中
            JwtClainsObject jco=new JwtClainsObject();
            jco.setRandomId(myUtils.randomString(46));
            jco.setSubjectUid(b.getId());
            //5分钟 毫秒
            //jco.setTtlMillis(Constants.JwtConst.EXPMILLIS.getTimee());
            //jwt有效时间20分钟
            jco.setTtlMillis(Long.parseLong(exp));
            jco.setUsername(user.getUsername());
            //生成token
            token=jwtTokenUtil.generateToken2(jco);
            System.out.println("login2的token是"+token);
            try{
               
                redisUtil.set("uid"+b.getId()+"token",token,Long.parseLong(exp)/1000L);
                redisUtil.set("uid"+b.getId()+"pVersion",(String)redisUtil.get(pVersionName),Long.parseLong(userExp)/1000L);
            }catch (Exception e){
                System.out.println("捕捉到redis错误。。。。");
                throw new PoolException("redis连接不行了");
            }
            //MyAesUtils.encrypt(b.getId(),"ppp");
            map.put("token",token);
            mapCookies.put("nickname",b.getUsername());
            mapCookies.put(userIdent,jco.getRandomId());
            String[] fakes=userIdentFakes.split(",");
            for(String f : fakes){
                mapCookies.put(f,myUtils.randomString(myUtils.GetRandomNum(5,46)));
            }
            map.put("cookies",mapCookies);
           
            
            
            System.out.println("正在返回cookies 在login页面");
           
        }
//把token cookies返回到vue前端
        return map;
    }
    @Override
    public void logout(String token, HttpServletRequest request) throws Exception{
        jwtTokenUtil.logoutJWT(token,request);
    }
}

7.0  下面是security的配置类 我没有弄sucessLoginHandler,failLoginHandler之类的,但还是有必要的,可以在里面做一些日志记录的业务

7.1  JwtAuthenticationEntryPoint.java  ---用户没有权限时,走这个


import com.alibaba.fastjson.JSON;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    //当用户无权限时时  走这步
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException)
            throws IOException, ServletException {


        log.error("JwtAuthenticationEntryPoint不通过:"+authException.getMessage());
        //response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"没有凭证");
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(ResultBody.error("500","权限不足")));
    }
}

7.2 SecurityUserDetails.java  --封装用户权限信息,继承SysUser(上面2.5代码有),实现UserDetails,可以在SysUer中加入自定义的信息,比如是否单身isSingle,户籍所在String birthPlace等等,当然SecurityUserDetails构造方法也要增加SecurityUserDetails(boolean isSingle,String birthPlace)

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@EqualsAndHashCode(callSuper = false) //不调用父类的equals方法
@Accessors(chain = true)
public class SecurityUserDetails extends SysUser implements UserDetails {
    private Collection authorities;
    @Override
    public Collection getAuthorities() {
        return authorities;
    }
    //一号构造方法
    public SecurityUserDetails(String uid, Collection authorities){
        this.authorities = authorities;
        this.setUsername(uid);
        
        this.setAuthorities(authorities);
    }
    //二号构造方法
    public SecurityUserDetails(String uid,boolean isSingle, boolean isAccountNonExpired,boolean isAccountLock,Collection authorities){
        this.authorities = authorities;
        this.setUsername(uid);
        this.setAccountNonExpired(isAccountNonExpired);
        this.setSingle(isSingle);
       
        this.setAuthorities(authorities);
    }
    /**
     * 账户是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return this.isAccountNonExpired();        
        //return true;
    }
    /**
     * 是否禁用
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    /**
     * 密码是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

7.3   JwtUserDetailsService.java  ---用户信息实现类,从数据库取用户信息封装到上面的SecurityUserDetails中,如果要判断账户是否过期,是否锁定,可以使用SecurityUserDetails的二号构造方法,构造方法可以自己添加,本人为了测试方便用的是一号构造方法,details是给下面的JwtAuthorizationTokenFilter过滤器用的,如果账户锁定,可以在过滤器中抛出异常

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Service("jwtUserDetailsService")
@Slf4j
public class JwtUserDetailsService implements UserDetailsService {
    //返回userdetails这个主体信息
    @Autowired
    private JwtUserMapper jwtUserMapper;
    @Autowired
    private RedisUtil redisUtil;
    @Value("${jwt.pVersionName}")
    String pVersionName;
    @Value("${jwt.userVersionExpiration}")
    String userExp;
    @Override   //在这个方法中可以判断账户的情况 用不同SecurityUserDetails构造方法
    public UserDetails loadUserByUsername(String uid) throws UsernameNotFoundException {
        System.out.println("JwtUserDetailsService:" + uid);
        List authorityList  = new ArrayList<>();
        //QueryWrapper qw = new QueryWrapper();
        Set userRoles=(Set)redisUtil.get("uid"+uid+"auths");
        String redisUserVersion=(String)redisUtil.get("uid"+uid+"pVersion");
        String reidsRolePVersion=(String)redisUtil.get(pVersionName);
        //如果在生成token时生成的用户版本号与 redis中 权限版本号不同,说明permission表有变动
        //需要重新从数据库拿用户权限 如果相同直接从redis中拿权限
        if(userRoles==null ||redisUserVersion==null || reidsRolePVersion==null ||!redisUserVersion.equals(reidsRolePVersion) ){
            log.info(">>>>>>>>>>>从数据库取userRoles");
            userRoles= jwtUserMapper.getUserAuths(Long.parseLong(uid) );
            // 用户权限保存时间为一个小时 可以将3600放在application.yml中
            redisUtil.set("uid"+uid+"auths",userRoles,3600);
            redisUtil.set("uid"+uid+"pVersion",reidsRolePVersion,Long.parseLong(userExp)/1000L  );
        }
        if(userRoles!=null){
            log.info(">>>>>>>>>>>进入JwtUserDetailsService");
            //箭头写法相当于for循环 role代表集合中的一个元素
            userRoles.forEach((role)->authorityList.add(new SimpleGrantedAuthority(role.getName())));
            //authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
            return new SecurityUserDetails(uid,authorityList);
        }
        return new SecurityUserDetails(uid,null);
    }
}

7.4  MyInvocationSecurityMetadataSourceService.java  ----安全元数据,从数据库把url对应的所需权限封装到Collection集合中,供后面的decisionManage决策器使用,结合redis进行缓存


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@Service
@Slf4j
public class MyInvocationSecurityMetadataSourceService  implements
        FilterInvocationSecurityMetadataSource {
    @Autowired
    private JwtUserMapper jwtUserMapper;
    @Autowired
    private RedisUtil redisUtil;
    private HashMap> map = null;
    /**
     * 加载权限表中所有权限
     */
    public void loadResourceDefine() {
        map = new HashMap<>();
        Collection array;
        ConfigAttribute cfg;
        HashSet permissions = null;
        if((permissions=(HashSet)redisUtil.get("AllAuths"))==null){
            permissions = jwtUserMapper.findAllPermissons();
            log.info("从数据库取AllAuths");
            redisUtil.set("AllAuths",permissions);
        }else{
            log.info("从redis取AllAuths");
        }
        for (Permission permission : permissions) {
            array = new ArrayList<>();
            //cfg代表一个权限
            cfg = new SecurityConfig(permission.getName());
            //此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
            array.add(cfg);
            log.info("装填permission表中所有权限。。。"+permission.getName());
            //用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
            map.put(permission.getUrl(), array);
        }
    }
    //此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        if (map == null) loadResourceDefine();
        //object 中包含用户请求的request 信息
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        String resUrl;
        for (Iterator iter = map.keySet().iterator(); iter.hasNext(); ) {
            resUrl = iter.next();
            //resUrl是AllAuths里面的路径
            matcher = new AntPathRequestMatcher(resUrl);
            //这个是模糊匹配 比如客户端request中传来的url是/user/login?username=aaa&pass=bbb,而resUrl是/login
            //都能匹配成功
            if (matcher.matches(request)) {
                log.info("url匹配成功。。。"+resUrl);
                //返回此url所需的权限 给DecisionManager
                return map.get(resUrl);
            }
        }
        return null;
    }
    @Override
    public Collection getAllConfigAttributes() {
        return null;
    }
    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

7.5  MyAccessDecisionManager.java  --决策管理器,decide方法中authentication表示用户所具有的权限,比如该用户有ROLE_CHECK审核权限,ROLE_MANAGER管理权限,从jwtUserDetailService中得到! configAttributes表示从客户端传来的url所对应的所有权限,通常只有一个,比如ROLE_MANAGER,configAttributes由上面的MetadataSourceService元数据获得,如果configAttributes为null,说明该url对所有用户开放,即使没登陆也可以访问

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Iterator;
@Service
@Slf4j
public class MyAccessDecisionManager implements AccessDecisionManager {
    // decide 方法是判定是否拥有权限的决策方法,
    //authentication 是释jwtUserDetailService中循环添加到 GrantedAuthority 对象中的权限信息集合.
    //object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
    //configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        if(null== configAttributes || configAttributes.size() <=0) {
            log.info("configAttributes为空。。。");
            return;
        }
        ConfigAttribute c;
        String needRole;
        for(Iterator iter = configAttributes.iterator(); iter.hasNext(); ) {
            c = iter.next();
            needRole = c.getAttribute();
            for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
                if(needRole.trim().equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("no right");
    }
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }
    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

7.6  JwtAuthorizationTokenFilter.java  --1号过滤器,一共有两个,另一个是MyFilterSecurityInterceptor

,看作2号过滤器,1号判断url的request中有没有token,有就从token中解析出用户id,给jwtUserDetailService从数据库拿该用户的所有权限,没有token,那就判断该url是否需要权限,如果需要权限,那就通知前端用户登陆.不需要权限就执行chain.doFilter(request, response);进入下一个过滤器,也就是2号过滤器,还有进入2号过滤器之前,一定要在Security上下文中添加用户权限,也就是SecurityUserDetails

,如果用户什么权限都没有,那就这样new SecurityUserDetails(null,null);

,不然会报空指针NullPoint异常,

,这个1号过滤器有代码冗余,可以自己优化一下,过滤器可以按需添加,之后按顺序放到SecurityConfig.java配置文件中即可

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
@Component
@Slf4j                     //1号过滤器
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
    @Autowired
    @Qualifier("jwtUserDetailsService")
    private  UserDetailsService userDetailsService;
    @Autowired
    private  JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.token}")
    private  String tokenHeader;
    @Value("${jwt.loginPage}")
    private  String loginPage;
    @Value("${jwt.AllAuths}")
    private  String allAuths;
    @Autowired
    private  RedisUtil redisUtil;
    public JwtAuthorizationTokenFilter() {
        //this.userDetailsService = userDetailsService;
        //this.jwtTokenUtil = jwtTokenUtil;
        //this.tokenHeader = tokenHeader;
    }
//ServletException, IOException
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        log.info("执行到JwtAuthorizationTokenFilter");
        final String requestHeader = request.getHeader(this.tokenHeader);
        System.out.println("请求头是"+requestHeader+"全路径"+request.getRequestURL());
        String uid = null;
        String jwt = null;
        //&& requestHeader.startsWith("Bearer ")
        if (StringUtils.isNotBlank(requestHeader) ) {
            jwt = requestHeader;
            System.out.println("头不为空");
            try {
                uid = jwtTokenUtil.getUsernameFromToken(jwt);
            } catch (Exception e) {
                log.error("jwtFilter捕捉到错误"+e.getMessage());
            }
        }else{
            HashSet permissions= null;
            try {
                permissions = (HashSet) redisUtil.get(allAuths);
            } catch (Exception e) {
                log.info("错误"+e.getMessage());
            }
            if(permissions!=null){
                log.info("无token 进入匹配。。。");
                AntPathRequestMatcher matcher;
                boolean flag=false;
                for(Permission p:permissions){
                    matcher = new AntPathRequestMatcher(p.getUrl());
                    if (matcher.matches(request)) {
                        log.info("JwtAuthorizationTokenFilter url匹配成功。。。"+p.getUrl());
                        //说明该路径要权限 通知前端登陆
                        flag=true;
                        throw new IOException("权限不足,请登陆。。。");

                    }
                }
                if(!flag){
                    log.info("该路径不需要权限。任何人都可访问。。");
                    UserDetails userDetails=new SecurityUserDetails(null,null);
                    //在Security上下文中添加userDetails 不能直接添加,而是放到UsernamePasswordAuthenticationToken中
                    SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()));

                    chain.doFilter(request, response);
                }
            }
        }

//if (uid != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        if (uid != null ) {
            System.out.println("token解析成功,拿到uid。。。"+uid);
            //返回用户所有的权限
            UserDetails userDetails = null;
            try {
                if (jwtTokenUtil.pureValidateToken(jwt,request)) {
                    log.info("进入上下文主体。。。jwt有效");
                    userDetails = userDetailsService.loadUserByUsername(uid);
                    
                    
                }else{
                    log.info("进入上下文主体。。。jwt无无无无效");
                    userDetails=new SecurityUserDetails(null,null);
                    
                    
                }
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
                System.out.println("到了.....jwtFilter的最后。。。");

            }catch (Exception e){
                //redis可能会宕机 
                log.error("JwtAuthorizationTokenFilter token 过期:"+e.getMessage());
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
                System.out.println(request.getRequestURI());
                
            }
            chain.doFilter(request, response);
        }
       
    }
}

7.7  MyFilterSecurityInterceptor.java  ---2号过滤器,里面用decisonManager决策器验证用户是否有权限

package com.tuu.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;
import javax.servlet.*;
import java.io.IOException;
@Service
@Slf4j
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("执行到MyFilterSecurityInterceptor");
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
//执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
    @Override
    public void destroy() {
    }
    @Override
    public Class getSecureObjectClass() {
        return FilterInvocation.class;
    }
    @Override    //加载MetaSource元数据  也就是Permission表中所有数据
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

7.8   SecurityConfig.java  ---Security配置类,过滤器就加在这里面

package com.tuu.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity// 这个注解必须加,开启Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//保证post之前的注解可以使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Autowired
    JwtUserDetailsService jwtUserDetailsService;
    @Autowired
    JwtAuthorizationTokenFilter authenticationTokenFilter;
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
    //先来这里认证一下
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(jwtUserDetailsService); //user Details Service验证
    }
    //拦截在这配
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                .authorizeRequests()  定义哪些URL需要被保护、哪些不需要被保护 因为是基于JDBC动态验证权限,所以不用写死  下面都打了注释
                //.antMatchers("/**").permitAll()
                //.antMatchers("/login").permitAll()
                //.antMatchers("/login2").permitAll()
                //.antMatchers("/createPass").permitAll()
                //.antMatchers("/checkTokenCookie2").permitAll()
                //.antMatchers("/logout").permitAll()
                //.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
                .anyRequest().authenticated()// 剩下所有的验证都需要验证
                .and()
                .csrf().disable()                      // 禁用 Spring Security 自带的跨域处理
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 定制我们自己的 session 策略:调整为让 Spring Security 不创建和使用 session
        //注册过滤器  
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
    }
//这是密码验证Bean 在UserServiceImpl中用到 BCrypt加密比md5加密安全
    @Bean
    public PasswordEncoder passwordEncoderBean() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

8.0  UserController.java  ---控制器

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserService userService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private  JwtTokenUtil jwtTokenUtil;
    @Autowired
    private RedisTemplate redisTemplate;
    @Value("${jwt.pVersionName}")
    String pVersionName;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @PostMapping("/login2")
    public Object login2(@RequestBody JwtUser loginInfo, HttpServletRequest request) throws Exception {
        System.out.println("进入login2");
        
        BrowerUtils bu = BrowerUtils.getBrowerInfo(request);
        //生成token后,
        Map deviceInfo = new HashMap<>();
        
        System.out.println("浏览器" + bu.getBrowserName());
        System.out.println("系统" + bu.getOperatingSystem());
        System.out.println("浏览器版本" + bu.getBrowserVersion());
        String token = null;
        Map map = null;
        map = userService.login2(deviceInfo,loginInfo);
        //deviceInfo这个本来想把用户设备信息也放到token的Claims中 后面放弃了
        deviceInfo=null;
        if (map.get("token") != null) {
            System.out.println("token已经产生"+map.get("token"));
            return ResultBody.success(map);
        }
        return ResultBody.error("用户名密码错误,请重试");
    }
    
    @GetMapping("/pureCheckToken")
    public Object pureCheckToken() {
        System.out.println("进入pureCheckToken");
        return ResultBody.success("pureCheckToken_token有效");
    }
    
    @GetMapping("/logout2")
    public ResultBody logout(@RequestParam("token") String token, HttpServletRequest request) throws Exception {
        
        System.out.println("进入logout。。。。");
        userService.logout(token, request);
        return ResultBody.success("退出成功。。。");
    }
   
    //@PreAuthorize("hasAnyRole('AAA')")
    @GetMapping(value = "/aaab")
    public ResultBody testNeed() {
        return ResultBody.success("hasAnyRole。。。AAA");
    }
    //@PreAuthorize("hasAnyRole('BBB')")
    @GetMapping(value = "/bbba")
    public ResultBody bbb() {
        return ResultBody.success("hasAnyRole。。。BBB");
    }
    @GetMapping(value = "/sendRedis")
    public ResultBody sendRedis() {
        //publish user_role bbb 在redis客户端给主题发消息
        redisTemplate.convertAndSend("user_role",String.valueOf(Math.random()));
        redisTemplate.convertAndSend("role_permission",String.valueOf(Math.random()));
        return ResultBody.success("sendRedis成功");
    }
    @GetMapping(value = "/annymbb")
    public void annym() {
        Random r=new Random();
        redisUtil.set(pVersionName,String.valueOf(Math.random()));
        System.out.println("role_permission版本号是"+redisUtil.get(pVersionName));
    }
}

9.0  启动类  --修改@MapperScan为自己的

package com.tuu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.tuu.mapper")
public class SpringjwApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringjwApplication.class, args);
	}
}

10.0  修改windows  的hosts 加入两个映射

#   A网站  看作淘宝
127.0.0.1 www.springbootjwt.com
#   验证中心
127.0.0.1 www.sso.com

11.0  nginx.conf  也是加入两个server

server{
		listen 80;
		server_name www.springbootjwt.com;
		location / {
            # 反向到vue前端
			proxy_pass http://localhost:3000;
			#try_files $uri $uri/ /index.html;
			#设置超时 访问下一台服务器
			proxy_connect_timeout       450;  
			proxy_read_timeout          450;  
			proxy_send_timeout          450;
			proxy_http_version 1.1;
			#proxy_set_header        X-Real-IP       $remote_addr;
			proxy_set_header        Host            $host;
			proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_pass_request_headers              on;
			
		}
		
	}
	server{
		listen 80;
		server_name www.sso.com;
		location / {
			proxy_pass http://localhost:5000;
			#try_files $uri $uri/ /index.html;
			#设置超时 访问下一台服务器
			proxy_connect_timeout       450;  
			proxy_read_timeout          450;  
			proxy_send_timeout          450;
			proxy_http_version 1.1;
			#proxy_set_header        X-Real-IP       $remote_addr;
			proxy_set_header        Host            $host;
			proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_pass_request_headers              on;
			
		}
		
	}

12  接下来做vue前端www.springbootjwt.com

用的是vue-Cli3脚手架,vue2......把下面代码复制,然后用在命令行 输入cnpm install安装所需依赖,安装完再输入npm run serve命令就能启动了

12.1 目录结构

(原创)springboot+springsecurity+redis+jwt+vue+iframe 做一个前后端分离的SSO单点登陆鉴权系统 类似淘宝天猫单点登陆_第1张图片(原创)springboot+springsecurity+redis+jwt+vue+iframe 做一个前后端分离的SSO单点登陆鉴权系统 类似淘宝天猫单点登陆_第2张图片

12.2  vue.config.js

const CopyWebpackPlugin = require('copy-webpack-plugin')
    
module.exports = {
    //baseUrl:'/springjwt/',
    //如果是生产环境就是./  如果是开发环境就是 /
    publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
    outputDir: 'dist',
    assetsDir: 'static',
    filenameHashing: true,
   
    pages: {
        index: {
            // entry for the pages
            entry: 'src/main.js',
            // the source template
            template: 'src/pages/index/index.html',
            // output as dist/index.html
            filename: 'index.html',
            // when using title option,
            // template title tag needs to be <%= htmlWebpackPlugin.options.title %>
            title: '首页',
            // chunks to include on this pages, by default includes
            // extracted common chunks and vendor chunks.
            chunks: ['chunk-vendors', 'chunk-common', 'index']
        },
        
        error: {
            // entry for the pages
            entry: 'src/pages/error/error.js',
            // the source template
            template: 'src/pages/error/error.html',
            // output as dist/index.html
            filename: 'error.html',
            // when using title option,
            // template title tag needs to be <%= htmlWebpackPlugin.options.title %>
            title: '异常错误',
            // chunks to include on this pages, by default includes
            // extracted common chunks and vendor chunks.
            chunks: ['chunk-vendors', 'chunk-common', 'error']
        },
        manager: {
            // entry for the pages
            entry: 'src/pages/manager/manager.js',
            // the source template
            template: 'src/pages/manager/manager.html',
            // output as dist/index.html
            filename: 'manager.html',
            // when using title option,
            // template title tag needs to be <%= htmlWebpackPlugin.options.title %>
            title: '后台管理',
            // chunks to include on this pages, by default includes
            // extracted common chunks and vendor chunks.
            chunks: ['chunk-vendors', 'chunk-common', 'manager']
        },
        
    },
   
    // eslint-loader 是否在保存的时候检查
    lintOnSave: false,
    // 是否使用包含运行时编译器的Vue核心的构建
    runtimeCompiler: false,
    // 默认情况下 babel-loader 忽略其中的所有文件 node_modules
    transpileDependencies: [],
    // 生产环境 sourceMap
    productionSourceMap: false,
    // cors 相关 https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors
    // corsUseCredentials: false,
    // webpack 配置,键值对象时会合并配置,为方法时会改写配置
    // https://cli.vuejs.org/guide/webpack.html#simple-configuration
    configureWebpack: (config) => {
		// 用这个插件将WEB-INF文件夹打包进去,WEB-INF里面放404页面
        config.plugins.push(
            new CopyWebpackPlugin([{
                        from: 'WEB-INF/',
                        to: 'WEB-INF'
                    }
                ]), )
    },
    // webpack 链接 API,用于生成和修改 webapck 配置
    // https://github.com/mozilla-neutrino/webpack-chain
    chainWebpack: (config) => {
        // 因为是多页面,所以取消 chunks,每个页面只对应一个单独的 JS / CSS
        config.optimization
        .splitChunks({
            cacheGroups: {}
        });
       
    },
    // 配置高于chainWebpack中关于 css loader 的配置
    css: {
        // 是否开启支持 foo.module.css 样式
        modules: false,
        // 是否使用 css 分离插件 ExtractTextPlugin,采用独立样式文件载入,不采用 

12.10  manager.html ---接下来是管理员界面,没有token,或者没有权限没有登陆,都不显示manager的内容   ---目录路径-''根目录/src/pages/manager/manager.html'



  
    
    
    
    
    <%= htmlWebpackPlugin.options.title %>
  
  
    
    

12.11  manager.js  ---manger.html的入口文件,用来将manager.vue加载到manager.html中

没有权限或者未登陆都不会加载,跟manager.html同目录下

import Vue from 'vue'
import Manager from './manager.vue'
import router from './manager.router'
import axios from '../../http'
import 'element-ui/lib/theme-chalk/index.css';
import ElementUI from 'element-ui';
import utils from '../../utils/utils';
import { pureCheckToken } from '../../utils/cheToken';
Vue.use(ElementUI);
Vue.config.productionTip = false
//这样可以在各个组件中使用axios
Vue.prototype.$axios = axios;

utils.setIframe().then(resole=>{
    //第一个参数为true,表示需要验证token和权限才能加载manager.vue,
//验证不通过就跳转到sso登陆页面
  utils.addEve(true,Vue,new Vue({
    router,
    render: h => h(Manager)
  }),'#manager')
})

12.11.1  manager.vue


12.12  cheToken.js  --验证token工具类  路径''根目录/src/utils/cheToken.js'

import axios from '../http'
import utils from './utils'

export function pureCheckToken(){
    
    var flag = null;
    console.log('进入pureCheckToken...');
    var toToken = null;
	//从localStorage获取token
    if (localStorage.getItem('token')) {
        toToken = localStorage.getItem('token')
        //return 888;
    }
    console.log("toToken===" + toToken)
    if (toToken) {  //没有就返回Promise.resolve("no"); utils接收到no这个参数在then中跳转到SSO
        //使用了vue的proxyTable进行跨域访问,看vue.config.js中配置
        flag=axios.get('/api/pureCheckToken').then((res) => {
            if (res.data.code && res.data.code == "200") {
                console.log("message是"+res.data.message);
                return Promise.resolve("ok")
                //return "ok";
            }else if(res.data.code == "500"){
				//返回这个就跳转到错误页面
                return Promise.resolve("service_error");
            }else{
                
                console.log("不等于200...");
                return Promise.resolve("no");
            }
        }).catch((err) => {
            //Message.error(response.data.message);
            //return Promise.reject(err);
            console.log("carch Error 请求失败。。。");
            return Promise.resolve("no");
        })
    } else {
        flag=Promise.resolve("no");
    }
    return flag;
}

12.13 utils.js ---通用工具类  --路径''根目录/src/utils/utils.js'

import { isRejected } from "q";
import { pureCheckToken } from './cheToken';
/* eslint-disable 

import utils from '../../assets/scripts/utils'
// Vue.prototype.$utils = utils // main.js中全局引入
let id = utils.getUrlKey('id')
*/
export default {
    getUrlKey: function (name) {
        return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [, ""])[1].replace(/\+/g, '%20')) || null
    },
    setCookie: function (cname, cvalue, exdays) {
        var d = new Date();
        //console.log(d);
        //d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
        //单位是1秒
        d.setTime(d.getTime() + (exdays * 1000));
        //var expires = "expires=" + d.toUTCString();
        var expires = "expires=" + d.toUTCString();

        //console.info(cname + "=" + cvalue + "; " + expires);
        document.cookie = cname + "=" + cvalue + ";domain=.springbootjwt.com" + "; " + expires;
        //console.info(document.cookie);
    },
    setCookieD: function (cname, cvalue, domain, exdays) {
        var d = new Date();
        console.log(d);
        //d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
        d.setTime(d.getTime() + (exdays * 1000));
        //var expires = "expires=" + d.toUTCString();
        var expires = "expires=" + d;

        console.info(cname + "=" + cvalue + "; " + expires);
        document.cookie = cname + "=" + cvalue + "; " + "domain" + "=" + domain + "; " + expires;
        //console.info(document.cookie);
    },
    //获取cookie
    getCookie: function (cname) {
        var name = cname + "=";
        var ca = document.cookie.split(';');
        console.log("获取cookie,现在循环")
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            //console.log(c)
            while (c.charAt(0) == ' ') c = c.substring(1);
            if (c.indexOf(name) != -1) {
                return c.substring(name.length, c.length);
            }
        }
        return "";
    },
    //清除cookie
    clearCookie: function () {
        this.setCookie("username", "", -1);
    },
    //清除cookie
    delCookie: function (name) {
        this.setCookie(name, "", -1);
    },
    //把url后面的参数去掉
    delUrlParams: function (url) {
        //如果没有? 会返回-1
        //console.log("?????===="+url.indexOf('?'))
        if (url.indexOf('?') != -1) {
            return url.substring(0, url.indexOf('?'));
        } else {
            return url
        }

    },
    //得到url域名host
    getUrl: function (url) {
        var domain = url.split('/'); //以“/”进行分割
        if (domain[2]) {
            domain = domain[0] + '//' + domain[2];
        } else {
            domain = ''; //如果url不正确就取空
        }
    },

    //加载一个隐藏的iframe
    setIframe: function () {
        var ifr = document.createElement('iframe');
        ifr.id = "myframeId";
        ifr.src = 'http://www.sso.com';
        ifr.name = "myframeName";
        ifr.style.display = 'none';
        document.body.appendChild(ifr);
        console.log('0000000-----ifr.onload之前');

        return Promise.resolve("89889");
    },
    //添加监听器  接收SSO的消息
    addEve: function (needCretential,Vue, VueObj, comId) {
        console.log('111111-----addEve');
        let ssoflag = Vue.prototype.sssoflag = { flag: null };
        let pureCOk = Vue.prototype.pureCheckOk = { flag: null };
        //let ssoflag = {flag:null};
        let _that = this;
        //监听pureOK这个变量的变化,有变化就加载vue组件
        Object.defineProperty(pureCOk, 'flag', {
            get: function () {

            },
            set: function (newValue) {
                //收到变化 加载组件 
                VueObj.$mount(comId)
                console.log("55555挂载成功")
            }
        })
        //ssoflag有变化说明从SSO拿到消息,
        //并且token已经保存到localStorage中
        //pureCheckToken()验证token是否有效
        Object.defineProperty(ssoflag, 'flag', {
            get: function () {

            },
            set: function (newValue) {
                console.log("33333执行defineProperty")
                //需要触发的渲染函数可以写在这...
                pureCheckToken().then(data => {
                    console.log("4444444执行pureCheckToken后面")
                    if (data == "ok") {

                        console.log("data是" + data)
                        pureCOk.flag = "ok";
                        return Promise.resolve("addEvent");
                        //跑到首页
                        //window.location.href=document.location.protocol+"//"+window.location.host
                        //window.location.href = utils.delUrlParams(window.location.href);
                    } else if (data == "service_error") {
                        console.log("跳转error");
                        window.location.href = "/error"
                        //window.location.href = "http://www.sso.com/login?redirect=" + utils.delUrlParams(window.location.href);
                    } else {
                        console.log("data是" + data)
                        window.location.href = "http://www.sso.com/login?redirect=" + _that.delUrlParams(window.location.href);

                    }
                })
            }
        });
        //监听器   从sso拿token和cookie
        //监听器是异步的 所有才用上面的Object.defineProperty监听变量的变化
        window.addEventListener('message', (event) => {
            if (event.origin.includes('sso.com')) {
                console.log('接受到sso的消息');
                localStorage.setItem('token', event.data.token);
                //if (!this.getCookie('nickname')) {
                // this.setCookie('nickname', event.data.nickname, 1296000);
                //}
                let cookies = event.data.cookies;
                
                if (cookies) {
                    for (let k in cookies) {
                        //console.log(k, cookies[k]);
                        _that.setCookie(k, cookies[k], 1296000)
                    }

                }
                //伪造一些无用的cookie,
                _that.fillCookie()
                console.log("222222-addEventListener");
                if(needCretential){
                    //needCretential为true就验证token才显示组件
                   
                    ssoflag.flag = 'ok';
                }else{
                    //不要验证 直接显示组件
                    pureCOk.flag = "ok";
                }
                
                
            };

        }, true);

        //return Promise.resolve("addEvent");
    },
    //得到随机整数
    GetRandomNum(Min, Max) {
        var Range = Max - Min;
        var Rand = Math.random();
        //round +0.5  再取整
        return (Min + Math.round(Rand * Range));
    },
    //随机字符串
    randomString(len) {
        len = len || 46;
        //把t字母去掉,因为验证token时用到一个cookie,key是tRoU,
        //伪造cookie时 要避免把tRoU覆盖
        var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprswxyz2345678';    /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
        var maxPos = $chars.length;
        var pwd = '';
        for (let i = 0; i < len; i++) {
            pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
        }
        return pwd;
    },
    //伪造cookie 把cookie数量填充至30个,可以自定义数量
    fillCookie() {
        var cookieArry = document.cookie.split(";");
        var setCookNum;
        if (cookieArry.length < 30) {
            setCookNum = 30 - cookieArry.length;
        }

        console.log("cookie数---" + setCookNum)

        for (let i = 1; i <= setCookNum; i++) {
            let k = this.randomString(this.GetRandomNum(1, 6));
            let kvalue = this.randomString(this.GetRandomNum(5, 46));
            //15天 21600分钟
            this.setCookie(k, kvalue, 1296000)
        }
    }
}

12.14  web.xml  --设置404页面---路径  '根目录/WEB-INF/web.xml',这个页面无法打包进项目,要安装一个插件const CopyWebpackPlugin = require('copy-webpack-plugin'),具体看vue.config.js



	Router for Tomcat
	        
		404
		/index.html 
	

13.0  下面是SSO验证中心的vue前端代码,也就是登陆页面, 这个SSO验证中心前端简单,就一个首页和login登陆页面, 在其他地方新建一个SSO文件夹

13.1  vue.config.js    ---SSO

const CopyWebpackPlugin = require('copy-webpack-plugin')

var vuecookies = require('./src/utils/cooInfo');
var axios = require('axios');
function getCookie(regg, cname) {
  var name = cname + "=";
  //var ca = document.cookie.split(';');
  var ca = regg.headers.cookie.split(';');
  console.log("获取cookie,现在循环")
  for (var i = 0; i < ca.length; i++) {
    var c = ca[i];
    //console.log(c)
    while (c.charAt(0) == ' ') c = c.substring(1);
    if (c.indexOf(name) != -1) {
      return c.substring(name.length, c.length);
    }
  }
  return "";
}
module.exports = {
  //baseUrl:'/springjwt/',
  //如果是生产环境就是./  如果是开发环境就是 /
  publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
  outputDir: 'dist',
  assetsDir: 'static',
  filenameHashing: true,
  pages: {
    index: {
      
      entry: 'src/main.js',
      
      template: 'src/pages/index/index.html',
      filename: 'index.html',
      title: '首页',
      chunks: ['chunk-vendors', 'chunk-common', 'index']
    },
    login: {
      entry: 'src/pages/login/login.js',
      template: 'src/pages/login/login.html',
      filename: 'login.html',
      title: '登陆页面',
      chunks: ['chunk-vendors', 'chunk-common', 'login']
    }
  },
  // eslint-loader 是否在保存的时候检查
  lintOnSave: false,
  // 是否使用包含运行时编译器的Vue核心的构建
  runtimeCompiler: false,
  // 默认情况下 babel-loader 忽略其中的所有文件 node_modules
  transpileDependencies: [],
  // 生产环境 sourceMap
  productionSourceMap: false,
 
  configureWebpack: (config) => {
    config.plugins.push(
      new CopyWebpackPlugin([{ from: 'WEB-INF/', to: 'WEB-INF' }]),
    )
  },
  // webpack 链接 API,用于生成和修改 webapck 配置
  // https://github.com/mozilla-neutrino/webpack-chain
  chainWebpack: (config) => {
    // 因为是多页面,所以取消 chunks,每个页面只对应一个单独的 JS / CSS
    config.optimization
      .splitChunks({
        cacheGroups: {}
      });
  },
  // 配置高于chainWebpack中关于 css loader 的配置
  css: {
    // 是否开启支持 foo.module.css 样式
    modules: false,
    // 是否使用 css 分离插件 ExtractTextPlugin,采用独立样式文件载入,不采用 

13.3  main.js  ----路径  根目录/src/main.js       ---SSO

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import axios from './http'
import 'element-ui/lib/theme-chalk/index.css';
import ElementUI from 'element-ui';
import utils from './utils/utils';
Vue.use(ElementUI);
Vue.config.productionTip = false
//这样可以在各个组件中使用axios
Vue.prototype.$axios=axios;
//Vue.prototype.jsEncrypt = JsEncrypt
new Vue({
  router,
  
  render: h => h(App)
}).$mount('#app')
//伪造cookie  并通过postMessage发送给父页面www.springbootjwt.com postMessage是常用跨域手段之一
var data=utils.fillCookReturnData()
window.onload=function(){
  window.parent.postMessage(data,'*');
}

13.3.1 index.html  路径 根目录/src/pages/index/index.html  ---SSO



  
    
    
    
    
    SSO
  
  
    

13.4  login.html  路径 根目录/src/pages/login/login.html  ---SSO



  
    
    
    
    
    <%= htmlWebpackPlugin.options.title %>
  
  
 

13.5  login.js  入口文件  跟login.html同目录  ---sso

import Vue from 'vue'
import Login from './Login.vue'
import router from './login.router'
import axios from '../../http'
import {Message,Loading} from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css';
import { cheeToken } from '../../utils/ssoCheckToken';
import utils from '../../utils/utils';
import ElementUI from 'element-ui';
Vue.use(ElementUI);
Vue.config.productionTip = false
//这样可以在各个组件中使用axios
Vue.prototype.$axios=axios;
const checToken = cheeToken();
checToken.then(data => {
	//跳到login.html时,通过checToken方法先检测SSO自己有没有登陆,登陆了就
	//返回原来页面,hasRePath用来存储跳过来的页面,没登陆显示登陆页面
  if (data == "invalid") {
	  //显示页面
    new Vue({
      router,
      render: h => h(Login)
    }).$mount('#login')
  } else {
	  //验证成功,token有效,跳到回调地址 没有回调地址就跳到WWW.SSO.COM首页
	  //检查url路径中是否有回调地址 
    var hasRePath=null;
    if(hasRePath=utils.getUrlKey("redirect")){
      var domain=null;
	  //判断回调地址是否是http://xxx.com/变样的形式
      if(domain=utils.getUrl(hasRePath)){
        console.log('domain='+domain)
        window.location.href =hasRePath;
      }
    }else{
      window.location.href = "http://www.sso.com/" ;
    }
    
  }
})

13.6  Login.vue    ----跟login.html同目录   ---SSO


13.7  ssoCheckToken.js  --工具类  检验token是否有效 目录 '根目录/src/utils/ssoCheckToken.js

import axios from '../http'
export function cheeToken(){
    var flag = null;
    console.log('进入SSO checkToken...');
    var toToken = null;
    if(!localStorage.getItem('token')){
        //没有token就无效  就显示登陆页面
        return Promise.resolve("invalid")
    }
    toToken=localStorage.getItem('token');
    console.log("SSO toToken===" + toToken)
    if (toToken) {
         //checkTokenCookie
        flag=axios.get('/api/pureCheckToken').then((res) => {
            if (res.data.code == "200") {
                console.log("SSO TOKEN 有效。"+res.data.code+"---"+res.data.message);
                return Promise.resolve("valid")
            } else {
                console.log("SSO TOKEN 无效");
                return Promise.resolve("invalid")
            }
        }).catch((err) => {
            console.log("carch Error 请求失败。。。");
            return Promise.resolve("invalid");
        })
    } 
    return flag;
}

13.8  utils.js  通用工具类  跟ssoCheckToken.js同目录

export default {
	//从url中得到参数
    getUrlKey: function (name) {
        return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [, ""])[1].replace(/\+/g, '%20')) || null
    },
    //exdays  原单位是毫秒
    setCookie: function (cname, cvalue, exdays) {
        var d = new Date();
        //几分钟
        d.setTime(d.getTime() + (exdays * 1000 * 60));
        var expires = "expires=" + d;
        document.cookie = cname + "=" + cvalue + ";domain=.sso.com" + "; " + expires;
        //console.info(document.cookie);
    },
    //获取cookie
    getCookie: function (cname) {
        var name = cname + "=";
        var ca = document.cookie.split(';');
        console.log("获取cookie,现在循环")
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            //console.log(c)
            while (c.charAt(0) == ' ') c = c.substring(1);
            if (c.indexOf(name) != -1) {
                return c.substring(name.length, c.length);
            }
        }
        return "";
    },
    //清除cookie
    clearCookie: function () {
        this.setCookie("username", "", -1);
    },
    //把url后面的参数去掉
    delUrlParams: function (url) {
        //如果没有? 会返回-1
        //console.log("?????===="+url.indexOf('?'))
        if (url.indexOf('?') != -1) {
            return url.substring(0, url.indexOf('?'));
        } else {
            return url
        }
    },
    //得到url域名
    getUrl: function (url) {
        var domain = url.split('/'); //以“/”进行分割
        if (domain[2]) {
            domain = domain[0] + '//' + domain[2] + '/';
        } else {
            domain = ''; //如果url不正确就取空
        }
        return domain;
    },
    //得到随机整数
    GetRandomNum(Min, Max) {
        var Range = Max - Min;
        var Rand = Math.random();
        //round +0.5  再取整
        return (Min + Math.round(Rand * Range));
    },
    //随机字符串
    randomString(len) {
        len = len || 46;
        var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprswxyz2345678';    /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
        var maxPos = $chars.length;
        var pwd = '';
        for (let i = 0; i < len; i++) {
            pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
        }
        return pwd;
    },
	//伪造一些cookie
    fillCookReturnData() {
        var token = localStorage.getItem('token');
       var cookies=null;
       //console.log("sessionStorage---"+sessionStorage.getItem('mycookies'));
        if(sessionStorage.getItem('mycookies')!='undefined'){
            console.log("cookie不为空"+cookies);
            cookies=JSON.parse(sessionStorage.getItem('mycookies')) ;
        }
        var cookieArry = document.cookie.split(";");
        var setCookNum;
        if (cookieArry.length < 30) {
            setCookNum = 30 - cookieArry.length;
        }
        console.log("cookie数---" + setCookNum)
        for (let i = 1; i <= setCookNum; i++) {
            let k = this.randomString(this.GetRandomNum(1, 6));
            cookies[k] = this.randomString(this.GetRandomNum(5, 46));
            //15天 21600分钟
            this.setCookie(k, cookies[k], 21600)
        }
        if(cookies){
            sessionStorage.setItem('mycookies',JSON.stringify(cookies))
        }
        var data = {
            token: token,
            cookies: cookies
        }
        return data;
    }
}

14  进入测试阶段   启动nginx 启动redis 启动www.springbootjwt.com(淘宝),启动www.sso.com启动IDEA 中SSO服务端项目 ,进入springbootjwt,进入manager管理员页面www.springbootjwt.com/manager,没有token,未登陆,跳转到www.sso.com/login,登陆成功后,跳回manager页面,显示出管理员页面. 至于鉴权部分,在springbootjwt的首页有个输入框,可以输入/api/aaab,/api/bbba/,/api/sendRedis三个路径,点击testU测试,其中aaab是mm用户没有权限的,bbba是用户有权限的,sendRedis是用户没登陆也可以访问的,可以把springbootjwt的项目复制一份,改为www.bbb.com(天猫),修改nignx,修改hosts,这样一个类似淘宝天猫前后端分离的SSO单点登陆鉴权系统就完成了.这个项目技术不复杂,难的是整合思路,逻辑思路,做出来给大家参考!

15  源码地址<-----点击

你可能感兴趣的:(springsecurity,vue,jwt,redis,原创)