前言

最近有个需求,需要将我们一个平台对接到redmine,让用户可以通过这个平台直接在redmine提工单,需要实现免登录跳转。首先是想到去查redmine有无相应的单点登录功能,查到redmine是有LDAP认证功能的,

解决方案

LDAP认证

Redmine 支持通过 LDAP (轻量级目录访问协议) 实现用户认证,这使得它可以与现有的目录服务(如 Active Directory 或 OpenLDAP)集成,进而实现多系统认证。这种集成让用户能够使用他们的公司或组织凭据登录 Redmine,简化了帐户管理并提高了安全性。下面是如何在 Redmine 中配置 LDAP 认证的详细步骤:

  1. 访问 Redmine 管理界面

首先,您需要有 Redmine 的管理员权限才能配置 LDAP 认证。

  • 登录到 Redmine。
  • 导航到 "管理" > "配置" > "LDAP认证"
  1. 新增 LDAP 认证方式

在 LDAP 认证页面,点击 "新增LDAP认证方式"

  1. 填写 LDAP 服务器信息

在新增页面,您需要填写 LDAP 服务器的详细信息:

  • 名称:为您的 LDAP 配置命名,例如 “公司LDAP”。
  • 主机:LDAP 服务器的地址。例如,ldap.example.com
  • 端口:LDAP 服务器的端口,默认是 389,如果使用 SSL,则可能是 636
  • 帐号密码:如果您的 LDAP 服务器不允许匿名绑定,您需要提供一个帐号和密码来绑定LDAP目录。
  • 基准DN:基础 DN(Distinguished Name),用于搜索用户,例如 ou=people,dc=example,dc=com
  • LDAP 过滤器:(可选)一个过滤器,用于在搜索时筛选用户,例如 (objectClass=person)
  • 在登录时动态创建帐户:如果启用,当通过 LDAP 认证的用户首次登录时,Redmine 会自动创建一个对应的用户帐户。
  • 属性映射:配置如何从 LDAP 属性映射到 Redmine 用户属性,例如用户名、邮件等。
  1. 测试连接

填写完毕后,您可以使用提供的测试功能来验证 Redmine 是否能够成功连接到 LDAP 服务器。通常,您需要输入一个有效的 LDAP 用户名和密码来测试是否能够成功认证。

  1. 保存配置

如果测试成功,保存您的配置。现在,Redmine 已配置为使用 LDAP 认证用户。

  1. LDAP 安全性考虑
  • 使用 SSL/TLS:如果可能,应配置 LDAP 服务器以使用 SSL(LDAPS)或通过 STARTTLS 加密通信,以提高安全性。
  • 最小权限原则:用于绑定 LDAP 的帐号应该具有最低必要的权限,仅足以搜索和读取用户信息。
  1. 多系统认证

通过 LDAP 认证集成,Redmine 可以与其他同样配置为从同一 LDAP 目录服务认证的系统共享用户帐户和登录凭据。这意味着用户可以使用同一套凭据在多个系统(如电子邮件、VPN、Redmine 等)中登录,实现单点登录(SSO)的效果。

存在的问题

原先的系统并没有集成LDAP认证,这套方案明显是行不通的,只能找另外的方案。

获取Redmine Cookie实现登录

在谷歌上搜索一些与redmine有关的多端登录的插件,感觉都不太适合当下的场景。

通过Cookie实现Redmine单点登录

在网上看到有这一篇文,感觉可以按这个思路来

从Java后端获取Redmine的cookie代码

redmine登录需要以下的参数,以post方式发送formdata数据,以下是数据:

除了以上数据,header还需要加入一个必要字段Cookie。

这里第一张图的authenticity_token和Cookie,都是用户第一次访问redmine的登录页面时候获取的,这意味着,要拿到登录后的Cookie需要发送两次请求,第一次请求login页面拿到上面两个字段,第二次请求通过POST请求把参数注入,发送完后获取相应的cookie,完整代码如下

