当前位置: 首页 > news >正文

无源码实现免登录功能

因项目要求需要对一个没有源代码的老旧系统实现免登录功能,系统采用前后端分离的方式部署,登录时前端调用后台的认证接口,认证接口返回token信息,然后将token以json的方式存储到cookie中,格式如下:
在这里插入图片描述
这里有一个auth_token采用JSON格式存储,尝试了好几种写入Cookie的方式,均无法实现,现将可以实现方式记录如下。

Nginx配置

首先,为了避免跨域问题,以及Cookie的作用域问题,必须将现有系统和免登录跳转系统配置到同一个端口下,配置如下

    server {listen       80;server_name  localhost;# 免登录跳转系统location / {proxy_pass http://127.0.0.1:8084;#proxy_cookie_path / "/; httponly; secure; SameSite=Strict";#proxy_cookie_flags ~ nosecure samesite=strict;}# 老旧系统location / {proxy_pass http://127.0.0.1:8000;proxy_cookie_path / "/; httponly; secure; SameSite=Strict";proxy_cookie_flags ~ nosecure samesite=strict;}

注意这里proxy_cookie_path和proxy_cookie_flags要注释掉,否则在免登录跳转系统中设置的Cookie在跳转到现有系统后无法认证成功,下面对这两个参数说明一下:

proxy_cookie_path

proxy_cookie_path / "/; httponly; secure; SameSite=Strict";

proxy_cookie_path:此指令用于修改通过Nginx代理传递的Cookie的路径以及其他属性。
/:指定Cookie的路径为根路径,这意味着Cookie对整个域名有效。
“; httponly; secure; SameSite=Strict”:这部分是对Cookie添加额外的属性:
httponly:此属性指示浏览器仅允许HTTP(S)协议访问该Cookie,不允许JavaScript访问,这有助于防止跨站脚本攻击(XSS)。
secure:此属性指示浏览器仅在HTTPS连接中传输该Cookie,不允许在不安全的HTTP连接中传输,这有助于保护Cookie不被中间人攻击窃取。
SameSite=Strict:此属性控制Cookie在跨站请求中的发送行为。设置为Strict意味着Cookie仅在同站请求中发送,不会在跨站请求中发送,这有助于减少跨站请求伪造(CSRF)攻击的风险。

proxy_cookie_flags

proxy_cookie_flags ~ nosecure samesite=strict;

proxy_cookie_flags:此指令用于设置或修改通过Nginx代理传递的Cookie的标志。
~:这是一个正则表达式匹配操作符,表示接下来的模式应用于所有匹配的Cookie。
nosecure:此标志指示Nginx移除Cookie中的Secure属性。这意味着即使原始Cookie设置了Secure属性,通过Nginx代理后,该属性将被移除,Cookie可以在HTTP和HTTPS连接中都传输。这通常不推荐,因为它降低了安全性。
samesite=strict:此标志指示Nginx添加或修改Cookie的SameSite属性为Strict。这与proxy_cookie_path中的SameSite=Strict类似,控制Cookie在跨站请求中的发送行为。
注意:通常情况下,不建议在生产环境中使用nosecure标志,因为它会降低Cookie的安全性。如果需要移除Secure属性,应该仔细考虑安全性影响,并确保有其他安全措施来保护数据。
这两行配置通常一起使用,以确保通过Nginx代理传递的Cookie具有适当的安全属性。然而,proxy_cookie_flags中的nosecure标志可能会抵消proxy_cookie_path中设置的secure属性,因此在实际应用中需要谨慎使用。

免登录跳转

搭建一个SpringBoot工程,前端实现一个简单的跳转页面test.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>OSS</title><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta http-equiv="X-UA-Compatible" content="IE=Edge" />
</head>
<body><a href="http://192.168.31.112">http://192.168.31.112</a>
</body>
</html>

后端IndexController类关键方法如下

package org.example.onemaposs.controller;import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;@Slf4j
@Controller
public class IndexController {@Value("${app.login.url}")private String loginUrl;@Value("${app.user}")private String user;@Value("${app.pass}")private String pass;@Value("${app.index.url}")private String indexUrl;// 经过测试,该方法可以实现免登录功能,注意在Nginx中代理不能加如下两行,否测Cookie无法共享// proxy_cookie_path / "/; httponly; secure; SameSite=Strict";// proxy_cookie_flags ~ nosecure samesite=strict;@RequestMapping("/test")public String test(HttpServletRequest request, HttpServletResponse response, ModelMap model) {String token = null;JSONObject json = new JSONObject();json.put("user", user);json.put("pwd", pass);String str = doPost(loginUrl, json);JSONObject jsonObject = JSONObject.parseObject(str);JSONObject obj = jsonObject.getJSONObject("obj");JSONObject rtnJson = new JSONObject(true);JSONObject authToken = new JSONObject(true);if (obj != null && obj.containsKey("token")) {rtnJson.put("code", 200);rtnJson.put("msg", "登录成功");token = obj.getString("token");authToken.put("data", token);}String time = String.format("expires=%s; path=/", getTime());log.debug("Cookie time = {}", time);response.setHeader("authorization", token);response.addHeader("Set-Cookie", String.format("auth_token=%s; ", authToken.toJSONString()) + time);response.addHeader("Set-Cookie", "user_name=admin; " + time);response.addHeader("Set-Cookie", "is_anager=true; " + time);response.addHeader("Set-Cookie", "user_role={%22uId%22:%22650e5cc596d8932f88ec8c90%22%2C%22user%22:%22admin%22%2C%22realName%22:%22%22%2C%22role%22:%22Admin%22%2C%22userRolePrivilege%22:{%22rolePriv%22:[]%2C%22userPriv%22:{%22privileges%22:[]}}}; " + time);return "test";}private String getTime() {// 获取当前时间的ZonedDateTime实例ZonedDateTime now = ZonedDateTime.now(java.time.ZoneId.of("GMT"));// 增加8天ZonedDateTime futureDateTime = now.plusDays(8);// 定义日期时间格式DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH);// 格式化日期时间return futureDateTime.format(formatter);}protected String doPost(String url, JSONObject json) {String rtn = null;CloseableHttpClient httpClient = null;try {httpClient = HttpClientBuilder.create().build();RequestConfig.custom().setConnectionRequestTimeout(5000).setSocketTimeout(5000).setConnectTimeout(5000).build();// 创建Get请求HttpPost http = new HttpPost(url);http.addHeader("Accept", "application/json, text/plain, */*");http.addHeader("Content-Type", "application/json");http.setEntity(new StringEntity(json.toJSONString(), "UTF-8"));CloseableHttpResponse response = null;// 由客户端执行(发送)Get请求response = httpClient.execute(http);// 从响应模型中获取响应实体HttpEntity responseEntity = response.getEntity();rtn = EntityUtils.toString(responseEntity);} catch (Exception e) {log.error(String.format("Search request throw exception: %s", e.getMessage()), e);} finally {if (httpClient != null) {try {httpClient.close();} catch (Exception e) {log.error(String.format("Close http client throw exception: %s", e.getMessage()), e);}}}return rtn;}
}

application.properties配置如下:

server.port=8084
server.servlet.context-path=/ossapp.login.url=http://192.168.31.112/v1/auth/login
app.user=admin
app.pass=4de93544234adffbb681ed60ffcfb941
app.index.url=http://192.168.31.112spring.thymeleaf.cache=false
spring.thymeleaf.check-template=true
spring.thymeleaf.check-template-location=true
spring.thymeleaf.servlet.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.excluded-view-names=
spring.thymeleaf.mode=HTML5
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

这样当访问http://192.168.31.112/oss/test时,系统通过端调用原有系统的认证接口,将相关的认证信息写入cookie,然后当前端跳转到新系统时,就实现了免登录功能。

几种写Cookie坑

由于这里写入Cookie的值为JSON格式,且为没有编码的,因此这里尝试了几种其他的方式写入Cookie,都失效了,因此记录如下:

后端Java实现

    private void setCookie(HttpServletResponse response, String key, String value) {log.debug("Set cookie {}={}", key, value);Cookie cookie = new Cookie(key, value);cookie.setMaxAge(3600 * 24 * 30);cookie.setPath("/");response.addCookie(cookie);}

无法写入JSON,提示错误如下

2024-10-01 21:40:56.737 DEBUG Set cookie data={"a":"b"} [http-nio-8084-exec-3](org.example.onemaposs.controller.IndexController:121)
2024-10-01 21:40:56.749 ERROR Servlet.service() for servlet [dispatcherServlet] in context with path [/oss] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: An invalid character [34] was present in the Cookie value] with root cause [http-nio-8084-exec-3](org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/oss].[dispatcherServlet]:175)
java.lang.IllegalArgumentException: An invalid character [34] was present in the Cookie valueat org.apache.tomcat.util.http.Rfc6265CookieProcessor.validateCookieValue(Rfc6265CookieProcessor.java:197)at org.apache.tomcat.util.http.Rfc6265CookieProcessor.generateHeader(Rfc6265CookieProcessor.java:123)at org.apache.catalina.connector.Response.generateCookieString(Response.java:1001)at org.apache.catalina.connector.Response.addCookie(Response.java:953)......

