单点登陆系统,简单说就是用户登陆了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进入验证,这个没有深究。下面进入代码,同时也会把思路说一说,就当参考吧,我也不是什么技术大佬。
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');
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
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
@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;
}
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;
}
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;
}
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class RPVersion {
private String name;
private Integer versionNum;
}
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;
}
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<>();
}
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));
}
}
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
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;
}
}
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);
//这里是收到通道的消息之后执行的方法
}
}
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");
}
}
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");
}
}
}
}
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;
}
}
public interface BaseErrorInfoInterface {
/** 错误码*/
String getResultCode();
/** 错误描述*/
String getResultMsg();
}
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;
}
}
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());
}
}
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);
}
}
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();
}
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;
}
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);
}
}
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","权限不足")));
}
}
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 extends GrantedAuthority> authorities;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
//一号构造方法
public SecurityUserDetails(String uid, Collection extends GrantedAuthority> authorities){
this.authorities = authorities;
this.setUsername(uid);
this.setAuthorities(authorities);
}
//二号构造方法
public SecurityUserDetails(String uid,boolean isSingle, boolean isAccountNonExpired,boolean isAccountLock,Collection extends GrantedAuthority> 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;
}
}
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);
}
}
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;
}
}
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;
}
}
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);
}
}
}
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;
}
}
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();
}
}
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));
}
}
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);
}
}
# A网站 看作淘宝
127.0.0.1 www.springbootjwt.com
# 验证中心
127.0.0.1 www.sso.com
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;
}
}
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,采用独立样式文件载入,不采用
<%= htmlWebpackPlugin.options.title %>
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')
})
这是管理员页面
首页
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;
}
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)
}
}
}
Router for Tomcat
404
/index.html
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,采用独立样式文件载入,不采用
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,'*');
}
SSO
<%= htmlWebpackPlugin.options.title %>
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/" ;
}
}
})
This is an login page
用户名:
密码:
回调地址:{{rpath}}
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;
}
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;
}
}