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

【工作记录】前后端分离场景下CAS单点登录的集成思路与实践@20230926

背景及目的

之前做一个公司项目的时候甲方要求集成他们指定的CAS服务端实现登录,要求不影响原有业务。
CAS服务端提供的文档都是基于前后端不分离的应用,对前后端分离应用没有任何说明,找官方人问也是爱答不理的,近期正好有时间就想着研究下这个集成过程。
于是有了这篇文章,主要为了记录下集成过程和相关配置,方便后续类似的对接。
当然也希望能帮助到需要的朋友。

CAS涉及到的角色

  • 认证服务器
  • 客户端-API
  • 客户端-前台

整体流程梳理:

  • 客户端请求地址A
  • 发现用户没有token,跳转到指定登录页面,地址类似/cas/login?service=xxxxx
  • 请求成功后会回调callbackUrl同时拼接ticket参数,
  • 客户端发起ticket验证请求,v2: /serviceValidate v3: /p3/serviceValidate
  • 验证成功后会回调到callbackUrl中,同时在session中添加指定用户属性
  • 生成token并传递到前端或前端通过请求拿到token
    • 思路一: 客户端拿到用户属性后根据用户生成token返回给前端,可以通过指定url拼接参数给到前端,前端拦截后保存
    • 思路二: 客户端拿到用户属性后重定向到一个约定的前端地址,前端在路由守卫中拦截该路径并在拦截到后发起getToken请求,请求成功后
      保存token等信息到缓存或者cookie即可。

实现过程

前期准备

  1. cas认证服务端部署
  2. 注册CAS协议的应用,需要配置service/callbackUrl/clientName
  3. vue-admin-template模板项目
  4. 简单的springboot项目
  5. 配置几个用于跳转的路由,在路由文件中添加
  {path: '/callback',hidden: true},{path: '/tologin',hidden: true},

这里只是为了路由守卫的拦截,可以不写vue页面,只需要在路由列表中添加定义即可,亲测无误。

改造过程-前端

