应用系统基于OAuth2实现单点登录的解决方案

news2024/11/16 7:49:27

1、OAuth2单点认证原理

基于OAuth2的认证方式包含四种,其中单点登录最常用的是授权码模式,其基本的认证过程如下:

  1. 用户访问业务应用,业务应用进行登录检查;
  2. 业务应用重定向到OAuth2认证服务器,调用获取授权码的认证接口;
  3. OAuth2认证服务负责判断登录状态,如果未登录,则跳转到统一认证登录页面,如果已经登录,则直接到步骤5;
  4. 用户输入用户名、密码进行授权确认;
  5. 授权成功后,OAuth2认证服务携带授权码,重定向到指定的回调地址;
  6. 跳转到业务应用后,业务应用收到授权码,然后携带授权码,换取OAuth2的授权token;
  7. OAuth2认证服务校验授权码成功,返回授权token;
  8. 业务应用携带授权token,调用OAuth2接口获取用户信息;
  9. OAuth2认证服务校验授权token成功,返回用户信息;
  10. 业务系统根据用户信息,创建本系统的登录信息,单点登录成功。

2、启动OAuth2服务

平台提供了一个基于开源项目“spring-security-oauth2-authorization-server”封装的OAuth2服务,工程名是“yuncheng-oauth-boot”。

2.1、配置数据库链接

  • 因OAuth2集成涉及多处配置,平台出厂已经默认配好,建议初学者第一次本地调试时,保持默认端口9000、服务根路径为空不变。后续有多处默认配置OAuth2服务地址是“http://127.0.0.1:9000”。
  • 数据库链接与平台保持一致,读同一个数据库,这样就保证了OAuth2的用户信息与平台维护的用户信息一致。

2.2、配置用户登录标识

大部分情况下,系统使用username,即登录名,作为用户的唯一标识,唯一标识在OAuth2认证服务、平台服务、业务系统之间传递,完成单点登录认证。

如果需要使用手机号或者邮箱作为唯一标识,可以修改后端yml里的配置“yuncheng.loginNameType”,值范围包括:username,即登录名;phone,即手机号;email,即邮箱,不配置时,默认为“username”。

配置修改后,在OAuth2的登录页面,就可以使用对应的唯一标识进行登录了。

注意:如果配置了用户登录标识,那么,平台的yml文件,以及业务系统的集成代码(后面有个示例介绍业务系统如何集成),也需要使用相同的用户登录标识,保持一致。

2.3、启动服务

配置好yml文件的参数,启动OAuth2服务即可。

3、云程平台集成OAuth2服务

3.1、配置前端参数

在“public/config/bootConfig.js”文件中配置“VUE_APP_SSO”为“oauth2”,意为开启OAuth2单点登录功能。

开启后,需同时配置“VUE_APP_OAUTH2_URL”、“VUE_APP_OAUTH2_CLIENT_ID”、“VUE_APP_OAUTH2_SCOPE”的值,如果是本地调试,平台出厂已经默认配好。

参数说明:

  • VUE_APP_OAUTH2_URL:上一步启动的OAuth2的服务地址。
  • VUE_APP_OAUTH2_CLIENT_ID:在OAuth2注册的客户端ID,平台出厂脚本中默认有一条“yuncheng-client”的客户端配置,客户端ID需要结合第4部分的《启用OAuth2客户端注册模块》理解。
  • VUE_APP_OAUTH2_SCOPE:请求认证的权限范围,保持固定参数“openid”不变即可。

3.2、配置后端参数

在yml文件中配置“yuncheng.oauth2-client.default”的值,平台出厂已经默认配好,初学者第一次本地调试时,可以保持不变。

参数说明:

  • provider-uri:上一步启动的OAuth2的服务地址。
  • client-id:在OAuth2注册的客户端ID,平台出厂脚本中默认有一条“yuncheng-client”的客户端配置。
  • client-secret:在OAuth2注册的客户端ID的密钥。
  • redirect-uri:在OAuth2注册的客户端ID的重定向地址,也就是平台前端的访问地址,注意本地调试不要使用“localhost”,应使用“127.0.0.1”。

