集成第三方单点登录JIRA(Colfluence同理)

2023-11-11

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

自行测试自己的单点逻辑。

完毕。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

集成第三方单点登录JIRA(Colfluence同理) 的相关文章

随机推荐

  • 哈希表结构

    1 哈希值 1 概念 是一个十进制的整数 由系统随机给出 就是对象的地址值 但这是一个逻辑地址 是模拟出来的 不是数据实际存储的物理地址 2 获取哈希值 可通过Object类的 hasCode 方法获取哈希值 hasCode 源码如下 pu
  • End-to-End Object Detection with Transformers(论文解析)

    End to End Object Detection with Transformers 摘要 介绍 相关工作 2 1 集合预测 2 2 transformer和并行解码 2 3 目标检测 3 DETR模型 3 1 目标检测集设置预测损失
  • 水库大坝安全监测的主要项目

    由于大坝失事原因是多方面的 其表现形式和可能发生的部位因各坝具体条件而异 因此 在大坝安全监测系统的设计中 应根据坝型 坝体结构和地质条件等 选定观测项目 布设观测仪器 提出设计说明书和设计图纸 设计中考虑埋设或安装仪器的范围包括坝体 坝基
  • 建站之星网站 和服务器,建站之星服务器

    建站之星服务器 内容精选 换一换 云速建站企业版有独立的IP 其他版本没有 其他版本的数据分布在多台服务器上 登录云速建站控制台 在待查看帐号和密码的站点所在行 选择 更多 gt 多用户管理 弹出 多用户管理 对话框 云速建站控制台在 主账
  • 全志F1C100s使用记录:u-boot & linux & rootfs 编译与烧录测试(基于SD卡)

    文章目录 目的 基础准备 硬件准备 开发环境 制作toolchain和rootfs 设置编译工具链 u boot linux编译 u boot boot scr linux 测试程序 文件烧录 分区设置 分块烧录 上电测试 系统镜像 制作镜
  • constexpr的使用

    l 含义 1 常量表达式 指值不会改变并且在编译过程就能得到计算结果的表达式 如 const int max files 20 max files是常量表达式 const int limit max files 1 limit是常量表达式
  • el-table滚动加载、懒加载(自定义指令)

    我们在实际工作中会遇到这样的问题 应客户要求 某一个列表不允许分页 但是不分页的话 如果遇到大量的数据加载 不但后端响应速度变慢 前端的渲染效率也会降低 页面出现明显的卡顿 那如何解决这个问题呢 我们可以用模拟分页 当滚动条滚动到底部时再次
  • 04 C/C++ math库详解

    本文转载链接 https blog csdn net AnthongDai article details 78696573 04 math库的详解 1 cos 函数 cos example include
  • CDC问题的解决方案总结

    CDC 不同时钟之间传数据 问题是ASIC FPGA设计中最头疼的问题 CDC本身又分为同步时钟域和异步时钟域 这里要注意 同步时钟域是指时钟频率和相位具有一定关系的时钟域 并非一定只有频率和相位相同的时钟才是同步时钟域 异步时钟域的两个时
  • Redis之哨兵模式解读

    目录 基本介绍 单哨兵模式 多哨兵模式 哨兵的本质 配置哨兵模式 故障恢复原理 哨兵监控工作流程 哨兵模式缺点 基本介绍 当主服务器宕机后 需要手动把一台从服务器切换为主服务器 这就需要人工干预 费事费力 还会造成一段时间内服务不可用 这不
  • Vue前端路由

    路由概念 SPA 单页面应用程序 中 前端路由 router 就是对应关系 Hash地址与组件间对应关系 工作方式 用户点击页面上的路由链接 导致URL地址栏中Hash值发生变化 前端路由监听到Hash地址变化 前端路由把当前Hash地址对
  • 使用docker安装部署kuboard并导入k8s集群

    1 官网地址 https kuboard cn install v3 install html kuboard v3 x E7 89 88 E6 9C AC E8 AF B4 E6 98 8E 2 找到推荐的安装方式 先点击左上角的安装 3
  • 1秒30000QPS,前后端设计思路

    A1 作者 李道兵 没做过支付 不考虑细节 随便聊聊 1 首先要解决掉数据库的压力 3万qps对应的磁盘 iops 很大 不过现在好的 SSD 能提供很好的 iops 比如这款 ARK Intel SSD DC P3700 Series 8
  • android 4.2版本的sdcard文件目录分析

    1 今天遇到一个问题 修改已经解决 1 首先看看真机测试下的文件结构 2 简单介绍android文件结构的作用 以下是几个重要目录 文件的说明 1 mnt 挂载点目录 2 etc 系统主要配置文件 3 system Android 系统文件
  • ios-设置状态栏颜色(电池颜色)

    iOS 状态栏的两种形式 白底黑字和黑底白字 UIStatusBarStyleLightContent UIStatusBarStyleDefault 设置方法如下 1 Info plist文件添加一行 2 要改变的VC中添加代码 void
  • Android自定义Dialog仿IOS的Dialog

    由于时间原因 没有详细整理 直接拿网上代码 先看看效果 首先 布局文件 activity main xml
  • 抖音小程序如何运营;怎么快速变现赚钱。

    随着LBS 企业号和购物车等能力的上线 抖音早已不是单纯的短视频APP了 抖音正在成为一个融合了线上线下的全新导流平台 而随着抖音小程序的上线 标志着抖音正式完成变现转型 那么抖音小程序有哪些优势 又是怎么貝兼钱的呢 今天 我们就来了解下如
  • 华为OD机试 - 玩牌高手(Java)

    题目描述 给定一个长度为n的整型数组 表示一个选手在n轮内可选择的牌面分数 选手基于规则选牌 请计算所有轮结束后其可以获得的最高总分数 选择规则如下 在每轮里选手可以选择获取该轮牌面 则其总分数加上该轮牌面分数 为其新的总分数 选手也可不选
  • 多传感器融合之雷达图像数据集自动生成 - 20220613

    文章目录 Automatic Radar Camera Dataset Generation for Sensor Fusion Applications 1 Radar Camera Co Calibration 2 ROS pipeli
  • 集成第三方单点登录JIRA(Colfluence同理)

    jira单点登录原理 jira单点登录依赖于seraph插件 在searph的配置文件中引入我们自定义的验证类 类似一个过滤器 jira登录时会解析代码中的逻辑 具体内部实现逻辑自己定义 如header token cookie等等形式 j