路由守卫文件 src/permission.js
import {getToken} from '@/utils/auth'; // get token from cookie
import getPageTitle from '@/utils/get-page-title'
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
import router from './router'
import store from './store'NProgress.configure({ showSpinner: false }) // NProgress Configurationconst whiteList = ['/login'] // no redirect whitelist//是否支持cas登录的开关
const enableCasLogin = truerouter.beforeEach(async(to, from, next) => {// start progress barNProgress.start()// set page titledocument.title = getPageTitle(to.meta.title)const hasToken = getToken()//判断是否是去login页面if(to.path === '/tologin') {if(enableCasLogin){//实际访问的cas登录地址window.location.href = `http://localhost:9009/cas/login?service=http://localhost:8989/test1/index&redirect=${to.params.redirect}`} else {next(`/login`)}NProgress.done()} else if (whiteList.indexOf(to.path) !== -1) {next();} else if (to.path === '/callback') {if(!hasToken) {await store.dispatch(`user/resetToken`)}next('/')NProgress.done()} else {if(hasToken) {next()} else {if(to.path === '/') {next(`/tologin`)} else {next(`/tologin?redirect=${to.path}`)}}NProgress.done()}
})router.afterEach(() => {// finish progress barNProgress.done()
})
接口配置 /api/user.js
import request from '@/utils/request'export function login(data) {return request({url: '/auth/login',method: 'post',data})
}export function getCaptcha() {return request({url: '/auth/captcha',method: 'get',})
}export function getCasToken() {return request({url: '/auth/getToken',method: 'get',})
}export function logout() {return request({url: '/vue-admin-template/user/logout',method: 'post'})
}
store配置 src/store/modules/user.js
import { getCasToken, login, logout } from '@/api/user'
import { resetRouter } from '@/router'
import { getToken, removeToken, setToken } from '@/utils/auth'const getDefaultState = () => {return {token: getToken(),name: '',avatar: ''}
}const state = getDefaultState()const mutations = {RESET_STATE: (state) => {Object.assign(state, getDefaultState())},SET_TOKEN: (state, token) => {state.token = token},SET_NAME: (state, name) => {state.name = name},SET_AVATAR: (state, avatar) => {state.avatar = avatar}
}const actions = {// user loginlogin({ commit }, userInfo) {return new Promise((resolve, reject) => {login(userInfo).then(response => {const { data } = responsecommit('SET_TOKEN', data.tokenType + " " + data.token)setToken(data.token)resolve()}).catch(error => {reject(error)})})},// user logoutlogout({ commit, state }) {return new Promise((resolve, reject) => {logout(state.token).then(() => {removeToken() // must remove  token  firstresetRouter()commit('RESET_STATE')resolve()}).catch(error => {reject(error)})})},// remove tokenresetToken({ commit }) {return new Promise(resolve => {removeToken() // must remove  token  firstcommit('RESET_STATE')getCasToken().then(response => {const { data } = responsecommit('SET_TOKEN', data.tokenType + " " + data.token)commit('SET_NAME', data.user.username)setToken(data.token)resolve()}).catch(error => {reject(error)})})}
}export default {namespaced: true,state,mutations,actions
}
配置下接口地址 src/vue.config.js
module.exports = {/*** You will need to set publicPath if you plan to deploy your site under a sub path,* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,* then publicPath should be set to "/bar/".* In most cases please use '/' !!!* Detail: https://cli.vuejs.org/config/#publicpath*/publicPath: '/',outputDir: 'dist',assetsDir: 'static',lintOnSave: process.env.NODE_ENV === 'development',productionSourceMap: false,devServer: {port: port,open: true,overlay: {warnings: false,errors: true},// before: require('./mock/mock-server.js')proxy: {'/dev-api': {target: 'http://localhost:8989',pathRewrite: { '^/dev-api': '' }}}},configureWebpack: {// provide the app's title in webpack's name field, so that// it can be accessed in index.html to inject the correct title.name: name,resolve: {alias: {'@': resolve('src')}}},chainWebpack(config) {// it can improve the speed of the first screen, it is recommended to turn on preloadconfig.plugin('preload').tap(() => [{rel: 'preload',// to ignore runtime.js// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],include: 'initial'}])// when there are many pages, it will cause too many meaningless requestsconfig.plugins.delete('prefetch')// set svg-sprite-loaderconfig.module.rule('svg').exclude.add(resolve('src/icons')).end()config.module.rule('icons').test(/\.svg$/).include.add(resolve('src/icons')).end().use('svg-sprite-loader').loader('svg-sprite-loader').options({symbolId: 'icon-[name]'}).end()config.when(process.env.NODE_ENV !== 'development',config => {config.plugin('ScriptExtHtmlWebpackPlugin').after('html').use('script-ext-html-webpack-plugin', [{// `runtime` must same as runtimeChunk name. default is `runtime`inline: /runtime\..*\.js$/}]).end()config.optimization.splitChunks({chunks: 'all',cacheGroups: {libs: {name: 'chunk-libs',test: /[\\/]node_modules[\\/]/,priority: 10,chunks: 'initial' // only package third parties that are initially dependent},elementUI: {name: 'chunk-elementUI', // split elementUI into a single packagepriority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or apptest: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm},commons: {name: 'chunk-commons',test: resolve('src/components'), // can customize your rulesminChunks: 3, //  minimum common numberpriority: 5,reuseExistingChunk: true}}})// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunkconfig.optimization.runtimeChunk('single')})}
}

改造过程-后端

添加依赖 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.13</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.zjtx.tech</groupId><artifactId>auth-cas-api</artifactId><version>0.0.1-SNAPSHOT</version><name>auth-cas-api</name><description>CAS API</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>net.unicon.cas</groupId><artifactId>cas-client-autoconfig-support</artifactId><version>2.3.0-GA</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

依赖中主要添加的是cas-client-autoconfig-support这个jar包,提供了cas需要的一些配置和拦截器

配置文件添加
server:port: 8989
cas:# 配置实际的cas地址server-url-prefix: http://localhost:9009/cas# 配置实际的cas登录地址server-login-url: http://localhost:9009/cas/loginclient-host-url: http://localhost:8989/# 这里可以选择cas 和 cas3 区别是请求的部分地址不一样,如ticket验证的接口validation-type: cas
#  拦截的URL地址authentication-url-patterns:- /*
spring:jackson:serialization:FAIL_ON_EMPTY_BEANS: false
获取token的controller

src/main/java/com/zjtx/tech/controller/AuthController.java

package com.zjtx.tech.contorller;import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;@RestController
@RequestMapping("/auth")
public class AuthController {@GetMapping("getToken")public ResultBean<TokenUser> getToken(HttpServletRequest request){Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);System.out.println("assertion = " + assertion.getPrincipal().getAttributes());String username = assertion.getPrincipal().getName();System.out.println(username);//这里仅为了演示直接new了一个简单对象返回给前端SimpleUserBean user = new SimpleUserBean("1", username, "123456", "456789");return new ResultBean<>(200, "success", new TokenUser(user, "123456", "Bearer"));}}
回调接口对应的controller

src/main/java/com/zjtx/tech/controller/TestController.java

package com.zjtx.tech.contorller;import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@RestController
public class TestController {@GetMapping("test1/index")public void index(HttpServletRequest request, HttpServletResponse resp) throws IOException {String token = request.getParameter("congress");System.out.println("congress : " + token);Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);System.out.println("assertion = " + assertion.getPrincipal().getAttributes());String username = assertion.getPrincipal().getName();System.out.println(username);resp.sendRedirect("http://localhost:9528/#/callback");}@GetMapping("test1/index1")public String index1(HttpServletRequest request) {String token = request.getParameter("token");System.out.println("token : " + token);Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);String username = assertion.getPrincipal().getName();System.out.println(username);return "test index cas拦截正常,登录账号:" + username;}/*** 不走cas认证,无法获取登录信息** @param request* @return*/@GetMapping("test1/index2")public String index2(HttpServletRequest request) {return "cas 未拦截";}
}