客户端ID、客户端ID密钥、客户端ID重定向地址,需要结合第4部分的《启用OAuth2客户端注册模块》理解。

3.3、配置用户登录标识

在启动OAuth2服务的时候,配置了用户登录标识,在云程平台的服务中,也需要配置与OAuth2一致的用户登录标识。

3.4、启动平台

配置好参数后,启动平台。

3.5、测试单点效果

访问平台,如果没有登录,系统会跳转到OAuth2的登录页面,账号、密码同平台一致,输入账号密码,登录成功后会跳转到平台首页。

4、启用OAuth2客户端注册模块

4.1、授权客户端注册菜单

平台提供了一个“客户端注册”模块,用于管理OAuth2的客户端数据,该模块的菜单默认是未授权的。

使用管理员账号进入平台控制台->角色授权->后台角色->后台管理员,勾中“配置管理”下的“客户端注册”菜单及其子菜单,点击保存,完成授权。

刷新页面,重新加载授权信息,就可以看到“配置管理”菜单下的“客户端注册”菜单了。

4.2、说明客户端注册参数

模块下有一条默认数据“yuncheng-client”,就是平台的默认配置中使用的OAuth2客户端ID,不要删除这条数据。

如果要变更默认配置,可以通过“修改密钥”按钮,修改客户端ID的密钥,通过“编辑”按钮,修改客户端ID的重定向地址、授权范围等信息,也可以点击“新增”,新注册一个客户端ID。

修改或新增后,再对应修改第二部分平台的默认配置,包括前端配置和后端配置,然后重启平台服务。

需要注意的是,密钥分为“加密”和“明文”两种方式,加密方式不可逆,如果配置加密方式,需要自己提前记录下密钥原文,防止丢失。

密钥前面的字符串“{noop}”等,是加密方式,不需要关心,配置到配置文件中的应该是密钥原文。

新注册一个客户端ID,为下一步的业务系统集成OAuth2提供客户端ID。

参数说明:

  • 客户端ID:client-id,客户端的唯一标识。
  • 密钥类型:分为加密和明文两种方式。
  • 密钥:密钥原文,如果密钥类型选择“加密”,需要提前记录下密钥原文,防止丢失。
  • 客户端名称:客户端中文名称。
  • 认证方式:单点登录保持默认即可。
  • 重定向地址:认证请求后的重定向地址,也就是业务服务的访问地址,与平台或业务服务发送认证请求时携带的redirect_uri参数一致。如果平台或业务服务地址变动了,这里也需要同步修改,业务服务的请求参数也需要同步修改,最终需要保持一致。
  • 授权范围:单点登录保持默认即可。

5、业务系统集成OAuth2

5.1、同步用户

必须完成与平台的用户同步,与OAuth2的用户保持一致,才能使用OAuth2的单点登录功能。

用户的唯一标识是可配置的,可以是username(登录名)、phone(手机号)、email(邮箱)中的一个,这三个关键字段各业务系统应该保持一致。

5.1.1、从平台同步

根据平台提供的接口,业务系统主动从平台拉取数据,完成用户同步。

1、获取所有平台用户

对应接口:List<UserActorImpl> getAllUserList()

接口地址:http://127.0.0.1:30001/api/system/sysOrgConver/getAllUserList (注:接口地址路径以实际为准)

请求类型:GET

参数:无

返回值:HttpResult<List<UserActorImpl>>用户对象集合

返回值示例:

{
  "code": 200,
  "message": "操作成功",
  "success": true,
  "timestamp": 1630752126366,
  "result": [{
    "id": "1373536011387523073",
    "loginName": "admin",
    "name": "管理员",
    "createTime": 1577808000000,
    "deptId": "1373536559281065985",
    "deptCode": " A01A06",
    "orgCode": "A01A06",
    "deptName": "研发部",
    "email": "aaa@163.com",
    "phone": "13801066662",
    "weixin": "xxxxxx"
  }]
}

