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前端代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.tuu</groupId>
<artifactId>springjw</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springjw</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.45</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>
<!--jwt工具类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
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<Permission> 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<String, Object> 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<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> 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<String, Object> 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<Object> 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=================================