涉及到的简单bean就不在此列举了。

测试过程

  1. 启动前后端及认证服务器项目
  2. 访问前端地址 会跳转到认证服务器定义的loginUrl
  3. 登录完成后会调用http://localhost:9528/#/callback,被路由守卫拦截后进行token的获取和保存
  4. 保存完成后进入首页

总结

本文主要记录了前后端分离场景下集成CAS单点登录的基本流程。
作为记录的同时也希望能帮助到需要的朋友,有任何疑问欢迎留言评论。
创作不易,欢迎一键三连~

相关文章:

【工作记录】前后端分离场景下CAS单点登录的集成思路与实践@20230926

背景及目的 之前做一个公司项目的时候甲方要求集成他们指定的CAS服务端实现登录&#xff0c;要求不影响原有业务。 CAS服务端提供的文档都是基于前后端不分离的应用&#xff0c;对前后端分离应用没有任何说明&#xff0c;找官方人问也是爱答不理的&#xff0c;近期正好有时间就…...

阿里云数据库RDS有哪些?细数关系型数据库大全

阿里云RDS关系型数据库大全&#xff0c;关系型数据库包括MySQL版、PolarDB、PostgreSQL、SQL Server和MariaDB等&#xff0c;NoSQL数据库如Redis、Tair、Lindorm和MongoDB&#xff0c;阿里云百科分享阿里云RDS关系型数据库大全&#xff1a; 目录 阿里云RDS关系型数据库大全 …...

【计算机网络】因特网中的电子邮件

文章目录 简单邮件传送协议SMTP邮件访问协议POP3IMAPHTTP 参考资料 电子邮件为异步通信媒介 因特网电子邮件系统 电子邮件系统的三个构件&#xff1a;用户代理、邮件服务器、邮件发送和读取协议 用户代理 User Agent 即UA 电子邮件客户端软件&#xff0c;用户与电子邮件系统的接…...

【C++11】多线程

多线程创建线程thread提供的成员函数获取线程id的方式线程函数参数的问题线程join场景和detach 互斥量库&#xff08;mutex&#xff09;mutexrecursive_mutexlock_guard 和 unique_lock 原子性操作库&#xff08;atomic&#xff09;条件变量库&#xff08;condition_varuable&a…...

【vue3】shallowReactive与shallowRef;readonly与shallowReadonly;toRaw与markRaw

假期第六篇&#xff0c;对于基础的知识点&#xff0c;我感觉自己还是很薄弱的。 趁着假期&#xff0c;再去复习一遍 1、shallowReactive与shallowRef shallowReactive&#xff1a;只处理对象最外层属性的响应式&#xff08;浅响应式&#xff09; shallowRef&#xff1a;只处理…...

手机建模教程 | 如何从易模App中导出模型?有哪些格式?含贴图吗?

很多小伙伴使用易模App是为了能快速地将已有实物的物体“变成”三维模型后转到自己习惯的3D软件中去编辑&#xff0c;于是&#xff0c;大家都关心模型能否导出&#xff0c;以及导出格式有没有自己想要的&#xff1f; 博雅仔告诉大家&#xff0c;当然可以导出&#xff01; 在导出…...

数据分析技能点-机器学习优化思想