2、通过用户id获取用户信息

对应接口:UserActorImpl getUserById(String userId)

接口地址:http://127.0.0.1:30001/api/system/sysOrgConver/getUserById?userId=xxxxx (注:接口地址路径以实际为准)

请求类型:GET

参数:userId

返回值:HttpResult<UserActorImpl>(用户对象)

返回值示例:

{
  "code": 200,
  "message": "操作成功",
  "success": true,
  "timestamp": 1630752126366,
  "result": {
    "id": "1373536011387523073",
    "loginName": "admin",
    "name": "管理员",
    "createTime": 1577808000000,
    "deptId": "1373536559281065985",
    "deptCode": " A01A06",
    "orgCode": "A01A06",
    "deptName": "研发部",
    "email": "aaa@163.com",
    "phone": "13801066662",
    "weixin": "xxxxxx"
  }       
}

5.1.2、从钉钉同步

平台完成了钉钉与平台的用户同步功能,可以参考文档:https://yunchengxc.yuque.com/staff-kxgs7i/public/ir11upm4igg0egr1#OLjQt,结合钉钉官方API文档,自行开发钉钉用户同步功能。

5.2、改造代码完成集成

为了便于理解,我们选取一个基于SpringMVC开发的开源工程作为示例,讲解OAuth2的集成过程和原理,如果您的系统不是SpringMVC的技术栈,您也可以参考这个思路,完成自己代码的OAuth2集成。

5.2.1、引入Jar包

集成OAuth2的代码改造中,引入了4个工具Jar包,分别是JSON解析工具fastjson、远程调用工具httpclient和httpcore、jwt解析工具java-jwt。如果您的项目中已经有类似功能的工具包,也可以使用。

5.2.2、改造登录拦截逻辑

下图是SpringMVC的拦截器,主要做了两部分改造。

  • 红框1:定义排除拦截的请求,原逻辑是排除登录请求,改为排除“/oauth2.action”,“/oauth2.action”是业务系统提供给OAuth2的回调地址,也就是客户端配置的重定向地址。下一步有这个服务的具体实现示例。
  • 红框2:如果拦截器判断用户未登录,原逻辑是跳转到本系统的登录页面,改为跳转到OAuth2认证页面。具体逻辑见下图,其中使用的参数的含义后面会说明。这里需要增加一个session属性记录要访问的地址,当认证成功后,系统跳转时,可以从session中拿到该地址,进行跳转,保证业务请求是连贯的。

请求OAuth2需要携带一些参数,可以定义一个参数常量类,也可以使用配置文件配置的方式,本例使用了常量类,定义的参数含义说明如下:

  • clientId: 客户端ID。
  • clientSecret:客户端ID的密钥。
  • redirectUri:客户端定义的重定向地址。
  • providerUri:OAuth2的服务地址
  • tokenUri:换取token请求路径,固定成“/oauth2/token”。
  • authorizeUri:权限认证的请求路径,固定成“/oauth2/authorize”。
  • logoutUri:退出请求路径,固定成“/logout”。
  • loginNameType:单点登录身份标识类型,配置哪个属性标识唯一用户,默认为username,值范围包括: username(登录名)、phone(手机号)、email(邮箱)。

这些参数的值与第3部分注册的客户端对应。

下面是截图涉及的源码,供参考。

