jira单点登录原理:
jira单点登录依赖于seraph插件。在searph的配置文件中引入我们自定义的验证类(类似一个过滤器),jira登录时会解析代码中的逻辑。具体内部实现逻辑自己定义,如header,token,cookie等等形式。
jira单点登录官方资料:
https://docs.atlassian.com/atlassian-seraph/2.6.1-m1/configuration.html
https://docs.atlassian.com/atlassian-seraph/2.6.1-m1/sso.html
集成jira单点实现步骤:
1,找到seraph-config.xml文件。一般在\atlassian-jira\WEB-INF\classes目录下。此处我注释掉了jira原本的seraph验证器,引入了自定义的验证器,其中实现了jira原有的逻辑同时,引入了自定义的逻辑。
<!-- CROWD:START - The authenticator below here will need to be commented out for Crowd SSO integration -->
<!-- <authenticator class="com.atlassian.jira.security.login.JiraSeraphAuthenticator"/>-->
<!-- CROWD:END -->
<!-- 注释掉上方jira自身的登录逻辑,自定义一个类,实现JiraSeraphAuthenticator自身原有三种方式的同事,添加自定义的的单点逻辑 -->
<authenticator class="com.atlassian.jira.security.cuslogin.CusJiraSeraphAuthenticator"/>
2,编写自定义单点逻辑类(我一共三个,一个核心类,两个工具类)。开发如下工具类需要三个jar包,可在jira部署的lib中找到。(有些jar可能根据版本不同,名称存在差异,也可自己去atlassian下载对应版本)
插件代码如下
核心逻辑类:
package com.atlassian.jira.security.cuslogin;
import com.atlassian.crowd.embedded.api.CrowdService;
import com.atlassian.crowd.exception.AccountNotFoundException;
import com.atlassian.crowd.exception.FailedAuthenticationException;
import com.atlassian.crowd.exception.runtime.CommunicationException;
import com.atlassian.crowd.exception.runtime.OperationFailedException;
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.user.util.UserManager;
import com.atlassian.seraph.auth.AuthenticationContextAwareAuthenticator;
import com.atlassian.seraph.auth.AuthenticationErrorType;
import com.atlassian.seraph.auth.AuthenticatorException;
import com.atlassian.seraph.auth.DefaultAuthenticator;
import com.atlassian.seraph.auth.LoginReason;
import com.atlassian.seraph.elevatedsecurity.ElevatedSecurityGuard;
import com.atlassian.seraph.util.SecurityUtils;
import com.atlassian.seraph.util.SecurityUtils.UserPassCredentials;
import java.io.IOException;
import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
@AuthenticationContextAwareAuthenticator
public class CusJiraSeraphAuthenticator extends DefaultAuthenticator {
private static final Logger log = Logger.getLogger(CusJiraSeraphAuthenticator.class);
protected Principal getUser(String username) {
return getUserManager().getUserByName(username);
}
/**
* 权限鉴定
* @param user
* @param password
* @return
* @throws AuthenticatorException
*/
protected boolean authenticate(Principal user, String password)
throws AuthenticatorException {
try {
crowdServiceAuthenticate(user, password);
return true;
} catch (AccountNotFoundException e) {
log.debug("authenticate : '" + user.getName() + "' does not exist and cannot be authenticated.");
return false;
} catch (FailedAuthenticationException e) {
return false;
} catch (CommunicationException ex) {
throw new AuthenticatorException(AuthenticationErrorType.CommunicationError);
} catch (OperationFailedException ex) {
log.error("Error occurred while trying to authenticate user '" + user.getName() + "'.", ex);
}
throw new AuthenticatorException(AuthenticationErrorType.UnknownError);
}
/**
* 鉴定用户名和密码
* @param user
* @param password
* @throws FailedAuthenticationException
*/
private void crowdServiceAuthenticate(Principal user, String password)
throws FailedAuthenticationException {
Thread currentThread = Thread.currentThread();
ClassLoader origCCL = currentThread.getContextClassLoader();
try {
currentThread.setContextClassLoader(getClass().getClassLoader());
getCrowdService().authenticate(user.getName(), password);
} finally {
currentThread.setContextClassLoader(origCCL);
}
}
/**
* 刷新从session中获取的主体
* @param httpServletRequest
* @param principal
* @return
*/
protected Principal refreshPrincipalObtainedFromSession(HttpServletRequest httpServletRequest,
Principal principal) {
Principal freshPrincipal = principal;
if ((principal != null) && (principal.getName() != null)) {
if ((principal instanceof ApplicationUser)) {
freshPrincipal = getUserManager().getUserByKey(((ApplicationUser) principal).getKey());
} else {
freshPrincipal = getUser(principal.getName());
}
putPrincipalInSessionContext(httpServletRequest, freshPrincipal);
}
return freshPrincipal;
}
/**
* 从基本权限中获取用户信息
* @param httpServletRequest
* @param httpServletResponse
* @return
*/
protected Principal getUserFromBasicAuthentication(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) {
String METHOD = "getUserFromSession : ";
boolean dbg = log.isDebugEnabled();
String header = httpServletRequest.getHeader("Authorization");
LoginReason reason = LoginReason.OK;
if (SecurityUtils.isBasicAuthorizationHeader(header)) {
if (dbg) {
log.debug("getUserFromSession : Looking in Basic Auth headers");
}
SecurityUtils.UserPassCredentials creds =
SecurityUtils.decodeBasicAuthorizationCredentials(header);
ElevatedSecurityGuard securityGuard = getElevatedSecurityGuard();
if (!securityGuard.performElevatedSecurityCheck(httpServletRequest, creds.getUsername())) {
if (dbg) {
log.debug("getUserFromSession : '" + creds.getUsername() + "' failed elevated security check");
}
reason = LoginReason.AUTHENTICATION_DENIED.stampRequestResponse(httpServletRequest,
httpServletResponse);
securityGuard.onFailedLoginAttempt(httpServletRequest, creds.getUsername());
} else {
if (dbg) {
log.debug("getUserFromSession : '" + creds.getUsername() + "' does not require elevated security check. Attempting authentication...");
}
try {
boolean loggedin = login(httpServletRequest, httpServletResponse, creds.getUsername(),
creds.getPassword(), false);
if (loggedin) {
reason = LoginReason.OK.stampRequestResponse(httpServletRequest, httpServletResponse);
securityGuard.onSuccessfulLoginAttempt(httpServletRequest, creds.getUsername());
if (dbg) {
log.debug("getUserFromSession : Authenticated '" + creds.getUsername() + "' via Basic Auth");
}
return getUser(creds.getUsername());
}
reason = LoginReason.AUTHENTICATED_FAILED.stampRequestResponse(httpServletRequest,
httpServletResponse);
securityGuard.onFailedLoginAttempt(httpServletRequest, creds.getUsername());
} catch (AuthenticatorException e) {
log.warn("getUserFromSession : Exception trying to login '" + creds.getUsername() + "' via Basic Auth:" + e, e);
}
}
try {
httpServletResponse.sendError(401, "Basic Authentication Failure - Reason : " + reason.toString());
} catch (IOException e) {
log.warn("getUserFromSession : Exception trying to send Basic Auth failed error: " + e, e);
}
return null;
}
try {
httpServletResponse.setHeader("WWW-Authenticate", "Basic realm=\"protected-area\"");
httpServletResponse.sendError(401);
} catch (IOException e) {
log.warn("getUserFromSession : Exception trying to send Basic Auth failed error: " + e, e);
}
return null;
}
private CrowdService getCrowdService() {
return (CrowdService) ComponentAccessor.getComponent(CrowdService.class);
}
private UserManager getUserManager() {
return ComponentAccessor.getUserManager();
}
/**
* 自定义单点逻辑,校验第三方token进行单点验证
* @param request
* @param response
* @return
*/
public Principal getUser(HttpServletRequest request, HttpServletResponse response) {
Principal user = null;
try {
//生成封装request后的cusSSOObj对象
CusSSOUtil cusSSOObj = CusSSOUtil.getSSOObj(request);
log.info("cus Got cusSSOObj " + cusSSOObj);
//判断token是否有效
if (cusSSOObj != null && !cusSSOObj.isExpired()) { // Seamless login from intranet
log.info("Trying seamless Single Sign-on...");
//从token中获取用户名
String username = cusSSOObj.getLoginId();
System.out.println("Got username = " + username);
//判断是否取到用户名
if (username != null) {
//根据用户名生成Principal对象
user = getUser(username);
//判断是否映射到jira用户
if (null != user) {
//单点登录成功,设置jira单点的信息
log.info("Logged in via SSO, with User " + user);
request.getSession().setAttribute(DefaultAuthenticator.LOGGED_IN_KEY,
user);
request.getSession().setAttribute(DefaultAuthenticator.LOGGED_OUT_KEY, null);
} else {
//匹配jira用户失败
System.out.println("get not user");
}
}
} else {
log.info("cusSSOObj is null;");
//自定义单点失败后,使用jira自身登录逻辑(尝试从 session,cookie,BasicAuthentication 获取登录用户)
log.info("exceute jira login logic");
user = super.getUser(request, response);
if(user!=null){
return user;
}
//user was not found, or not currently valid
return null;
}
} catch (Exception e) // catch class cast exceptions
{
log.warn("Exception: " + e, e);
}
return user;
}
}
Token加密工具类
package com.atlassian.jira.security.cuslogin;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Date;
/**
* @description:自定义token加密解密工具类
* @author:cuiyao
* @date:2021/10/15
*/
public class CusEncrypTokenUtil {
/**
* aes加密解密的秘钥
*/
public static final String RULE = "customEncryptKey";
/**
* easy自定义生成token
* @param username
* @param expireNum 单位是s
* @return
*/
public static String cusTokenCreate(String username,int expireNum){
if(username==null||expireNum<=0){
return null;
}
//生成时间
Long createtime = System.currentTimeMillis();
//生成token
String token = createtime+"-"+username+"-"+expireNum;
//String token = encode(username+"-"+createtime+"-"+expireNum);
//token = strTo16(token);
//AES加密
try {
token = aesEncrypt(token);
} catch (Exception e) {
e.printStackTrace();
}
return token;
}
/**
* 解析自定义token,返回用户名
* @param token
* @return
*/
public static String cusTokenParseUsername(String token){
//还原字符串
try {
token = aesDecrypt(token);
} catch (Exception e) {
e.printStackTrace();
}
//token = hexStringToString(token);
//token = decode(token);
//分割
String[] tokenArr = token.split("-");
return tokenArr[1];
}
/**
* 验证自定义token有效性
* @param token
* @return
*/
public static boolean cusIsExpire(String token){
//还原字符串
try {
token = aesDecrypt(token);
} catch (Exception e) {
e.printStackTrace();
}
//token = hexStringToString(token);
//token = decode(token);
//分割
String[] tokenArr = token.split("-");
//获取过期时间和创建时间
Integer expireNum = Integer.valueOf(tokenArr[2]);
Long createtime = Long.valueOf(tokenArr[0]);
//当前时间
Long currentTime = System.currentTimeMillis();
//过期时间
Long expiresTime = createtime+expireNum*1000;
//如果当前时间小于过期时间,则没有过期
if(currentTime<expiresTime){
return false;
}
return true;
}
/**
* 字符串转化成为16进制字符串
* @param s
* @return
*/
public static String strTo16(String s) {
String str = "";
for (int i = 0; i < s.length(); i++) {
int ch = (int) s.charAt(i);
String s4 = Integer.toHexString(ch);
str = str + s4;
}
return str;
}
/**
* 16进制转换成为string类型字符串
* @param s
* @return
*/
public static String hexStringToString(String s) {
if (s == null || s.equals("")) {
return null;
}
s = s.replace(" ", "");
byte[] baKeyword = new byte[s.length() / 2];
for (int i = 0; i < baKeyword.length; i++) {
try {
baKeyword[i] = (byte) (0xff & Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16));
} catch (Exception e) {
e.printStackTrace();
}
}
try {
s = new String(baKeyword, "UTF-8");
new String();
} catch (Exception e1) {
e1.printStackTrace();
}
return s;
}
/**
* 加密,把一个字符串在原有的基础上+2
* @param data 需要解密的原字符串
* @return 返回解密后的新字符串
*/
public static String encode(String data) {
//把字符串转为字节数组
byte[] b = data.getBytes();
//遍历
for(int i=0;i<b.length;i++) {
b[i] += 2;//在原有的基础上+2
}
return new String(b);
}
/**
* 解密:把一个加密后的字符串在原有基础上-2
* @param data 加密后的字符串
* @return 返回解密后的新字符串
*/
public static String decode(String data) {
//把字符串转为字节数组
byte[] b = data.getBytes();
//遍历
for(int i=0;i<b.length;i++) {
b[i] -= 2;//在原有的基础上-2
}
return new String(b);
}
/**
* AES加密字符串
* @param content
* @return
* @throws Exception
*/
public static String aesEncrypt(String content) throws Exception {
// 1.构造密钥生成器,指定为AES算法,不区分大小写
KeyGenerator keygen = KeyGenerator.getInstance("AES");
// 2.根据ecnodeRULEs规则初始化密钥生成器
// 生成一个128位的随机源,根据传入的字节数组
// 防止linux下 随机生成key
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(RULE.getBytes());
// 根据密钥初始化密钥生成器
keygen.init(128, secureRandom);
// 3.产生原始对称密钥
SecretKey original_key = keygen.generateKey();
// 4.获得原始对称密钥的字节数组
byte[] raw = original_key.getEncoded();
// 5.根据字节数组生成AES密钥
SecretKey key = new SecretKeySpec(raw, "AES");
// 6.根据指定算法AES自成密码器
Cipher cipher = Cipher.getInstance("AES");
// 7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作,第二个参数为使用的KEY
cipher.init(Cipher.ENCRYPT_MODE, key);
// 8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
byte[] byte_encode = content.getBytes("utf-8");
// 9.根据密码器的初始化方式--加密:将数据加密
byte[] byte_AES = cipher.doFinal(byte_encode);
// 10.将加密后的数据转换为字符串
String AES_encode = new String(new BASE64Encoder().encode(byte_AES));
// 11.将字符串返回
return AES_encode;
}
/**
* AES解密字符串
* @param content
* @return
* @throws Exception
*/
public static String aesDecrypt(String content) throws Exception {
content = content.replaceAll(" ", "+");
// 1.构造密钥生成器,指定为AES算法,不区分大小写
KeyGenerator keygen = KeyGenerator.getInstance("AES");
// 2.根据ecnodeRULEs规则初始化密钥生成器
// 生成一个128位的随机源,根据传入的字节数组
// 防止linux下 随机生成key
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(RULE.getBytes());
// 根据密钥初始化密钥生成器
keygen.init(128, secureRandom);
// 3.产生原始对称密钥
SecretKey original_key = keygen.generateKey();
// 4.获得原始对称密钥的字节数组
byte[] raw = original_key.getEncoded();
// 5.根据字节数组生成AES密钥
SecretKey key = new SecretKeySpec(raw, "AES");
// 6.根据指定算法AES自成密码器
Cipher cipher = Cipher.getInstance("AES");
// 7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
cipher.init(Cipher.DECRYPT_MODE, key);
// 8.将加密并编码后的内容解码成字节数组
byte[] byte_content = new BASE64Decoder().decodeBuffer(content);
//解密
byte[] byte_decode = cipher.doFinal(byte_content);
String AES_decode = new String(byte_decode, "utf-8");
return AES_decode;
}
public static void main(String[] args) throws Exception {
//15:26:49
System.out.println(new Date());
String token = cusTokenCreate("yao.cui",60);
System.out.println("token:"+token);
String username = cusTokenParseUsername(token);
System.out.println("username:"+username);
boolean isExpire = cusIsExpire(token);
System.out.println("isExpire:"+isExpire);
//对字符串jack进行加密
// String encrypt = aesEncrypt("yao.cui"+System.currentTimeMillis());
// System.out.println("---jack的加密结果="+encrypt);
// //对字符串进行解密
// String decrypt = aesDecrypt(encrypt);
// System.out.println("---解密结果="+decrypt);
}
}
jira处理token的工具类
package com.atlassian.jira.security.cuslogin;
import javax.servlet.http.HttpServletRequest;
/**
* @description: 获取 token 的方式
* @author:cuiyao
* @date:2021/10/14
*/
public class CusSSOUtil {
//第三方自定义的jira用户在token中的key
private final String JIRA_AUTH_KEY = "cusAuthToken";
private HttpServletRequest request = null;
/**
* 生成封装request后的SSOToken对象
* @param request
* @return
*/
public static CusSSOUtil getSSOObj(HttpServletRequest request) {
CusSSOUtil cusSSOUtil = new CusSSOUtil();
cusSSOUtil.request = request;
return cusSSOUtil;
}
/**
* 判断是否有效
* @return
*/
public boolean isExpired() {
//从request中获取参数
String cusAuthToken = request.getParameter(JIRA_AUTH_KEY);
//token不为空并且没有过期
if(cusAuthToken!=null&&!CusEncrypTokenUtil.cusIsExpire(cusAuthToken)){
return false;
}
return true;
}
/**
* 返回请求中隐含的用户名。
* @return
*/
public String getLoginId() {
//从request中获取参数
String cusAuthToken = request.getParameter(JIRA_AUTH_KEY);
System.out.println("cusAuthToken----------------->"+cusAuthToken);
if(cusAuthToken!=null){
String username = CusEncrypTokenUtil.cusTokenParseUsername(cusAuthToken);
return username;
}
return "";
}
}
3,代码编译完成后,部署到对应位置。部署位置为com.atlassian.jira.security.cuslogin;cuslogin为自定义包名。修改seraph-config.xml文件。
重启jira
自行测试自己的单点逻辑。
完毕。