@Slf4j
public class RedmineUtil { /********************************
* @function : 根据用户和url获取cookie
* @parameter : [userName | 用户名, password | 密码, loginUrl | 登录url]
* @return : java.lang.String
* @date : 2024/2/1 14:21
********************************/
public static String getCookie(String userName, String password, String loginUrl) throws IOException {
HttpURLConnection firstConnection = (HttpURLConnection) new URL(loginUrl).openConnection();
String cookie = firstConnection.getHeaderField("Set-Cookie");
int pos = cookie.indexOf(";");
cookie = cookie.substring(0, pos);
// 通过正则在登录页面内解析出登录需要的authenticity_token
String loginPageSource = readResponse(firstConnection);
String authenticityToken = extractAuthToken(loginPageSource);
cookie += ";sso=true"; // 这一行后面重点讲
log.info("username:{},authenticityToken:{}, cookie:{}", userName, authenticityToken, cookie);
// 登录参数
LinkedHashMap<String, String> values = new LinkedHashMap<>();
values.put("utf8", "✓");
values.put("authenticity_token", authenticityToken);
values.put("back_url", "/");
values.put("username", userName);
values.put("password", password);
values.put("login", "登录"); String formData = getPostDataString(values); byte[] bytes = formData.getBytes(StandardCharsets.UTF_8); // 请求头
Map<String, String> headers = new HashMap<>();
headers.put("Cookie", cookie);
// // 发送登录表单
HttpURLConnection secondConnect = sendPostRequest(loginUrl, headers, bytes);
cookie = secondConnect.getHeaderField("Set-Cookie");
pos = cookie.indexOf(";");
int start = cookie.indexOf("=");
cookie = cookie.substring(start + 1, pos);
log.info("username:{}, Login Status Code:{}, cookie:{} " ,userName,secondConnect.getResponseCode(), cookie);
return cookie;
} /********************************
* @function : 读取response
* @parameter : [connection | 连接]
* @return : java.lang.String
* @date : 2024/2/4 16:23
********************************/
private static String readResponse(HttpURLConnection connection) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
}
} /********************************
* @function : 通过正则表达式获取token的值
* @parameter : [pageSource | 网页内容]
* @return : java.lang.String
* @date : 2024/2/4 16:23
********************************/
private static String extractAuthToken(String pageSource) {
// 使用正则表达式提取 authenticity_token
// 这里的正则表达式可能需要根据实际页面结构进行调整
String pattern = "<meta\\s+name=\"csrf-token\"\\s+content=\"(.*?)\"\\s*/?>";
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern, java.util.regex.Pattern.DOTALL);
java.util.regex.Matcher matcher = regex.matcher(pageSource);
if (matcher.find()) {
return matcher.group(1);
}
return null;
} /********************************
* @function :
* @parameter : [url | 链接, headers | 请求头, bytes | post的body]
* @return : java.net.HttpURLConnection
* @date : 2024/2/4 16:24
********************************/
private static HttpURLConnection sendPostRequest(String url, Map<String, String> headers, byte[] bytes) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
if (headers != null) {
setHeaders(connection, headers);
}
try (OutputStream os = connection.getOutputStream()) {
if (bytes != null) {
os.write(bytes);
}
os.flush();
}
return connection;
} /********************************
* @function : 设置请求头到connection里面
* @parameter : [connection | 连接, headers | 请求头]
* @return : void
* @date : 2024/2/4 16:25
********************************/
private static void setHeaders(HttpURLConnection connection, Map<String, String> headers) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
connection.setRequestProperty(entry.getKey(), entry.getValue());
}
} /********************************
* @function : 对参数进行编码
* @parameter : [postData | 表单参数]
* @return : java.lang.String
* @date : 2024/2/1 14:25
********************************/
private static String getPostDataString(Map<String, String> postData) throws UnsupportedEncodingException {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : postData.entrySet()) {
result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
result.append("&");
}
String resultString = result.toString();
return resultString.length() > 0 ? resultString.substring(0, resultString.length() - 1) : resultString;
}
}

遇到的问题

本以为这样子就可以实现,但是拿到的cookie一直是错的,我反复对比了我跟浏览器登录发的参数的区别,看了老久都没找出问题。于是我去查看redmine的log

redmine是使用docker部署的,通过docker logs 容器id,看到的问题是:

无论是浏览器登录和java发起请求登录,redmine里面都successfully authorize,在登录成功后redmine会发起一个302重定向,在发起302重定向的时候,redmine会读取当前的登录状态,java发起的请求当前的登录用户不能够获取到,于是返回的cookie结果不对,但是浏览器的却可以,我去翻了redmine的源码。

进入/redmine/app/controllers/account_controller.rb,

用户调用login接口后,会进入到这个方法def successful_authentication(user):

  def successful_authentication(user)
logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
# Valid user
self.logged_user = user
# generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin?
set_autologin_cookie(user)
else
call_hook(:controller_account_success_authentication_after, {:user => user})
redirect_back_or_default my_page_path
end
end

每次登录后都会进行302重定向,进入else语句中的call_hook,但是不知道为啥用java发出的请求,在这里面redirect后丢失了登录状态,而且猜想cookie的设置也这种情况下也是在redirect后才设值的,上面的set_autologin_cookie(user)方法如下:

  def set_autologin_cookie(user)
token = user.generate_autologin_token
secure = Redmine::Configuration['autologin_cookie_secure']
if secure.nil?
secure = request.ssl?
end
cookie_options = {
:value => token,
:expires => 1.year.from_now,
:path => (Redmine::Configuration['autologin_cookie_path'] || RedmineApp::Application.config.relative_url_root || '/'),
:same_site => :lax,
:secure => secure,
:httponly => true
}
cookies[autologin_cookie_name] = cookie_options
end

可以看到这个方法里面进行了cookie的设置,那就在登录的时候让他不进行重定向进到这个方法里好了,修改def successful_authentication(user)方法为:

  def successful_authentication(user)
logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
# Valid user
self.logged_user = user
# generate a key and set cookie if autologin
logger.info " if cookie '#{cookies[:sso] && cookies[:sso] == "true"}'"
if cookies[:sso] && cookies[:sso] == "true"
set_autologin_cookie(user)
else
call_hook(:controller_account_success_authentication_after, {:user => user})
redirect_back_or_default my_page_path
end
end

并且在RedmineUtil方法的getCookie方法里面,增加一行代码:

在cookie里面携带sso=true这个标识,就不会进行重定向,直接返回cookie,经过修改后得到的cookie已经能够使用了。

Cookie写到Response返回给前端

接下来需要把cookie返回给前端,在controller层,将cookie直接set到response里面:

    @GetMapping(value = "/redirect")
public RedirectView redirectToExternalWithCookie(HttpServletResponse response) throws IOException {
// 创建 Cookie
String cookie1 = RedmineUtil.getCookie("admin", "xx", "http:/ip/redmine/login");
Cookie cookie = new Cookie("_redmine_session", cookie1);
cookie.setPath("/");
// 根据需要设置其他 Cookie 属性,如 domain, secure 等
// 将 Cookie 添加到响应中
response.addCookie(cookie);
// 重定向到外部地址
return new RedirectView("http://ip/redmine/projects");
}

调试的时候,直接访问这个接口,也可以直接重定向到redmine并跳过登录的步骤。