/**
 * 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求的URL
        String url = request.getRequestURI();

        // 注释掉原来的不拦截逻辑
//		if (url.indexOf("/login.action") >= 0) {
//			return true;
//		}
        // 不拦截oauth2的回调请求
        if (url.indexOf("/oauth2.action") >= 0) {
            return true;
        }

        // 获取Session
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("USER_SESSION");
        // 判断Session中是否有用户数据,如果有,则返回true,继续向下执行
        if (user != null) {
            return true;
        }

        // 注释掉原来的登录跳转逻辑,改为跳转到oauth2单点登录页面
//		request.setAttribute("msg", "您还没有登录,请先登录!");
//		request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);

        // 记录当前请求地址,oauth2认证通过后调用回调地址时,会使用这个记录,跳转到用户想要访问的页面
        String contextPath = request.getContextPath();
        String nextUrl = url;
        if (contextPath != null && !contextPath.equals("")) {
            nextUrl = nextUrl.substring(request.getContextPath().length());
        }
        session.setAttribute("nextUrl", nextUrl);

        // 跳转到oauth2的登录页面
        String oauth2Url = Oauth2Constant.providerUri + Oauth2Constant.authorizeUri;
        oauth2Url += "?response_type=code";
        oauth2Url += "&client_id=" + Oauth2Constant.clientId;
        oauth2Url += "&scope=openid";
        oauth2Url += "&redirect_uri=" + Oauth2Constant.redirectUri;
        response.sendRedirect(oauth2Url);
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

/**
 * oauth2参数定义
 */
public class Oauth2Constant {

    // 客户端ID
    public static final String clientId = "crm-client";
    // 密钥
    public static final String clientSecret = "crm-client";
    // 重定向地址
    public static final String redirectUri = "http://127.0.0.1:8090/crm/oauth2.action";
    // oauth2服务地址
    public static final String providerUri = "http://127.0.0.1:9000";
    // 换取token请求路径
    public static final String tokenUri = "/oauth2/token";
    // 权限认证的请求路径
    public static final String authorizeUri = "/oauth2/authorize";
    // 退出请求路径
    public static final String logoutUri = "/logout";
    // 单点登录身份标识类型,配置哪个属性标识唯一用户,默认为username
    // 值范围: username:登录名;phone:手机号;email:邮箱
    public static final String loginNameType = "username";
}

5.2.3、集成OAuth2认证服务

OAuth2认证通过后,会携带授权码重定向到业务系统指定服务地址,也就是上一步请求地址的“redirectUri”参数指定的地址,业务系统需要实现该服务,接收授权码,然后携带授权码,换取OAuth2的授权token,完成本系统的登录。对应第1部分原理图的步骤6-步骤10。

  • 红框1:参数校验,如果code为空,则跳转到登录页。一般退出操作导致的重定向是没有code参数的。
  • 红框2:使用code获取OAuth2的userToken,使用jwt解析工具类,从token中解析出用户唯一标识,这里使用了自己封装的一个工具类“Oauth2RestClient”,后面有源码供参考。
  • 红框3:根据配置的用户唯一标识的类型,调用本系统对应的的获取用户接口,获得用户对象。
  • 红框4:如果用户存在,则创建本系统的session,完成登录,如果用户不存在,则跳转OAuth2的退出页面,参数含义可以参考上一部分的参数含义说明。登录成功后的跳转地址,原逻辑是跳转到首页,这里需要从session记录里取到认证前要访问的地址,然后跳转到该地址。

下图是自己封装的一个工具类“Oauth2RestClient”。

下面是截图涉及的源码,供参考。

@Controller
public class Oauth2Controller {

    @Resource
    private UserService userService;