优化思想,这个听起来极其专业和高端的词汇,其实它无处不在,悄无声息地影响着我们的生活和决策。从寻找最快的上班路线,到决定如何配置投资组合,优化思想都是一个不可或缺的元素。而在机器学习领域,优化思想更是扮演着至关重要的角色。 文章目录 优化的基础优化问题与实际…...

应用架构的演进:亚马逊的微服务实践

当你在亚马逊上购物时,或许不会想到,你看到的这个购物网站,其背后技术架构经历了什么样的变迁与升级。 还记得上世纪 90 年代,那个只卖书的网上书店吗?那时的亚马逊,不过是一个架构简单的网站,所有的功能都堆积在一个庞大的软件堡垒里。随着更多业务的增加、更新和迭代,这个软…...

leetCode 55.跳跃游戏 贪心算法

给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输入…...

CF505B Mr. Kitayuta‘s Colorful Graph

Mr. Kitayuta’s Colorful Graph 题面翻译 给出一个 n n n 个点&#xff0c; m m m 条边的无向图&#xff0c;每条边上是有颜色的。有 q q q 组询问 对于第 i i i 组询问&#xff0c;给出点对 u i , v i u_i,v_i ui​,vi​。求有多少种颜色 c c c 满足&#xff1a;有至…...

c#设计模式-结构型模式 之 组合模式

&#x1f680;简介 组合模式又名部分整体模式&#xff0c;是一种 结构型设计模式 &#xff0c;是用于把一组相似的对象当作一个 单一的对象 。组合模式 依据树形结构来组合对象 &#xff0c;用来表示部分以及整体层&#xff0c;它可以让你将对象组合成树形结构&#xff0c;并且…...

【Rust日报】2023-09-30 使用Rust做web抓取

CockroachDB 用rust重新实现 嘿&#xff0c;伙计们&#xff0c;我在 Rust 中实现了一个分布式 SQL 数据库。它就像 CockroachDB 和谷歌Google Spanner。告诉我你的想法。 注意: 这不是生产级别的数据库&#xff0c;这是一个以学习为目的的项目。有许多特性&#xff0c;但是缺少…...

【密评】商用密码应用安全性评估从业人员考核题库(三)

商用密码应用安全性评估从业人员考核题库&#xff08;三&#xff09; 国密局给的参考题库5000道只是基础题&#xff0c;后续更新完5000还会继续更其他高质量题库&#xff0c;持续学习&#xff0c;共同进步。 501 多项选择题 《个人信息保护法》要求个人信息处理者应当采取哪些…...

MySQL进阶_查询优化和索引优化

文章目录 第一节、索引失效案例1.1 数据准备1.2 全值匹配我最爱1.3 最佳左前缀法则 第一节、索引失效案例 可以从以下维度对数据库进行优化&#xff1a; 索引失效、没有充分利用到索引–索引建立关联查询太多JOIN (设计缺陷或不得已的需求)–SQL优化服务器调优及各个参数设置…...

Hadoop2复安装过程详细步骤

1、在vmware中更改了虚拟机的网络类型&#xff0c;--->NAT方式&#xff0c;&#xff08;虚拟交换机的ip可以从vmvare的edit-->vertual network editor看到&#xff09; 2、根据这个交换机&#xff08;网关&#xff09;的地址&#xff0c;来设置我们的客户端windows7的ip&…...

【Java-LangChain:面向开发者的提示工程-7】文本扩展

第七章 文本扩展 扩展是将短文本&#xff08;例如一组说明或主题列表&#xff09;输入到大型语言模型中&#xff0c;让模型生成更长的文本&#xff08;例如基于某个主题的电子邮件或论文&#xff09;。这种应用是一把双刃剑&#xff0c;好处例如将大型语言模型用作头脑风暴的伙…...

竞赛 基于设深度学习的人脸性别年龄识别系统

文章目录 0 前言1 课题描述2 实现效果3 算法实现原理3.1 数据集3.2 深度学习识别算法3.3 特征提取主干网络3.4 总体实现流程 4 具体实现4.1 预训练数据格式4.2 部分实现代码 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于深度学习机器视觉的…...

从技能需求到就业前景,了解前端和后端开发的优缺点和个人选择

文章目录 每日一句正能量一、引言前端开发后端开发 二、两者的对比分析三、技能转换和跨领域工作四&#xff1a;介绍全栈开发后记 每日一句正能量 命运决定的不是你的人生&#xff0c;能决定你人生的只有自己。 一、引言 前端和后端是Web开发中两个不可或缺的领域。前端开发主…...

Flutter笔记:AnimationMean、AnimationMax 和 AnimationMin 三个类的用法

