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无状态登录+跨域问题相关推荐

  1. oauth1+jwt无状态登录策略分析

    1.该版本的登录采用的是oauth1+jwt无状态登录策略 2.存在的问题     1)同一用户可以在不同的地点同时登陆     2)token的过期时间设置为1天,在一天内连续多次登陆,会生成多个不 ...

  2. 什么是有状态登录和无状态登录

    1.有状态登录 那缺点是什么? • 服务端保存大量数据,增加服务端压力 • 服务端保存用户状态,无法进行水平扩展 • 客户端请求依赖服务端,多次请求必须访问同一台服务器(如果集群了,相当于启动了多个t ...

  3. php跨域同步登录,织梦PC端移动端会员同步登录跨域AJAX

    利用织梦分别做移动端和PC端的时候会涉及到跨域问题,也就是说移动端和PC端采用不同的域名,就是所谓的跨域. 要实现PC端和移动端会员同步登录,用默认的AJAX来实现会员同步登录的方法就不再适用了,因为 ...

  4. 开箱即用 - jwt 无状态分布式授权

    基于JWT(Json Web Token)的授权方式 JWT 是JSON风格轻量级的授权和身份认证规范,可实现无状态.分布式的Web应用授权: 从客户端请求服务器获取token, 用该token 去访 ...

  5. java单点登录跨域_深入浅出让你理解跨域与SSO单点登录原理与技术

    原标题:深入浅出让你理解跨域与SSO单点登录原理与技术 一:SSO体系结构 SSO SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互 ...

  6. SSO单点登录跨域跨服务器

    单点登录系统总结 关于登录 一.登录 1.当用户点击登录的时候,把当前页面的url用参数传递到登录页面 2.用户成功登录,生成token,保存到redis中(service层),key为token,v ...

  7. jwt无状态权限认证(pings-shiro-jwt)

    单用户并发访问的问题 当用户AccessToken失效,用户使用该失效的AccessToken同时发起多个请求,会产生多AccessToken和RefreshToken认证失败问题: 多AccessT ...

  8. java cookie实现登录状态_java无状态登录实现方式之ThreadLocal+Cookie

    注:本文提到的无状态指的是无需session完毕认证.取用户封装信息. 无状态的优点: 1.多应用单点登录:在多应用的时候仅仅需在登录server登录后.各子应用无需再次登录. 2.多server集群 ...

  9. shiro手机无状态登录访问和电脑端登录访问两种方式处理

    shiro stateless Demo of shiro session and stateless 安全框架shiro的一个同时支持无状态和session登录的添加部分自定义的demo 完整dem ...

最新文章

  1. Swift开发:仿Clear手势操作(拖拽、划动、捏合)UITableView
  2. python十进制转二进制循环_python十进制转二进制的详解
  3. ITK:使用ParallelizeImageRegion
  4. 新闻发布项目——接口类(UserDao)
  5. 重复运行JUnit测试而没有循环
  6. android 部分区域点击,Android编程实现ListView中item部分区域添加点击事件功能
  7. Java 作用域修饰符
  8. es6 字符串的 Iterator 接口
  9. BeginnersBook Java 字符串教程
  10. 【Elasticsearch】 es ES节点memory lock重要性与实现方式
  11. Python——相对路径的学习笔记
  12. 《机器学习实战》学习总结(四)逻辑回归原理
  13. pycharm安装怎么选_安装新风系统,地送风和顶送风哪种?专业师傅分析,不纠结怎么选...
  14. java设计智慧教室_物联网智慧教室设计方案,更便捷的智慧教学体验
  15. DeepLab图像分割
  16. SPSS 24/25/26安装包分享 window和mac版本
  17. HUSTOJ配置文件解释
  18. Go 微服务开发框架 DMicro 的设计思路
  19. linux开机不运行桌面快捷方式,Android 开机自动运行和添加删除桌面快捷方式
  20. oracle logged on,ORA-01012:not logged on的解决办法

热门文章

  1. Bezier曲线生成【计算机图形学】
  2. Excel怎么设置每页都打印标题行?
  3. 【SSH框架/国际物流商综平台】-05 单点登录 用户-角色-权限分配 Ztree.js structs2.0 异常框架 细粒度权限控制 BaseEntitity中createby degree
  4. 自然数学-微积分的基本公式
  5. 三维地图(3D地图)离线地图开发
  6. c# OpenCvSharp 判断图片的是否黑白
  7. idea配置翻译插件(google翻译插件)
  8. w ndows10输入法设置,unity3d屏蔽Windows10输入法
  9. 自媒体多平台多账号群发工具开发日记:第1天 工具的统筹规划
  10. 小计 合计 总计 共计 怎么解释?