    /**
     * oauth2重定向地址,即回调地址
     */
    @RequestMapping(value = "/oauth2.action")
    public String oauth2(@RequestParam(name = "code", required = false) String code, HttpSession session) throws IOException {
        if (code == null || "".equals(code)) {
            // 如果参数不全,返回到登录页面
            // 推出之后,会跳转回该地址,此时没有code,需要校验
            return "redirect:login.action";
        }
        // 初始化工具类
        Oauth2RestClient oauth2RestClient = new Oauth2RestClient();
        // 换取accessToken
        UserToken userToken = oauth2RestClient.validCode(code);
        // 获取用户名
        String username = oauth2RestClient.getUserName(userToken);

        // 查询用户
        User user = null;
        if ("phone".equals(Oauth2Constant.loginNameType)) {
            user = userService.findUserByPhone(username);
        } else if ("email".equals(Oauth2Constant.loginNameType)) {
            user = userService.findUserByEmail(username);
        } else {
            user = userService.findUserByUserCode(username);
        }

        if (user != null) {
            // 将用户对象添加到Session
            session.setAttribute("USER_SESSION", user);

            // 注释掉原来的跳转到主页面的逻辑
//            return "redirect:customer/list.action";

            // 改为从记录中拿到认证前想要访问的请求地址,进行跳转
            String url = (String) session.getAttribute("nextUrl");
            return "redirect:" + url;
        }
        // 如果登录失败,重定向到oauth2的退出页面
        String oauth2Url = Oauth2Constant.providerUri + Oauth2Constant.logoutUri;
        oauth2Url += "?redirect_uri=" + Oauth2Constant.redirectUri;
        return "redirect:" + oauth2Url;
    }
}

/**
 * oauth2客户端工具类
 */
public class Oauth2RestClient {

    public UserToken validCode(String code) throws IOException {
        String url = Oauth2Constant.providerUri + Oauth2Constant.tokenUri;

        Map<String, String> map = new HashMap<>();
        map.put("grant_type", "authorization_code");
        map.put("client_id", Oauth2Constant.clientId);
        map.put("client_secret", Oauth2Constant.clientSecret);
        map.put("redirect_uri", Oauth2Constant.redirectUri);
        map.put("code", code);
        URI uri = this.createURI(url, map);

        return this.doPost(uri).toJavaObject(UserToken.class);
    }

    public String getUserName(UserToken userToken) {
        DecodedJWT jwt = JWT.decode(userToken.getAccessToken());
        return jwt.getSubject();
    }

    private URI createURI(String url, Map<String, String> map) {
        int loop = 1;
        for (Map.Entry<String, String> entry : map.entrySet()) {
            if (StringUtils.isNotEmpty(entry.getValue())) {
                url += loop == 1 ? "?" : "&";
                url += entry.getKey() + "=" + entry.getValue();
                loop++;
            }
        }
        return URI.create(url);
    }

    private JSONObject doPost(URI uri) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(uri);
        CloseableHttpResponse response = httpClient.execute(httpPost);
        String resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        return JSON.parseObject(resultString);
    }
}

/**
 * oauth2对象
 */
public class UserToken {

    private String accessToken;
    private String refreshToken;
    private String tokenType;
    private String scope;
    private String idToken;

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    public String getTokenType() {
        return tokenType;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public String getScope() {
        return scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }

    public String getIdToken() {
        return idToken;
    }

    public void setIdToken(String idToken) {
        this.idToken = idToken;
    }
}

5.2.4、改造退出功能

原退出逻辑是销毁session,跳转到本系统登录页面,改为销毁session,跳转到OAuth2的退出页面,参数含义可以参考上一部分的参数含义说明。

下面是截图涉及的源码,供参考。

