1.单点登录的核心思想
通过最近单点登录的实践;私以为单点登录的关键是让不同系统之间的保存登录信息的凭证共享。当满足共享后,就可以实现单点登录。然后实现共享有两种方式
1.1.在前端实现共享
利用顶级域名可以共享cookies,将token存储在前端。利用前后端分离。前端负责页面的跳转。后端负责鉴权。网关负责鉴权,auth模块负责登录认证逻辑。
1.2.在服务器端实现共享 (CAS springsession )
服务器端主要就是针对session,将不同服务器的连接到登录服务器,相当于共享的是登录服务器的session来完成登录信息的共享;
2 通过父域cookie实现
1.项目结构图大概是这样
nginx 负责前端的静态页面,前端三个服务 登陆服务,主服务,测试服务
后端gateway负责相关的过滤,检验jwt的正确性
auth模块对应登陆的前端
system模块对应前端的主服务
2.1nginx配置各个前端项目的入口
通过域名来进行访问;
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name system.sr.com;
location / {
root system; #静态html文件存放目录地址
index index.html index.htm; #默认首页
try_files $uri $uri/ /index.html;
}
location /prod-api/ {
# root html;
# index index.html index.htm;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://localhost:8080/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 80;
server_name auth.sr.com;
location / {
root auth; #静态html文件存放目录地址
index index.html index.htm; #默认首页
try_files $uri $uri/ /index.html;
}
location /prod-api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://localhost:8080/;
}
}
server {
listen 80; # nginx 监听端口
server_name gis.sr.com; #服务器地址
location / {
root html; #静态html文件存放目录地址
index index.html index.htm; #默认首页
}
location /prod-api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://localhost:8080/;
}
}
}
三个前端项目通过不同的域名进来
2.2前端;首页钩子拦截
auth逻辑(登陆前端):
1.检验当前是否有cookies,有的话 跳转向对应的模块
window.location.href = http://${module}.sr.com/
2.没有的话跳转到登陆模块
(注意:访问其他模块未登录将定向到auth模块并且在URL后面带上?redirect=${redirect}
)
3.登陆成功后根据?redirect=${redirect}
跳回自己的模块
sysytem(主前端):
1.未登录
window.location.href = ‘http://auth.sr.com/?redirect=system’
2.已登录 请求将通过网关做鉴权
前端cookies的存储处理
在设置cookie的时候加上domain
export function getToken() {
return Cookies.get(TokenKey,{ domain: 'sr.com' })
}
export function setToken(token) {
return Cookies.set(TokenKey, token,{ domain: 'sr.com' })
}
2.3gateway 做鉴权
从jwt里面解析出用户的唯一id(目前暂用uuid) 通过jwt来保证jwt令牌在前端的安全性问题
String UUID;
try {
Claims claims = JwtTokenUtil.parseJWT(token);
UUID = (String) claims.get("uuid");
log.info("UUID\t"+UUID);
} catch (Exception j) {
j.printStackTrace();
return unauthorizedResponse(exchange, "UUID令牌验证失败");
}
//拿到uuid后存入header
addHeader(mutate, SecurityConstants.DETAILS_USER_UUID, UUID);
2.4 jwt的生成 在auth模块
String token = null;
try {
token = JwtTokenUtil.generateToken(uuid);
log.info("jwt token\t"+token);
} catch (Exception e) {
e.printStackTrace();
}
ps
3.1 jwt 工具类
public class JwtTokenUtil {
private static final String SALT = "123456";//加密解密盐值
/**
* 生成token(请根据自身业务扩展)
*
* @param subject (主体信息)
* @param expirationSeconds 过期时间(秒)
* @param claims 自定义身份信息
* @return
*/
public static String generateToken(String subject, int expirationSeconds, Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)//主题
//.setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
.signWith(SignatureAlgorithm.HS512, SALT) // 不使用公钥私钥
//.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 解析token,获得subject中的信息
*
* @param token
* @return
*/
public static String parseToken(String token) {
String subject = null;
try {
subject = getTokenBody(token).getSubject();
} catch (Exception e) {
}
return subject;
}
/**
* 由字符串生成加密key
*
* @return
*/
public static SecretKey generalKey() {
String stringKey = "123456789";
// 本地的密码解码
byte[] encodedKey = Base64.decodeBase64(stringKey);
// 根据给定的字节数组使用AES加密算法构造一个密钥
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样
Claims claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(jwt).getBody(); //设置需要解析的jwt
return claims;
}
/**
* 获取token自定义属性
*
* @param token
* @return
*/
public static Map<String, Object> getClaims(String token) {
Map<String, Object> claims = null;
try {
claims = getTokenBody(token);
} catch (Exception e) {
}
return claims;
}
/**
* 解析token
*
* @param token
* @return
*/
private static Claims getTokenBody(String token) {
return Jwts.parser()
//.setSigningKey(publicKey)
.setSigningKey(SALT)
.parseClaimsJws(token)
.getBody();
}
/**
* 创建jwt
*/
public static String generateToken(String uuid) throws Exception {
// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
Map<String, Object> claims = new HashMap<>();
claims.put("uuid", uuid);
// 生成JWT的时间
long nowMillis = System.currentTimeMillis();
// 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
// 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
SecretKey key = generalKey();
// 下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
.setClaims(claims) // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId("123456") // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(new Date(nowMillis)) // iat: jwt的签发时间
.setIssuer("rose") // issuer:jwt签发人
.setSubject("{\"session_key\":\"qVXVkd123456b8YezQ==\"}") // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥
// 设置过期时间
long expMillis = nowMillis + 6000000L;
builder.setExpiration(new Date(expMillis));
return builder.compact();
}
}
3.2maven依赖
<!--jwt来完成的鉴权-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
3.3 快速构建
采用若依cloud开源项目 https://gitee.com/y_project/RuoYi-Cloud
在这个开源项目的基础上快速改进