前端JavaScript实现1

采用jquery.cookie.min.js,代码如下

function setCookie(key, value) {$.cookie(key, value, {expires: 8,   // 有效期为7天path: '/',    // 在整个网站有效domain: '',   // 默认的域secure: false // 不使用安全协议});
}setCookie('auth_token', '{"data":"xxxxxx"}');

写入Cookie中的{、}、"、:、+、空格等等均被编码了,因此现有系统在读取后转JSON报错。

前端JavaScript实现2

以下这种方法也不行,去掉encodeURIComponent也不行


setCookies([["user_name", "admin"],["auth_token", "{\"data\":\"xxxxxx\"}"]
]);function setCookies(nameValuePairList) {var cookieList = nameValuePairList.map(function(nameValuePair) {var cookieString = nameValuePair[0] + "=" + encodeURIComponent(nameValuePair[1]);// 可以添加其他属性,如过期时间、路径、域等return cookieString;});document.cookie = cookieList.join("; ");
}function escapeCookieValue(value) {return value.replace(/{/g, '\\{').replace(/}/g, '\\}').replace(/"/g, '\\"').replace(/,/g, '\\,').replace(/;/g, '\\;').replace(/ /g, '\\ ');
}function setCookie(key, value) {$.cookie(key, value, {expires: 8,   // 有效期为7天path: '/',    // 在整个网站有效domain: '',   // 默认的域secure: false // 不使用安全协议});
}

前端JavaScript实现3

定义Cookie类也不行

let ht;
if (!ht) ht = {};ht.Cookie = function () {let _p = ht.Cookie.prototype;_p.getCookieVal = function (offset) {let endstr = document.cookie.indexOf(";", offset);if (endstr == -1) {endstr = document.cookie.length;}return unescape(document.cookie.substring(offset, endstr));};_p.set = function (name, value) {let expdate = new Date();let argv = arguments;let argc = arguments.length;let expires = (argc > 2) ? argv[2] : null;let path = (argc > 3) ? argv[3] : "/";let domain = (argc > 4) ? argv[4] : null;let secure = (argc > 5) ? argv[5] : false;if (expires != null) {let temp = 0;let rdigit = /\d/;if (rdigit.test(expires) && !isNaN(expires)) {temp = parseInt(expires);}temp = temp <= 0 ? 1 : temp;expdate.setTime(expdate.getTime() + (temp * 1000 * 3600 * 24));}document.cookie = name+ "=" + value// + escape(value)+ ((expires == null) ? "" : ("; expires=" + expdate.toGMTString()))+ ((path == null) ? "" : ("; path=" + path))+ ((domain == null) ? "" : ("; domain=" + domain))+ ((secure == true) ? "; secure" : "");};_p.get = function (name) {let arg = name + "=";let alen = arg.length;let clen = document.cookie.length;let i = 0;while (i < clen) {var j = i + alen;if (document.cookie.substring(i, j) == arg) {return this.getCookieVal(j);}i = document.cookie.indexOf(" ", i) + 1;if (i == 0) {break;}}return null;};_p.remove = function (name) {let exp = new Date();exp.setTime(exp.getTime() - 1);let cval = this.get(name);document.cookie = name + "=" + cval + "; expires=" + exp.toGMTString() + "; path=/";};_p.removeAll = function clearAllCookie() {let keys = top.document.cookie.match(/[^ =;]+(?=\=)/g);if(keys) {for(var i = keys.length; i--;)  {let exp = new Date();exp.setTime(exp.getTime() - 1);document.cookie = keys[i] + '=0;expires=' + exp.toGMTString() + "; path=/";}}}
};
// cookie实例
ht.cookie = new ht.Cookie();ht.cookie.set('user_name', 'admin');
ht.cookie.set('auth_token', '{"data":"xxxxxx"}');

前端JavaScript实现4

使用document.cookie来设置Cookie也无法生效

$(document).ready(function() {let str = 'auth_token={"data":"xxxxxx"}; user_name=admin; is_anager=true;';let expirationDate = new Date();expirationDate.setDate(expirationDate.getDate() + 7);document.cookie = str + "expires=" + expirationDate.toUTCString() + ";path=/";
});

相关文章:

  • 如何用ChatGPT制作一款手机游戏应用
  • 10.1 刷题
  • 大数据-156 Apache Druid 案例实战 Scala Kafka 订单统计
  • unity一键注释日志和反注释日志
  • Linux启动mysql报错
  • 字符和Ascll码表
  • android compose ScrollableTabRow indicator 指示器设置宽度
  • uni-app #ifdef #ifndef #endif的使用和区别
  • 【架构】NewSQL
  • 去中心化自治组织(DAO)
  • [Python学习日记-34] 一篇文章让你弄懂 Python 中牛逼的递归函数
  • 【前端安全】js逆向之微信公众号登录密码
  • Golang | Leetcode Golang题解之第440题字典序的第K小数字
  • java-快速将普通main类变为javafx类,并加载自定义fxml
  • go 安装三方库
  • 2017 年终总结 —— 在路上
  • 2017-08-04 前端日报
  • C++入门教程(10):for 语句
  • co.js - 让异步代码同步化
  • happypack两次报错的问题
  • HTML中设置input等文本框为不可操作
  • Js基础——数据类型之Null和Undefined
  • PaddlePaddle-GitHub的正确打开姿势
  • PHP CLI应用的调试原理
  • Python - 闭包Closure
  • Python学习之路13-记分
  • supervisor 永不挂掉的进程 安装以及使用
  • vue学习系列(二)vue-cli
  • Webpack 4 学习01(基础配置)
  • 关于Flux,Vuex,Redux的思考
  • 警报:线上事故之CountDownLatch的威力
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 如何打造100亿SDK累计覆盖量的大数据系统
  • 数组大概知多少
  • 算法-插入排序
  • 3月7日云栖精选夜读 | RSA 2019安全大会:企业资产管理成行业新风向标,云上安全占绝对优势 ...
  • postgresql行列转换函数
  • ​ssh免密码登录设置及问题总结
  • ###STL(标准模板库)
  • #{}和${}的区别是什么 -- java面试
  • #HarmonyOS:基础语法
  • (12)目标检测_SSD基于pytorch搭建代码
  • (C语言)输入自定义个数的整数,打印出最大值和最小值
  • (ISPRS,2021)具有遥感知识图谱的鲁棒深度对齐网络用于零样本和广义零样本遥感图像场景分类
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (Oracle)SQL优化基础(三):看懂执行计划顺序
  • (读书笔记)Javascript高级程序设计---ECMAScript基础
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (附源码)ssm高校实验室 毕业设计 800008
  • (附源码)基于SpringBoot和Vue的厨到家服务平台的设计与实现 毕业设计 063133
  • (六)DockerCompose安装与配置
  • (十)【Jmeter】线程(Threads(Users))之jp@gc - Stepping Thread Group (deprecated)
  • (详细文档!)javaswing图书管理系统+mysql数据库
  • (循环依赖问题)学习spring的第九天
  • (一)utf8mb4_general_ci 和 utf8mb4_unicode_ci 适用排序和比较规则场景