    /**
     * 退出登录
     */
    @RequestMapping(value = "/logout.action")
    public String logout(HttpSession session) {
        // 清除Session
        session.invalidate();

        // 重定向到oauth2的退出页面
        String oauth2Url = Oauth2Constant.providerUri + Oauth2Constant.logoutUri;
        oauth2Url += "?redirect_uri=" + Oauth2Constant.redirectUri;
        return "redirect:" + oauth2Url;

        // 重定向到登录页面的跳转方法
//        return "redirect:login.action";
    }

5.3、测试单点效果

如下图,可以在平台的菜单中,配置一个集成好的业务系统的访问地址。完成菜单授权,刷新页面。

在平台与OAuth2集成、业务系统与OAuth2集成都正确的情况下,通过OAuth2认证页面登录平台后,在菜单中访问第三方业务系统,无需登录即可直接打开,即完成了单点登录集成。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/184801.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

米哈伊年终奖是32万,我的年终奖是彩虹屁!

数据来源沉默王二 | 数据报表小熊绘制 年都过完了&#xff0c;年终奖结果也都出来了&#xff0c;我这个年没有过好&#xff0c;每次想到就难受&#xff0c;在看王二整理出来的年终奖&#xff0c;整个人都不好了。 本次统计基于49条数据的不准确统计&#xff0c;仅抽取部分公司部…

Lesson 4.4 随机梯度下降与小批量梯度下降

文章目录一、损失函数理论基础二、随机梯度下降&#xff08;Stochastic Gradient Descent&#xff09;1. 随机梯度下降计算流程2. 随机梯度下降的算法特性3. 随机梯度下降求解线性回归4. 随机梯度下降算法评价三、小批量梯度下降&#xff08;Mini-batch Gradient Descent&#…

SpringMVC执行流程和原理

1、用户发送出请求到前端控制器DispatcherServlet。 2、DispatcherServlet收到请求调用HandlerMapping&#xff08;处理器映射器&#xff09;。 3、HandlerMapping找到具体的处理器(可查找xml配置或注解配置)&#xff0c;生成处理器对象及处理器拦截器 (如果有)&#xff0c;再…

51单片机学习笔记-3模块化编程

3 模块化编程 [toc] 注&#xff1a;笔记主要参考B站江科大自化协教学视频“51单片机入门教程-2020版 程序全程纯手打 从零开始入门”。 3.1 模块化编程 传统方式编程&#xff1a;所有的函数均放在main.c里&#xff0c;若使用的模块比较多&#xff0c;则一个文件内会有很多的…

1604_linux环境下使用命令行把网页转换成pdf

全部学习汇总&#xff1a; GreyZhang/toolbox: 常用的工具使用查询&#xff0c;非教程&#xff0c;仅作为自我参考&#xff01; (github.com) 使用的工具很容易在彼此之间产生隔离性障碍&#xff0c;比如我最近使用的墨水屏阅读的最合适的文件格式我觉得是pdf&#xff0c;但是我…

路由工具之路由策略router-policy、acl列表与ip-prefix前缀列表的区别、过滤列表filter-policy

3.0.0 路由工具之路由策略router-policy、acl列表与ip-prefix前缀列表的区别、过滤列表filter-policy 目录IP-Prefix前缀列表前缀列表与ACLrouter-policy路由策略应用路由策略过滤路由1、环境介绍2、配置OSPF3、过滤路由&#xff08;1&#xff09;ACL匹配路由方式过滤&#xff…

带死区的PID控制算法及仿真

在计算机控制系统中&#xff0c;某些系统为了避免控制作用过于频繁&#xff0c;消除由于频繁动作所引起的振荡&#xff0c;可采用带死区的PID控制算法&#xff0c;控制算式为&#xff1a;式中&#xff0c;e(k)为位置跟踪偏差;e为一个可调参数&#xff0c;其具体数值可根据实际控…

软件测试职场六年,一个女测试工程师的自我认知

微软自动化测试二年&#xff0c;而后转入阿里做自动化测试三年&#xff0c;经历了入行时的迷茫&#xff0c;而后的笃定&#xff0c;转入移动后对自身定位和价值的怀疑&#xff0c;继而对自动化测试的重新认识&#xff0c;职场六年&#xff0c;终于敢对自动化测试有所论述了。 先…

五个好用的PDF软件推荐!

我们在工作中经常需要选择一款好用的办公软件来转换PDF文件&#xff0c;如果选择的软件不好用&#xff0c;那就回影响我们工作的效率&#xff0c;如果选对了软件&#xff0c;就可以让我们的效率越来越高&#xff0c;足以证明软件的在我们办公中的重要性&#xff0c;下面小编就来…

win远程桌面连接无显示器Ubuntu(22.04.1 LTS)

1、安装ssh server 安装虚拟显示器会导致物理显示器无法使用&#xff0c;为防止虚拟显示出现问题无法连接Ubuntu&#xff0c;在必要时可以使用SSH连接系统。 # Ubuntu Terminal sudo apt-get install openssh-server在Windows中尝试连接 # Windows PowerShell ssh UsernameU…

C语言递归函数(递归调用)详解

一个函数在它的函数体内调用它自身称为递归调用&#xff0c;这种函数称为递归函数。执行递归函数将反复调用其自身&#xff0c;每调用一次就进入新的一层&#xff0c;当最内层的函数执行完毕后&#xff0c;再一层一层地由里到外退出。递归函数不是C语言的专利&#xff0c;Java、…

css背景

1、背景颜色&#xff1a;半透明 <style>div{width: 1000px;height: 100px;/* 背景颜色半透明&#xff0c;其他文字不受影响 */background: rgba(0 ,0 ,0 ,0.3 );}</style> </head> <body><div></div> </body>2、背景图 属性名: ba…

vue前端框架课程笔记(一)

目录初识Vue演示代码模板语法插值语法指令语法使用举例数据绑定两种数据绑定方式示例el和data的两种写法el的两种写法data的两种写法MVVM模型数据代理Object.defineProperty示例参数说明关于getter和setter使用举例泛化的数据代理举例vue中的数据代理原理事件处理指令methods配…

word样式管理:如何对样式进行修改删除

在前面的图文中简单为大家介绍了Word文本样式和表格样式的创建技巧。但对于已经创建好的样式&#xff0c;有时会对样式中的格式进行再次修改&#xff1b;或者是当拿到某个Word文档&#xff0c;它的样式经历多次或多人不断修改&#xff0c;可能导致样式库中的样式混乱不堪&#…

Dockerfile构建Tomcat镜像

Dockerfile构建Tomcat镜像构建步骤1 编写Dockfile文件2 dockcer build构建镜像3 docker run运行容器4 使用容器卷挂载搜索镜像的个数 docker search tomcat |wc -l因此&#xff0c;需要根据自己的项目需求来针对性的构建镜像 构建步骤 1 编写Dockfile文件 看成在编写一个纯净…

React 项目 黑马极客园

React 项目 黑马极客园git地址视频地址项目准备1.项目介绍2.项目搭建3.使用scss预处理器4.配置基础路由5.组件库antd使用6.配置别名路径8.别名路径提示9.安装dev-tools调试工具登录模块1.基本结构搭建2. 创建表单结构3. 表单校验实现4. 获取登录表单数据5. 封装http工具模块6. …

Android Studio Electric Eel | 2022.1.1 版本

前言 各位读者&#xff0c;新年快乐&#xff0c;现在是2023年了&#xff0c;新的一年&#xff0c;我们的开发也会焕然一新&#xff0c;Android开发的工具Android Studio&#xff0c;也能更新到Android Studio Electric Eel | 2022.1.1 版本了&#xff0c;短期内我将使用此版本学…

Delphi 中.deployproj文件结构

Delphi中&#xff0c;如果生成Android或者IOS等APP&#xff0c;需要在Project -> Deployment中按下Deploy图标 &#xff0c;然后就会生成和项目工程文件名称一致&#xff0c;后缀是.deployproj的文件。当然如果直接按下运行图标(RUN)也会自动生成这个文件。一、.deployproj文…

【自然语言处理】情感分析(五):基于 BERT 实现

情感分析&#xff08;五&#xff09;&#xff1a;基于 BERT 实现本文是 情感分析 系列的第 555 篇&#xff0c;也是本系列的收官之作。前四篇分别是&#xff1a; 【自然语言处理】情感分析&#xff08;一&#xff09;&#xff1a;基于 NLTK 的 Naive Bayes 实现【自然语言处理…

简答说明通用智能基础模型的基本处理逻辑

当能量多的时候 激发的是欲望 按照中庸之道 多余的能力要消耗掉 当没有可以查询的模仿行为或者是大量消耗能量行为的时候 就开始记录行为统计为一个发生概率行为表 当拥有了概率记录数据后&#xff0c;按照最大发生概率依次模仿行为 行为得到的反馈 反馈的时间越短 则判断剩余能…