分布式单点登录
介绍:
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一
。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
。
登录方式的分类:
按用户状态分:
- 有状态登录
服务器需要保存用户的信息,登录成功后将用户存在session中,通过cookie保存Jsessionid,下次访问携带id,获得服务器中的用户信息。
问题:session只能保存再一个服务器中,其他服务访问需要再次登录。 - 无状态登录
将信息保存到客户端(使用cookie)
好处:客户端访问时,服务器读取用户信息,服务器的负担小了。
缺点:安全性差。
单点登录实现(JWT+RSA)
JWT:是为了网络应用环境间传递说明而执行的一种基于json的开发标准
。该token被设计紧凑而安全的
,特别适用于分布式站点的单点登录(SSO)场景
。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
。
官方图解:
加密方式:
1)对称式加密:
加密和解密只需要一个密钥
特点:算法公开,计算量小,加密速度快,效率高。
缺点:安全性低
主要算法:des,sdes,tdea,rc5,idea
2)非对称式加密:
分为公钥和私钥
私钥加密:公钥私钥均可解
公钥加密:只有私钥可解
特点:算法复杂,加密速度慢,但是安全性会高很多。
主要算法:rsa(椭圆曲线加密算法)
具体项目中使用:
第一个工具类:CookieUtil 主要可以对cookie进行设置和存值
package com.cloud.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* Cookie 工具类 得到cookie中的值或者设置cookie内容
*
*/
public final class CookieUtil {
public static final String COOKIE_NAME = "token"; //cookie名称
public static final int COOKIE_MAX_AGE = 1800; //cookie生命周期
static final Logger logger = LoggerFactory.getLogger(CookieUtil.class);
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
return getCookieValue(cookieList,cookieName,isDecoder);
}
public static String getCookieValue(Cookie[] cookieList, String cookieName, boolean isDecoder) {
if (cookieList == null || cookieName == null){
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
logger.error("Cookie Decode Error.", e);
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null){
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
logger.error("Cookie Decode Error.", e);
}
return retValue;
}
/**
* 生成cookie,并指定编码
* @param request 请求
* @param response 响应
* @param cookieName name
* @param cookieValue value
* @param encodeString 编码
*/
public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, String encodeString) {
setCookie(request,response,cookieName,cookieValue,null,encodeString, null);
}
/**
* 生成cookie,并指定生存时间
* @param request 请求
* @param response 响应
* @param cookieName name
* @param cookieValue value
* @param cookieMaxAge 生存时间
*/
public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, Integer cookieMaxAge) {
setCookie(request,response,cookieName,cookieValue,cookieMaxAge,null, null);
}
/**
* 设置cookie,不指定httpOnly属性
*/
public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, Integer cookieMaxAge, String encodeString) {
setCookie(request,response,cookieName,cookieValue,cookieMaxAge,encodeString, null);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxAge
* cookie生效的最大秒数
*/
public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, Integer cookieMaxAge, String encodeString, Boolean httpOnly) {
try {
if(StringUtils.isEmpty(encodeString)) {
encodeString = "utf-8";
}
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxAge != null && cookieMaxAge > 0)
cookie.setMaxAge(cookieMaxAge);
// if (null != request)// 设置域名的cookie
// {
// String domainName = getDomainName(request);
// System.out.println("domain:" +domainName);
// cookie.setDomain(domainName);
// }
cookie.setPath("/");
if(httpOnly != null) {
cookie.setHttpOnly(httpOnly);
}
response.addCookie(cookie);
} catch (Exception e) {
logger.error("Cookie Encode Error.", e);
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
第二个工具类:JwtUtil 主要功能,加密获得token字符串,解密获得token中的用户信息,
package com.cloud.utils;
import com.cloud.entity.TbUser;
import com.cloud.entity.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* @author yanglihu‘
* 进行加密 和 解析token功能
*/
public class JwtUtil {
public static final String JWT_KEY_USERNAME = "username";
public static final String JWT_KEY_PASSWORD = "password";
public static final String JWT_KEY_AUTHORITIES = "authorities";
public static final int EXPIRE_MINUTES = 30;
/**
* 私钥加密token
*/
public static String generateToken(String username,String password,String authorities, PrivateKey privateKey, int expireMinutes) throws Exception {
return Jwts.builder()
.claim(JWT_KEY_USERNAME, username)
.claim(JWT_KEY_PASSWORD, password)
.claim(JWT_KEY_AUTHORITIES, authorities)
.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 从token解析用户
* @param token
* @param publicKey
* @return
* @throws Exception
*/
public static UserInfo getUserInfoFromToken(String token, PublicKey publicKey) throws Exception {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims body = claimsJws.getBody();
String username = (String) body.get(JWT_KEY_USERNAME);
UserInfo user = new UserInfo();
user.setUsername(username);
return user;
}
}
第三个工具类:RsaUtil 主要功能创建密钥及对密钥相关操作
package com.cloud.utils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* @author yanglihu
* 设置密钥的位置以及对密钥的相关操作
*/
public class RsaUtil {
public static final String RSA_SECRET = "java1130@#$%"; //秘钥
public static final String RSA_PATH = "D:\\rsa\\";//秘钥保存位置
public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pub.rsa";//公钥路径
public static final String RSA_PRI_KEY_PATH = RSA_PATH + "pri.rsa";//私钥路径
public static PublicKey publicKey;
public static PrivateKey privateKey;
static{
try {
File rsa = new File(RSA_PATH);
if(!rsa.exists()){
rsa.mkdirs();
}
File pubKey = new File(RSA_PUB_KEY_PATH);
File priKey = new File(RSA_PRI_KEY_PATH);
//判断公钥和私钥如果不存在就创建
if (!priKey.exists() || !pubKey.exists()) {
//创建公钥和私钥文件
RsaUtil.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
}
//读取公钥和私钥内容
publicKey = RsaUtil.getPublicKey(RSA_PUB_KEY_PATH);
privateKey = RsaUtil.getPrivateKey(RSA_PRI_KEY_PATH);
}catch (Exception ex){
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
public static PublicKey getPublicKey(byte[] bytes) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(1024, secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
鉴权服务中:对springscurity的用户登录验证成功处理器加密生成token
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
//将用户名转化为jwt 保存到cookie中
try {
String token = JwtUtil.generateToken(user.getUsername(), "", "", RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
CookieUtil.setCookie(req,resp,CookieUtil.COOKIE_NAME,token,CookieUtil.COOKIE_MAX_AGE);
} catch (Exception e) {
e.printStackTrace();
}
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
//jackson转换为json格式字符串
out.write(new ObjectMapper().writeValueAsString(user));
out.flush();
out.close();
}
}
网关服务中:对于token的解密
@Component
@Slf4j
public class MyGatewayAuthenticationFilter implements GlobalFilter, Ordered {
/**
* 白名单
*/
private static final String[] WHITE_LIST={"/login","/logout"};
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//转换对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//验证请求是否在白名单中
for (String str:WHITE_LIST){
if (request.getURI().getPath().endsWith(str)){
return chain.filter(exchange);
}
}
try {
//获得请求中的 token参数
String token = request.getCookies().getFirst("token").getValue();
//解读token 获得对象
UserInfo user = JwtUtil.getUserInfoFromToken(token, RsaUtil.publicKey);
} catch (Exception e) {
e.printStackTrace();
//验证失败返回信息
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String msg="request denied";
DataBuffer wrap = response.bufferFactory().wrap(msg.getBytes());
return response.writeWith(Mono.just(wrap));
}
return chain.filter(exchange);
}
/**
* 返回值 拦截器顺序 越小越靠前
*/
@Override
public int getOrder() {
return 0;
}
}