1. 问题
以前我们的登录都是将当前登录的用户信息放在session中,但是session是保存在服务器的,在分布式环境下服务器不只有一台,那就容易找不到登录的用户信息,所以应该是保存在客户端也就是cookie中的,但是存在cookie中有个问题,就是要进行一些操作时需要获取当前登录的用户信息,那么要将cookie中的user发送过去,这一来二去老是携带着明文的用户名和密码,就很不安全,会被人用一些技术手段拦截,那么就获取了你的账号密码信息。
随后就产生了一些加密技术,也就是说cookie中存的user信息是密文(此时称为token),就算被拦截了没有破解方法(称为密钥)也没有用。
1. RSA非对称加密技术
看大佬的讲解
https://www.zhihu.com/question/304030251
当然,后面还涉及到一些更深入的东西(中间人攻击,CA认证中心等),有时间再补笔记(●’◡’●)计网的笔记先埋个坑慢慢填
追更:计网的笔记点击这里
2. JWT
是token的一种,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范。格式是这样的(头部:我是JWT格式的token,载荷:用户信息,签名:头部和载荷合在一起再加密)
我们来看一下登录的流程
- 我们首先利用RSA生成公钥和私钥。私钥保存在授权中心,公钥保存在Zuul和各个信任的微服务
- 用户请求登录
- 授权中心校验,通过后用私钥对用户信息进行签名加密生成jwt
- 返回jwt给用户并保存在cookie中
- 用户携带JWT访问
- Zuul直接通过公钥解密JWT,进行验证,验证通过则放行
- 请求到达微服务,微服务直接用公钥解析JWT,获取用户信息,无需访问授权中心
3. 具体实现及登录功能细节
3.1 加密生成jwt部分实现
- 创建载荷对象,也就是jwt中的载荷部分,无需包含密码和盐(使用了rsa以后,只要公钥能解析不出错就说明认证成功了)
- 会用到的依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
|
- 四个工具类
- RsaUtils:生成rsa密钥(一个公钥一个私钥)方法,读取公钥密钥方法
- JwtUtils:将用户信息使用私钥加密成jwt格式的token方法,使用公钥解析token获得用户信息方法
代码太多了,反正是工具类,这里给个下载链接,直接下载粘贴即可食用
http://39.106.175.70/owncloud/index.php/s/xcW47RaFFiSjfQ0
- 运行测试类,直接复制即可食用
要替换的地方已用注释标明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class JwtTest { private static final String pubKeyPath = "C:\\Users\\wjw\\.leyoursa\\rsa.pub";
private static final String priKeyPath = "C:\\Users\\wjw\\.leyoursa\\rsa.pri";
private PublicKey publicKey;
private PrivateKey privateKey;
@Test public void testRsa() throws Exception { RsaUtils.generateKey(pubKeyPath, priKeyPath, "234"); } public void testGetRsa() throws Exception { this.publicKey = RsaUtils.getPublicKey(pubKeyPath); this.privateKey = RsaUtils.getPrivateKey(priKeyPath); }
@Test public void testGenerateToken() throws Exception { String token = JwtUtils.generateToken(new UserInfo(20L, "jack"), privateKey, 5); System.out.println("token = " + token); }
@Test public void testParseToken() throws Exception { String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MjAsInVzZXJuYW1lIjoiamFjayIsImV4cCI6MTU5NTIzMzI4OX0.IhrxQJ9vmdME2EaOiGl-IEI6JybKDo6y7UlCp0pBWwfusUHbY3xjMFnGnnRvQ2keOU7AFetBhMPw03T4pUbUil8tmzc38mk5etYs0kxkoa2_qmeu80qqQUBVvfexYmZkvo3nOjz66IFfX716CWgZ-9Ew8zAib1o0TFkJ4RnjV5w";
UserInfo user = JwtUtils.getInfoFromToken(token, publicKey); System.out.println("id: " + user.getId()); System.out.println("userName: " + user.getUsername()); } }
|
加密解密测试效果:
3.2 cookie部分实现
加密这部分测试成功之后我们注意到登录功能还需要保存到cookie中
也提供一个工具类,直接复制即可食用
http://39.106.175.70/owncloud/index.php/s/Y2tQJoCMUILK0ZY
3.3 登录流程业务逻辑实现
rsa,jwt,cookie都弄好后,就可以实现登录功能了
其实登录就比较简单,controller service那些,跟着之前流程来就行
不过测试用例写死了不优雅,可以使用yml+配置类读取(当然你可以暴力)
yml
配置类
给出代码,省略篇幅,包自己引入一下,getter/setter自己创建一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @ConfigurationProperties(prefix = "leyou.jwt") public class JwtProperties {
private String secret;
private String pubKeyPath;
private String priKeyPath;
private int expire;
private String cookieName;
private PublicKey publicKey;
private PrivateKey privateKey;
private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
@PostConstruct public 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(); } } }
|
后面的没什么好说了,代码太简单,有手就行,我贴图分析吧
controller
service
3.4 cookie细节的处理
cookie是保存在本地的,那么通过localhost访问就没什么问题,但是通过域名访问就有问题
以下是对比图
先解决方法再分析原因:
nginx配置文件的网关部分添加proxy_set_header Host $host;
zuul的yml
原因:我们通过调试可以看到cookie的domainName是计算机名称而不是leyou.com
这个计算机名是哪来的呢,因为zuul到登录微服务是通过eureka注册中心的
访问流程是nginx=>zuul=>登录微服务,因此我们要让计算机名带着访问路径一起走才对,所以才有了上面的设置,解决后的效果
3.5 token过期处理的思路
token和cookie都有过期时间,如果用户在一段时间内没有操作那么过期了就需要重新登录,没有问题。但是如果一直在操作(比如选购商品选了很久,最后下单的时候发现cookie过期了,拿不到用户登录信息,就很尴尬)。
解决思路比较简单,就是不管进行什么操作,vue可以放在created钩子函数中,只要加载了页面,就调用verify这个方法:验证是否有token,如果还有,则说明用户正在操作,重新更新一遍token和cookie,防止过期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@GetMapping("verify") public ResponseEntity<UserInfo> verify( @CookieValue("LY_TOKEN") String token, HttpServletRequest request, HttpServletResponse response){ try { UserInfo user = JwtUtils.getInfoFromToken(token, this.jwtProperties.getPublicKey()); if (user == null){ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); }
token = JwtUtils.generateToken(user, this.jwtProperties.getPrivateKey(), this.jwtProperties.getExpire()); CookieUtils.setCookie(request, response, jwtProperties.getCookieName(), token, jwtProperties.getExpire() * 60);
return ResponseEntity.ok(user); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } }
|
3.6 zuul网关拦截
注册,登录,搜索等功能不需要拦截
下单等功能需要拦截判断是否已登录
网关微服务只需要公钥和cookie名字,以及白名单
1 2 3 4 5 6 7 8 9 10 11 12
| leyou: jwt: pubKeyPath: C:\Users\wjw\.leyoursa\rsa.pub # 公钥地址 cookieName: LY_TOKEN filter: allowPaths: # 白名单 - /api/auth - /api/search - /api/user/register - /api/user/check - /api/user/code - /api/item
|
复制一份读取yml的配置类到网关微服务,需要相应修改,只读取不创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @ConfigurationProperties(prefix = "leyou.jwt") public class JwtProperties {
private String pubKeyPath;
private String cookieName;
private PublicKey publicKey;
private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
@PostConstruct public void init(){ try { this.publicKey = RsaUtils.getPublicKey(pubKeyPath); } catch (Exception e) { logger.error("初始化公钥和私钥失败!", e); throw new RuntimeException(); } } }
|
读取白名单的配置类单独出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
@ConfigurationProperties(prefix = "leyou.filter") public class FilterProperties {
private List<String> allowPaths;
public List<String> getAllowPaths() { return allowPaths; }
public void setAllowPaths(List<String> allowPaths) { this.allowPaths = allowPaths; } }
|
有了这两个配置类就可以读取yml参数了,主要完成下面这个拦截器即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
@Component @EnableConfigurationProperties({JwtProperties.class, FilterProperties.class}) public class LoginFilter extends ZuulFilter {
@Autowired private JwtProperties jwtProperties;
@Autowired private FilterProperties filterProperties;
@Override public String filterType() { return "pre"; }
@Override public int filterOrder() { return 10; }
@Override public boolean shouldFilter() { List<String> allowPaths = this.filterProperties.getAllowPaths();
RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String url = request.getRequestURL().toString();
for (String allowPath : allowPaths) { if (StringUtils.contains(url, allowPath)) { return false; } } return true; }
@Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); String token = CookieUtils.getCookieValue(context.getRequest(), this.jwtProperties.getCookieName()); if (StringUtils.isBlank(token)){ context.setSendZuulResponse(false); context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); } try { JwtUtils.getInfoFromToken(token, this.jwtProperties.getPublicKey()); } catch (Exception e) { e.printStackTrace(); context.setSendZuulResponse(false); context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); } return null; } }
|
3.7 最终效果
看看页面上,cookie有了,value是jwt,域名也对了,注册和登录也不拦截了,腰不酸腿不痛了
嗯!是主流登录的实现效果~