redmine获取cookie和其他系统实现单点登录的更多相关文章

  1. 多系统实现单点登录方案:SSO 单点登录

    一.什么是单点登录SSO(Single Sign-On) SSO是一种统一认证和授权机制,指访问同一服务器不同应用中的受保护资源的同一用户,只需要登录一次,即通过一个应用中的安全验证后,再访问其他应用 ...

  2. 从其它系统登录到SharePoint 2010系统的单点登录

    以前做的只是使用SharePoint的单一登录,用SharePoint去登录其他的系统,现在要反过来,用Form认证的系统来登录SharePoint. 我们都知道,SharePoint使用的是域认证系 ...

  3. ASP.NET Core Authentication系列(四)基于Cookie实现多应用间单点登录(SSO)

    前言 本系列前三篇文章分别从ASP.NET Core认证的三个重要概念,到如何实现最简单的登录.注销和认证,再到如何配置Cookie 选项,来介绍如何使用ASP.NET Core认证.感兴趣的可以了解 ...

  4. SSO 实现博客系统的单点登录

    https://blog.csdn.net/qq1350048638/article/details/78933375 https://blog.csdn.net/yejingtao703/artic ...

  5. 单点登录系统实现基于SpringBoot

    今天的干货有点湿,里面夹杂着我的泪水.可能也只有代码才能让我暂时的平静.通过本章内容你将学到单点登录系统和传统登录系统的区别,单点登录系统设计思路,Spring4 Java配置方式整合HttpClie ...

  6. B/S系统间跨域单点登录设计思路

    基于B/S系统间单点登录 此处说的单点登录的概念,即不同系统公用一个登录界面.一处系统通过登录验证,在接入的各系统均为登录状态.一般有两种情景: 1)  一级域名相同 例如:tieba.baidu.c ...

  7. SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统

    一.单点登录SSO介绍   目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的 ...

  8. cookie跨域,跨目录访问及单点登录。

    首先普及下域名的知识: 域名: baidu.com    // 一级域名  A play.baidu.com  //  二级域名 B abc.play.baidu.com // 三级域名  C 数有几 ...

  9. 单点登录(二)使用Cookie+File实现单点登录登出(附源代码)

    上一篇文章<单点登录(一)使用Cookie+File实现单点登录>中,我们实现了单点登录的功能. 本文作为上一篇文章的扩展部分,加入"单点登出"功能. 源代码下载:链接 ...

  10. 哔哩哔哩b站提取Cookie方法,bilibili获取Cookie教程

    大家可能对Cookie很陌生,甚至不知道是干嘛用,没关系,今天小编详细给大家讲解! Cookie是保存在客户端的纯文本文件,比如txt文件,所谓的客户端就是我们自己的本地电脑,当我们使用自己的电脑通过 ...

随机推荐

  1. 全流程机器视觉工程开发(一)环境准备,paddledetection和labelme

    前言 我现在在准备做一个全流程的机器视觉的工程,之前做了很多理论相关的工作.大概理解了机器视觉的原理,然后大概了解了一下,我发现现在的库其实已经很发展了,完全不需要用到非常多的理论,只需要知道开发过程 ...

  2. 以太网链路连接 和 ISIS/OSPF等路由协议关系

    转载请注明出处: 以太网链路连接和ISIS/OSPF协议之间存在关联和区别 关联: 以太网链路连接是指通过以太网物理媒介(如电缆)将网络设备进行连接,使它们可以交换数据. ISIS(Intermedi ...

  3. JVM 性能调优 及 为什么要减少 Full GC

    本文为博主原创,未经允许不得转载: 系统上线压测,需要了解系统的瓶颈以及吞吐量,并根据压测数据进行对应的优化. 对压测进行 JVM 性能优化,有两条思路: 第一种情况 : 使用压测工具 jmeter  ...

  4. zookeeper源码(03)启动流程

    本文将从启动类开始详细分析zookeeper的启动流程: 加载配置的过程 集群启动过程 单机版启动过程 启动类 org.apache.zookeeper.server.quorum.QuorumPee ...

  5. 搭建 github 报错 Permission denied (publickey)

    将 key 加入 github 出现如下问题 这是本地仓 user.name user.email 与 github 注册信息不一致造成 将本地仓 user 信息与 github 修改一致,出现如下问 ...

  6. 【KEIL】User's Guide

    µVision User's Guide

  7. ONVIF网络摄像头(IPC)客户端开发—RTSP RTCP RTP加载AAC音频流

    前言: RTSP,RTCP,RTP一般是一起使用,在FFmpeg和live555这些库中,它们为了更好的适用性,所以实现起来非常复杂,直接查看FFmpeg和Live555源代码来熟悉这些协议非常吃力, ...

  8. [转帖]SPEC-cpu2006的详细使用一键安装、手动安装。

    一.SPEC-cpu2006简介 SPEC CPU 2006 benchmark是SPEC新一代的行业标准化的CPU测试基准套件.重点测试系统的处理器,内存子系统和编译器. 说明:由于spec2006 ...

  9. Vue 中keep-alive组件将会被缓存

    动态包裹哈 <keep-alive> <component :is="comName"></component> </keep-alive ...

  10. 往返回来的数据数组Array中添加一个字段的最优写法

    在工作中我们经常会对后端返回来的数据进行添加一个字段: 最优的写法是 直接在 res.data[i].xx=aa 这样的方式去添加: 添加好了之后美酒 可以去赋值了: 让表格去渲染数据 this.$a ...