Flutter笔记 AnimationMean、AnimationMax 和 AnimationMin三个类的用法 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/…...

华为云云耀云服务器L实例评测|云耀云服务器L实例部署Gogs服务器

华为云云耀云服务器L实例评测&#xff5c;云耀云服务器L实例部署Gogs服务器 一、云耀云服务器L实例介绍1.1 云耀云服务器L实例简介1.2 云耀云服务器L实例特点 二、Gogs介绍2.1 Gogs简介2.2 Gogs特点 三、本次实践介绍3.1 本次实践简介3.2 本次环境规划 四、远程登录华为云云耀云…...

操作系统--分页存储管理

一、概念介绍 分页存储&#xff1a;一是分内存地址&#xff0c;二是分逻辑地址。 1.分内存地址 将内存空间分为一个个大小相等的分区。比如&#xff0c;每个分区4KB。 每个分区就是一个“页框”&#xff0c;每个页框有个编号&#xff0c;即“页框号”&#xff0c;“页框号”…...

【算法练习Day10】有效的括号删除字符串中的所有相邻重复项逆波兰表达式求值

​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;练题 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 有效的括号删除字符串中的所…...

10.1 校招 实习 内推 面经

绿泡*泡&#xff1a; neituijunsir 交流裙 &#xff0c;内推/实习/校招汇总表格 1、自动驾驶一周资讯 - 苹果汽车项目泡汤&#xff1f;纵目科技IPO终止&#xff0c;腾讯与岚图汽车合作升级&#xff0c;158亿元现金收购比亚迪“史上最大”并购案 自动驾驶一周资讯 - 苹果汽车…...

Redis中Set类型的操作

Set的结构与list相似&#xff0c;但底层存储结构是hashtable&#xff0c;因此它的值是唯一的&#xff0c;同时添加的顺序与保存的顺序并不一致。每一个Set类型的key中可以存储2^32-1个元素。 一、应用场景 1、保存用户的收藏 在小说网站中保存用户的收藏&#xff0c;收藏 的小…...

正确完成实时 AI

发表于 构建真实世界的实时 AI 一、说明 我们知道&#xff0c;当前的AI进展是扎根于历史数据&#xff0c;这就造成一个事实&#xff0c;模型总是赶不上实时进展&#xff0c;模型的洞察力不够尖锐&#xff0c;或者&#xff0c;时间损失等&#xff0c;本篇对这一系列AI的短板展开…...

深度学习笔记之线性代数

深度学习笔记之线性代数 一、向量 在数学表示法中&#xff0c;向量通常记为粗体小写的符号&#xff08;例如&#xff0c;x&#xff0c;y&#xff0c;z&#xff09;当向量表示数据集中的样本时&#xff0c;它们的值具有一定的现实意义。例如研究医院患者可能面临的心脏病发作风…...

Python与Scrapy:构建强大的网络爬虫

网络爬虫是一种用于自动化获取互联网信息的工具&#xff0c;在数据采集和处理方面具有重要的作用。Python语言和Scrapy框架是构建强大网络爬虫的理想选择。本文将分享使用Python和Scrapy构建强大的网络爬虫的方法和技巧&#xff0c;帮助您快速入门并实现实际操作价值。 一、Pyt…...

kind 安装 k8s 集群

在某些时候可能需要快速的部署一个k8s集群用于测试&#xff0c;不想部署复杂的k8s集群环境&#xff0c;这个时候我们就可以使用kind来部署一个k8s集群了&#xff0c;下面是使用kind部署的过程 一、安装单节点集群 1、下载kind二进制文件 [rootlocalhost knid]# curl -Lo ./kin…...

Leetcode 2871. Split Array Into Maximum Number of Subarrays

Leetcode 2871. Split Array Into Maximum Number of Subarrays 1. 解题思路2. 代码实现 题目链接&#xff1a;2871. Split Array Into Maximum Number of Subarrays 1. 解题思路 这一题实现上其实还是比较简单的&#xff0c;就是一个贪婪算法&#xff0c;主要就是思路上需要…...

Java基础---第十三篇

系列文章目录 文章目录 系列文章目录一、有数组了为什么还要搞个 ArrayList 呢?二、说说什么是 fail-fast?三、说说Hashtable 与 HashMap 的区别一、有数组了为什么还要搞个 ArrayList 呢? 通常我们在使用的时候,如果在不明确要插入多少数据的情况下,普通数组就很尴尬了,…...