JWT无状态登录+跨域问题
1.无状态登录原理
1.1.什么是有状态?
用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
服务端保存大量数据,增加服务端压力
服务端保存用户状态,无法进行水平扩展
客户端请求依赖服务端,多次请求必须访问同一台服务器
1.2.什么是无状态
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
服务端不保存任何客户端请求者信息
带来的好处是什么呢?
客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
服务端的集群和状态对客户端透明
服务端可以任意的迁移和伸缩
减小服务端存储压力
1.3.如何实现无状态
无状态登录的流程:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
以后每次请求,客户端都携带认证的token
服务端对token进行解密,判断是否有效。
token的安全性
我们将采用JWT + RSA非对称加密
1.4.数据格式
JWT包含三部分数据:
Header:头部,通常头部有两部分信息:
声明类型,这里是JWT
加密算法,自定义
我们会对头部进行base64加密(可解密),得到第一部分数据
Payload:载荷,就是有效数据,一般包含下面信息:
用户身份信息(注意,这里因为采用base64加密,可解密,因此不要存放敏感信息)
注册声明:如token的签发时间,过期时间,签发人等
这部分也会采用base64加密,得到第二部分数据
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
1、用户登录
2、服务的认证,通过后根据secret生成token
3、将生成的token返回给浏览器
4、用户每次请求携带token
5、服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
6、处理请求,返回响应结果
因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。
1.5非对称加密
1.对称加密,如AES
2.非对称加密,如RSA
基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
私钥加密,持有私钥或公钥才可以解密
公钥加密,持有私钥才可解密
3.不可逆加密,如MD5,SHA
我们首先利用RSA生成公钥和私钥。私钥保存在授权中心,公钥保存在Zuul和各个微服务
用户请求登录
授权中心校验,通过后用私钥对JWT进行签名加密
返回jwt给用户
用户携带JWT访问
Zuul直接通过公钥解密JWT,进行验证,验证通过则放行
请求到达微服务,微服务直接用公钥解析JWT,获取用户信息,无需访问授权中心
2.
导入JWT依赖
<dependencies><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId></dependency>
</dependencies>
package com.leyou.auth.utils;import com.leyou.auth.entity.UserInfo;
import io.jsonwebtoken.*;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;/*** @author bystander* @date 2018/10/1*/
public class JwtUtils {/*** 生成Token* @param userInfo* @param privateKey* @param expireMinutes* @return*/public static String generateToken(UserInfo userInfo, PrivateKey privateKey, int expireMinutes) {return Jwts.builder().claim(JwtConstans.JWT_KEY_ID, userInfo.getId()).claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getName()).setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()).signWith(SignatureAlgorithm.RS256, privateKey).compact();}/*** 生成Token* @param userInfo* @param privateKey* @param expireMinutes* @return* @throws Exception*/public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception {return Jwts.builder().claim(JwtConstans.JWT_KEY_ID, userInfo.getId()).claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getName()).setExpiration(DateTime.now().plus(expireMinutes).toDate()).signWith(SignatureAlgorithm.ES256, RsaUtils.getPrivateKey(privateKey)).compact();}/*** 公钥解析Token* @param publicKey* @param token* @return*/public static Jws<Claims> parseToken(PublicKey publicKey, String token) {return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);}/*** 公钥解析Token* @param publicKey* @param token* @return* @throws Exception*/public static Jws<Claims> parseToken(byte[] publicKey, String token) throws Exception {return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey)).parseClaimsJws(token);}/*** 从Token中获取用户信息(使用公钥解析)* @param publicKey* @param token* @return*/public static UserInfo getUserInfo(PublicKey publicKey, String token) {Jws<Claims> claimsJws = parseToken(publicKey, token);Claims body = claimsJws.getBody();return new UserInfo(ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME)));}/*** 从Token中获取用户信息(使用公钥解析)* @param publicKey* @param token* @return* @throws Exception*/public static UserInfo getUserInfo(byte[] publicKey, String token) throws Exception {Jws<Claims> claimsJws = parseToken(publicKey, token);Claims body = claimsJws.getBody();return new UserInfo(ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME)));}
}
package com.leyou.auth.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;/*** Created by ace on 2018/5/10.** @author HuYi.Zhang*/
public class RsaUtils {/*** 从文件中读取公钥** @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);}
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @author bystander* @date 2018/9/30*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {private Long id;private String name;
}
测试工具类
public class JwtTest {private static final String pubKeyPath = "C:\\tmp\\rsa\\rsa.pub";private static final String priKeyPath = "C:\\tmp\\rsa\\rsa.pri";private PublicKey publicKey;private PrivateKey privateKey;@Testpublic void testRsa() throws Exception {RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");}@Beforepublic void testGetRsa() throws Exception {this.publicKey = RsaUtils.getPublicKey(pubKeyPath);this.privateKey = RsaUtils.getPrivateKey(priKeyPath);}@Testpublic void testGenerateToken() throws Exception {// 生成tokenString token = JwtUtils.generateToken(new UserInfo(20L, "jack"), privateKey, 5);System.out.println("token = " + token);}@Testpublic void testParseToken() throws Exception {String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MjAsInVzZXJuYW1lIjoiamFjayIsImV4cCI6MTUzMzI4MjQ3N30.EPo35Vyg1IwZAtXvAx2TCWuOPnRwPclRNAM4ody5CHk8RF55wdfKKJxjeGh4H3zgruRed9mEOQzWy79iF1nGAnvbkraGlD6iM-9zDW8M1G9if4MX579Mv1x57lFewzEo-zKnPdFJgGlAPtNWDPv4iKvbKOk1-U7NUtRmMsF1Wcg";// 解析tokenUserInfo user = JwtUtils.getInfoFromToken(token, publicKey);System.out.println("id: " + user.getId());System.out.println("userName: " + user.getUsername());}
}
测试生成公钥和私钥,我们运行这段代码(注意去掉@Before的注释 ) :
@Test
public void testRsa() throws Exception {
RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");
}
引入CookieUtils
<dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-core</artifactId>
</dependency>
package com.leyou.common.utils;import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.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 工具类*/
@Slf4j
public final class CookieUtils {/*** 得到Cookie的值, 不编码** @param request* @param cookieName* @return*/public static String getCookieValue(HttpServletRequest request, String cookieName) {return getCookieValue(request, cookieName, null);}/*** 得到Cookie的值,** @param request* @param cookieName* @return*/public static String getCookieValue(HttpServletRequest request, String cookieName, String charset) {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)) {if (charset != null && charset.length() > 0) {retValue = URLDecoder.decode(cookieList[i].getValue(), charset);} else {retValue = cookieList[i].getValue();}break;}}} catch (UnsupportedEncodingException e) {log.error("Cookie Decode Error.", e);}return retValue;}public static CookieBuilder newBuilder(HttpServletResponse response) {return new CookieBuilder(response);}public static class CookieBuilder {private HttpServletRequest request;private HttpServletResponse response;private Integer maxAge;private String charset;private boolean httpOnly = false;public CookieBuilder(HttpServletResponse response) {this.response = response;}public CookieBuilder request(HttpServletRequest request) {this.request = request;return this;}public CookieBuilder maxAge(int maxAge) {this.maxAge = maxAge;return this;}public CookieBuilder charset(String charset) {this.charset = charset;return this;}public CookieBuilder httpOnly() {this.httpOnly = true;return this;}public void build(String cookieName, String cookieValue) {try {if (StringUtils.isBlank(charset)) {charset = "utf-8";}if (cookieValue == null) {cookieValue = "";} else if (StringUtils.isNotBlank(charset)) {cookieValue = URLEncoder.encode(cookieValue, charset);}Cookie cookie = new Cookie(cookieName, cookieValue);if (maxAge != null && maxAge > 0)cookie.setMaxAge(maxAge);if (null != request)// 设置域名的cookie//todocookie.setDomain(getDomainName(request));cookie.setPath("/");cookie.setHttpOnly(httpOnly);response.addCookie(cookie);} catch (Exception e) {log.error("Cookie Encode Error.", e);}}/*** 得到cookie的域名*/private 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.cndomainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];} else if (len <= 3 && len > 1) {// xxx.com or xxx.cndomainName = domains[len - 2] + "." + domains[len - 1];} else {domainName = serverName;}}if (domainName != null && domainName.indexOf(":") > 0) {String[] ary = domainName.split("\\:");domainName = ary[0];}return domainName;}}
}
ly:jwt:secret: leyou@Login(Auth}*^31)&heiMa% # 登录校验的密钥pubKeyPath: C:\\tmp\\rsa\\rsa.pub # 公钥地址priKeyPath: C:\\tmp\\rsa\\rsa.pri # 私钥地址expire: 30 # 服务器token过期时间,单位分钟cookieName: LY_TOKENcookieMaxAge: -1 #浏览器token过期时间,单位秒 -1表示只要关闭浏览器,此cookie就会消失
package com.leyou.auth.properties;import com.leyou.auth.utils.RsaUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {private String secret; // 密钥private String pubKeyPath;// 公钥private String priKeyPath;// 私钥private int expire;// 服务端token过期时间private PublicKey publicKey; // 公钥private PrivateKey privateKey; // 私钥private int cookieMaxAge;//浏览器token过期时间private String cookieName;private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);@PostConstructpublic void init(){try {File pubKey = new File(pubKeyPath);File priKey = new File(priKeyPath);if (!pubKey.exists() || !priKey.exists()) {// 生成公钥和私钥RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);}// 获取公钥和私钥this.publicKey = RsaUtils.getPublicKey(pubKeyPath);this.privateKey = RsaUtils.getPrivateKey(priKeyPath);} catch (Exception e) {logger.error("初始化公钥和私钥失败!", e);throw new RuntimeException();}}public String getSecret() {return secret;}public void setSecret(String secret) {this.secret = secret;}public String getPubKeyPath() {return pubKeyPath;}public void setPubKeyPath(String pubKeyPath) {this.pubKeyPath = pubKeyPath;}public String getPriKeyPath() {return priKeyPath;}public void setPriKeyPath(String priKeyPath) {this.priKeyPath = priKeyPath;}public int getExpire() {return expire;}public void setExpire(int expire) {this.expire = expire;}public PublicKey getPublicKey() {return publicKey;}public void setPublicKey(PublicKey publicKey) {this.publicKey = publicKey;}public PrivateKey getPrivateKey() {return privateKey;}public void setPrivateKey(PrivateKey privateKey) {this.privateKey = privateKey;}public int getCookieMaxAge() {return cookieMaxAge;}public void setCookieMaxAge(int cookieMaxAge) {this.cookieMaxAge = cookieMaxAge;}public String getCookieName() {return cookieName;}public void setCookieName(String cookieName) {this.cookieName = cookieName;}
}
String token = JwtUtils.generateToken(new UserInfo(user.getId(), user.getUsername()),properties.getPrivateKey(), properties.getExpire());// 将token写入cookie,并指定httpOnly为true,防止通过JS获取和修改CookieUtils.setCookie(request, response, prop.getCookieName(),token, prop.getCookieMaxAge(), null, true);
3.跨域问题
我们请求时的serverName明明是:api.leyou.com,现在却被变成了:127.0.0.1,因此计算domain是错误的,从而导致cookie设置失败!
我们首先去更改nginx配置,让它不要修改我们的host:proxy_set_header Host $host;
Zuul内部有默认的过滤器,会对请求和响应头信息进行重组,过滤掉敏感的头信息
zuul.sensitive-headers=
/*** 验证用户信息* @param token* @return*/
@GetMapping("verify")
public ResponseEntity<UserInfo> verifyUser(@CookieValue("LY_TOKEN")String token){try {// 从token中解析token信息UserInfo userInfo = JwtUtils.getInfoFromToken(token, this.properties.getPublicKey());// 解析成功返回用户信息return ResponseEntity.ok(userInfo);} catch (Exception e) {e.printStackTrace();}// 出现异常则,响应500return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
后面的略,具体参考文档~
JWT无状态登录+跨域问题相关推荐
- oauth1+jwt无状态登录策略分析
1.该版本的登录采用的是oauth1+jwt无状态登录策略 2.存在的问题 1)同一用户可以在不同的地点同时登陆 2)token的过期时间设置为1天,在一天内连续多次登陆,会生成多个不 ...
- 什么是有状态登录和无状态登录
1.有状态登录 那缺点是什么? • 服务端保存大量数据,增加服务端压力 • 服务端保存用户状态,无法进行水平扩展 • 客户端请求依赖服务端,多次请求必须访问同一台服务器(如果集群了,相当于启动了多个t ...
- php跨域同步登录,织梦PC端移动端会员同步登录跨域AJAX
利用织梦分别做移动端和PC端的时候会涉及到跨域问题,也就是说移动端和PC端采用不同的域名,就是所谓的跨域. 要实现PC端和移动端会员同步登录,用默认的AJAX来实现会员同步登录的方法就不再适用了,因为 ...
- 开箱即用 - jwt 无状态分布式授权
基于JWT(Json Web Token)的授权方式 JWT 是JSON风格轻量级的授权和身份认证规范,可实现无状态.分布式的Web应用授权: 从客户端请求服务器获取token, 用该token 去访 ...
- java单点登录跨域_深入浅出让你理解跨域与SSO单点登录原理与技术
原标题:深入浅出让你理解跨域与SSO单点登录原理与技术 一:SSO体系结构 SSO SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互 ...
- SSO单点登录跨域跨服务器
单点登录系统总结 关于登录 一.登录 1.当用户点击登录的时候,把当前页面的url用参数传递到登录页面 2.用户成功登录,生成token,保存到redis中(service层),key为token,v ...
- jwt无状态权限认证(pings-shiro-jwt)
单用户并发访问的问题 当用户AccessToken失效,用户使用该失效的AccessToken同时发起多个请求,会产生多AccessToken和RefreshToken认证失败问题: 多AccessT ...
- java cookie实现登录状态_java无状态登录实现方式之ThreadLocal+Cookie
注:本文提到的无状态指的是无需session完毕认证.取用户封装信息. 无状态的优点: 1.多应用单点登录:在多应用的时候仅仅需在登录server登录后.各子应用无需再次登录. 2.多server集群 ...
- shiro手机无状态登录访问和电脑端登录访问两种方式处理
shiro stateless Demo of shiro session and stateless 安全框架shiro的一个同时支持无状态和session登录的添加部分自定义的demo 完整dem ...
最新文章
- Swift开发:仿Clear手势操作(拖拽、划动、捏合)UITableView
- python十进制转二进制循环_python十进制转二进制的详解
- ITK:使用ParallelizeImageRegion
- 新闻发布项目——接口类(UserDao)
- 重复运行JUnit测试而没有循环
- android 部分区域点击,Android编程实现ListView中item部分区域添加点击事件功能
- Java 作用域修饰符
- es6 字符串的 Iterator 接口
- BeginnersBook Java 字符串教程
- 【Elasticsearch】 es ES节点memory lock重要性与实现方式
- Python——相对路径的学习笔记
- 《机器学习实战》学习总结(四)逻辑回归原理
- pycharm安装怎么选_安装新风系统,地送风和顶送风哪种?专业师傅分析,不纠结怎么选...
- java设计智慧教室_物联网智慧教室设计方案,更便捷的智慧教学体验
- DeepLab图像分割
- SPSS 24/25/26安装包分享 window和mac版本
- HUSTOJ配置文件解释
- Go 微服务开发框架 DMicro 的设计思路
- linux开机不运行桌面快捷方式,Android 开机自动运行和添加删除桌面快捷方式
- oracle logged on,ORA-01012:not logged on的解决办法
热门文章
- Bezier曲线生成【计算机图形学】
- Excel怎么设置每页都打印标题行?
- 【SSH框架/国际物流商综平台】-05 单点登录 用户-角色-权限分配 Ztree.js structs2.0 异常框架 细粒度权限控制 BaseEntitity中createby degree
- 自然数学-微积分的基本公式
- 三维地图(3D地图)离线地图开发
- c# OpenCvSharp 判断图片的是否黑白
- idea配置翻译插件(google翻译插件)
- w ndows10输入法设置,unity3d屏蔽Windows10输入法
- 自媒体多平台多账号群发工具开发日记:第1天 工具的统筹规划
- 小计 合计 总计 共计 怎么解释?