SpringBoot整合SpringSecurity+JWT

news2024/11/30 7:46:49

SpringBoot整合SpringSecurity+JWT

整合SpringSecurity步骤

  1. 编写拦截链配置类,规定security参数
  2. 拦截登录请求的参数,对该用户做身份认证。
  3. 通过登录验证的予以授权,这里根据用户对应的角色作为授权标识。

整合JWT步骤

  1. 编写JWTUtils,包括生成、验证JWT的方法。
  2. 编写登录认证过滤器,生成token,并将token中的payload添加到redis中
  3. 编写路由过滤器,可行的路由则放行
  4. 登录认证后生成token返回response

结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

依赖Jar

<!-- JWT-->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>
<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-core</artifactId>
	<version>9.0.63</version>
</dependency>
<!--Redis-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<version>2.6.8</version>
</dependency>
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>3.7.1</version>
</dependency>
<!--Spring Security-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
	<version>2.6.8</version>
</dependency>
<!--Spring data jpa-->
<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-jpa</artifactId>
	<version>2.6.4</version>
</dependency>
<!-- querydsl -->
<dependency>
	<groupId>com.querydsl</groupId>
	<artifactId>querydsl-jpa</artifactId>
	<version>5.0.0</version>
</dependency>
<!-- Hibernate对jpa的支持包 -->
<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-entitymanager</artifactId>
	<version>5.6.9.Final</version>
</dependency>
<!-- MySQL-->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.13</version>
</dependency>
<!-- Druid-->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-starter</artifactId>
	<version>1.1.16</version>
</dependency>
<!--Spring Boot相关-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>2.6.8</version>
</dependency>
<!--Spring aspect Auditor审计功能需要-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>5.3.20</version>
</dependency>
<!--Hutool 快速开发工具包-->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.8.9</version>
</dependency>

配置文件yml

server:
  port: 8642
spring:
  application:
	name: spring-data-jpa
  datasource:
	driver-class-name: com.mysql.cj.jdbc.Driver
	url: jdbc:mysql://localhost:3306/你的数据库?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
	username: 你的账号
	password: 你的密码
	type: com.alibaba.druid.pool.DruidDataSource
	druid:
	  # 下面为连接池的补充设置,应用到上面所有数据源中
	  # 初始化大小,最小,最大
	  initial-size: 5
	  min-idle: 5
	  max-active: 20
  jpa:
	database: MYSQL
	database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
	show-sql: true
	open-in-view: true
	hibernate:
	  ddl-auto: update
	  naming:
		physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
	properties:
	  hibernate:
		enable_lazy_load_no_trans: true
  redis:
	host: 127.0.0.1
	port: 6379
	password: 你的密码(没有不填)
	lettuce:
	  pool:
		# 最大活动数量
		max-active: 8
		# 当池耗尽时,在引发异常之前,连接分配应该阻塞的最长时间。使用负值可以无限期阻止。
		max-wait: -1
		# 最大闲置时间,单位:s
		max-idle: 500
	  # 超时关闭时间
	  shutdown-timeout: 0

整合SpringSecurity

Spring Security权限配置类

/**
 * @author Evad.Wu
 * @Description SpringSecurity权限配置类
 * @date 2022-06-28
 */
@Configuration
public class EvadSecurityConfig extends WebSecurityConfigurerAdapter {
	private AuthenticationManager authenticationManager;

	@Resource(name = "evadRedisTemplate")
	private RedisTemplate<String, Object> redisTemplate;
	@Resource(name = "userServiceImpl")
	private BaseUserDetailsService userDetailsService;

	/**
	 * 认证
	 *
	 * @param auth 认证管理器建造者
	 */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService);
	}

	/**
	 * 授权
	 *
	 * @param http 安全
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 开启 HttpSecurity 配置
		http.authorizeRequests()
				.antMatchers("/securityController/login").permitAll()
				.antMatchers("/securityController/evadLogin").permitAll()
				.antMatchers("/securityController/evadLogout").permitAll()
				.antMatchers("/securityController/user").permitAll()
				.antMatchers("/securityController/dev").access("hasAnyRole('DEV','MASTER')")
				.antMatchers("/securityController/devAndUser").access("hasAnyRole('MASTER') or (hasRole('DEV') and hasRole('USER'))")
				.antMatchers("/securityController/master").access("hasAnyRole('MASTER')")
				// 用户访问其它URL都必须认证后访问(登录后访问)
				.anyRequest().authenticated()
				// 开启表单登录并配置登录接口
				.and()
				.formLogin().loginProcessingUrl("/login").permitAll()
				.and()
				.logout().logoutUrl("/logout")
				.addLogoutHandler(new EvadLogoutHandler())
				.invalidateHttpSession(true)
				.deleteCookies("JSESSIONID", "XXL_JOB_LOGIN_IDENTITY")
				.clearAuthentication(true)
				.logoutSuccessUrl("/login")
				.permitAll()
				.and().exceptionHandling()
				.accessDeniedHandler((request, response, e) -> {
					request.setAttribute("state", 403);
					request.setAttribute("errMsg", "抱歉,您没有权限访问!");
					request.getRequestDispatcher("/toErrorPage");
				})
				// 添加jwt验证
				.and()
				.addFilter(new JwtLoginFilter(authenticationManager, redisTemplate))
				.addFilter(new JwtValidationFilter(authenticationManager, redisTemplate))
				// 不使用HttpSession
				.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
				.and()
				.cors()
				.and().csrf().disable();
	}

	/**
	 * 加密规则
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	/**
	 * 生成一个认证管理器bean
	 * @return
	 * @throws Exception
	 */
	@Bean(value = "authenticationManager")
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		this.authenticationManager = super.authenticationManagerBean();
		return authenticationManager;
	}
}

校验登录信息(继承UserDetailsService接口),并授权(根据roles)

/**
 * @Description 用户认证信息的顶级接口
 * @author Evad.Wu
 * @date 2022-06-28
 */
public interface BaseUserDetailsService extends UserDetailsService {
}


/**
 * @author Evad.Wu
 * @Description 登录时校验数据库中的密码
 * @date 2022-06-28
 */
@Service
public class UserServiceImpl implements BaseUserDetailsService {
	@Resource(name = "userRepository")
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
		User user = Optional.ofNullable(userRepository.findFirstByUsername(s)).orElseGet(User::new);
		if (user.getPassword().isEmpty()) {
			throw new UsernameNotFoundException("用户不存在!");
		}
		return this.user2UserDetail(user);
	}

	private UserDetail user2UserDetail(User user) {
		UserDetail userDetail = new UserDetail();
		userDetail.setId(user.getId());
		userDetail.setPassword(user.getPassword());
		userDetail.setUserName(user.getUsername());
		userDetail.setUserRoles(this.role2Dto(user.getRoles()));
		Boolean visible = Optional.ofNullable(user.getVisible()).orElse(true);
		userDetail.setEnabled(visible);
		userDetail.setLocked(!visible);
		return userDetail;
	}

	private List<RoleDto> role2Dto(Set<Role> roleList) {
		List<RoleDto> roleDtolist = new ArrayList<>();
		for (Role role : roleList) {
			RoleDto roleDto = new RoleDto();
			roleDto.setId(role.getId());
			roleDto.setRoleName(role.getRoleName());
			roleDto.setRoleCode(role.getRoleCode());
			roleDtolist.add(roleDto);
		}
		return roleDtolist;
	}
}

UserDetail 校验登录信息的对象(实现UserDetails)

/**
 * @author Evad.Wu
 * @Description 用户信息转换类
 * @date 2022-06-28
 */
@Data
@NoArgsConstructor
@JsonIgnoreProperties({"username", "password", "enabled", "accountNonExpired", "accountNonLocked", "credentialsNonExpired", "authorities"})
public class UserDetail implements UserDetails {
	@Serial
	private static final long serialVersionUID = -2028119927623038905L;

	private Long id;

	private String userName;

	private String password;

	private Boolean enabled;

	private Boolean locked;

	private List<RoleDto> userRoles;

	private List<SimpleGrantedAuthority> authorities;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		for (RoleDto role : userRoles) {
			authorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
		}
		return authorities;
	}

	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public String getUsername() {
		return userName;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return !locked;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return enabled;
	}
}

整合JWT

JWTUtils工具类

注意:SIGNATURE是生成token的公钥,当外部token进来时需要公钥解密。

/**
 * @author Evad.Wu
 * @Description JWT 工具类
 * @date 2023-01-15
 */
public class JWTUtils {
	/**
	 * 生成token
	 */
	public static <T extends UserDetails> String createToken(T principal, Long expire) {
		JwtBuilder jwtBuilder = Jwts.builder();
		Map<String, Object> headerParams = new HashMap<>(16);
		headerParams.put("typ", "JWT");
		headerParams.put("alg", SignatureAlgorithm.HS256.getValue());
		Map<String, Object> claims = new HashMap<>(16);
		UserDetail user = (UserDetail) principal;
		claims.put("id", user.getId());
		claims.put("username", principal.getUsername());
		claims.put("role", user.getUserRoles());
		Date exp = new Date(System.currentTimeMillis() + expire);
		claims.put("exp", exp);
		return jwtBuilder
				.setHeader(headerParams)
				.setIssuer(principal.getUsername())
				.setIssuedAt(new Date())
				.setClaims(claims)
				.setExpiration(exp)
				.signWith(SignatureAlgorithm.HS256, EvadSecretConstant.SIGNATURE)
				.compact();
	}

	/**
	 * 解析token
	 *
	 * @param token 令牌
	 * @return 解析结果
	 */
	public static boolean checkToken(String token) {
		JwtParser jwtParser = Jwts.parser();
		jwtParser.setSigningKey(EvadSecretConstant.SIGNATURE);
		try {
			jwtParser.parse(token);
			return true;
		} catch (ExpiredJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * 解析token
	 *
	 * @param token     令牌
	 * @param sercetKey 用户认证秘钥
	 * @return 用户认证信息参数
	 */
	public static Claims verifyToken(String token, String sercetKey) {
		return Jwts.parser()
				.setSigningKey(DatatypeConverter.parseBase64Binary(sercetKey))
				.parseClaimsJws(token).getBody();
	}
}

JWT登录认证过滤器

/**
 * @author Evad.Wu
 * @Description jwt用户信息认证 过滤器
 * @date 2023-01-16
 */
@Slf4j
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
	/**
	 * 获取授权管理
	 */
	private final AuthenticationManager authenticationManager;
	private final RedisTemplate<String, Object> redisTemplate;

	public JwtLoginFilter(AuthenticationManager authenticationManager, RedisTemplate<String, Object> redisTemplate) {
		this.authenticationManager = authenticationManager;
		this.redisTemplate = redisTemplate;
		// 指定一个路由作为登录认证的入口
		super.setFilterProcessesUrl("/securityController/evadLogin");
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		Authentication authentication;
		try {
			BufferedReader br = request.getReader();
			StringBuilder body = new StringBuilder();
			String str;
			while ((str = br.readLine()) != null) {
				body.append(str);
			}
			LoginVo loginVo = JSONObject.parseObject(body.toString(), LoginVo.class);
			//先得到前端传入的账号密码Authentication对象
			UsernamePasswordAuthenticationToken authenticationToken =
					new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
			//AuthenticationManager authentication进行用户认证
			authentication = authenticationManager.authenticate(authenticationToken);
			System.out.println("authencation: " + authentication);
			if (Optional.ofNullable(authentication).isEmpty()) {
				response.setCharacterEncoding("UTF-8");
				response.getWriter().print("登录失败!");
				return null;
			}
			return authentication;
		} catch (IOException e) {
			logger.error(e.getMessage());
		}
		return null;
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
		UserDetail userDetail = (UserDetail) authResult.getPrincipal();
		String jwtToken = JWTUtils.createToken(userDetail, 30 * 60 * 1000L);
		response.addHeader("token", jwtToken);
		//把完整的用户信息存入redis userid作为key
		log.info("token: " + jwtToken);
		redisTemplate.opsForValue().set("login-" + userDetail.getId(), userDetail);
	}

	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
		response.setCharacterEncoding("UTF-8");
		response.getWriter().print("登录失败!");
	}
}

JWT token校验过滤器

/**
 * @author Evad.Wu
 * @Description jwt验证令牌 过滤器
 * @date 2023-01-16
 */
@Slf4j
public class JwtValidationFilter extends BasicAuthenticationFilter {
	private final RedisTemplate<String, Object> redisTemplate;

	public JwtValidationFilter(AuthenticationManager authenticationManager, RedisTemplate<String, Object> redisTemplate) {
		super(authenticationManager);
		this.redisTemplate = redisTemplate;
	}

	/**
	 * 过滤请求验证
	 *
	 * @param request     请求体
	 * @param response    响应体
	 * @param filterChain 请求过滤链
	 * @throws IOException      IO异常
	 * @throws ServletException servlet异常
	 */
	@Override
	protected void doFilterInternal(
			@NonNull HttpServletRequest request,
			@NonNull HttpServletResponse response,
			@NonNull FilterChain filterChain) throws ServletException, IOException {
		String token = request.getHeader("token");
		if (Optional.ofNullable(token).isEmpty()) {
			filterChain.doFilter(request, response);
			return;
		}
		response.setHeader("token", token);
		Claims claims = JWTUtils.verifyToken(token, EvadSecretConstant.SIGNATURE);
		Long id = claims.get("id", Long.class);
		Date exp = claims.getExpiration();
		UserDetail userDetail = (UserDetail) redisTemplate.opsForValue().get("login-" + id);
		System.out.println("过期时间:" + exp);
		log.info("解析到的用户: " + userDetail);
		if (Optional.ofNullable(userDetail).isEmpty()) {
			throw new RuntimeException("用户未登录");
		}
		// 存入SecurityContextHolder, 其他filter会通过这个来获取当前用户信息
		// 获取权限信息封装到authentication中
		UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
				= new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
		SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
		// 放行
		filterChain.doFilter(request, response);
	}
}

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

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

相关文章

精智达在科创板上市:募资约11亿元,深创投等为其股东

7月18日&#xff0c;深圳精智达技术股份有限公司&#xff08;下称“精智达”&#xff0c;SH:688627&#xff09;在上海证券交易所科创板上市。本次上市&#xff0c;精智达的发行价为46.77元/股&#xff0c;发行数量为2350.2939万股&#xff0c;募资总额为10.99亿元&#xff0c;…

封装cpp-httplib成dll包,为老项目提供http网络支持

项目介绍&#xff1a; 公司内某些老的项目不支持https或者http1.1的一些新功能&#xff0c;需要开发对应的SDK供其调用&#xff0c;以便维护老项目。 第一步&#xff1a;下载cpp-httplib 点击这里去下载最新的代码&#xff1a;mirrors / yhirose / cpp-httplib GitCode 直接下…

QT:问题、解决与原因

在这里记录一些自己遇到的在QT开发上面的小问题和tips 目录 QComboBox 设置qss样式不生效qt按钮设置点击释放效果实现效果 QComboBox 设置qss样式不生效 我设置的样式是&#xff1a; box->setStyleSheet("QComboBox {""border: none;""padding:…

适用于 Type-C接口PD应用的智能二极管保护开关

日前&#xff0c;集设计、研发、生产和全球销售一体的著名功率半导体、芯片及数字电源产品供应商Alpha and Omega Semiconductor Limited&#xff08;AOS, 纳斯达克代码:AOSL) 推出一款采用理想二极管运作进行反向电流保护的新型Type-C PD 高压电源输入保护开关。AOZ13984DI-02…

上市公司前端开发规范参考

上市公司前端开发规范参考 命名规则通用约定文件与目录命名HTML命名CSS命名JS命名 代码格式通用约定HTML格式CSS格式JS格式注释 组件组件大小单文件组件容器组件组件使用说明Prop指令缩写组件通讯组件的挂载和销毁按需加载第三方组件库的规定 脚手架使用规范移动端脚手架PC端脚…

Linux下安装Elasticsearch以及ES-head插件

Linux下安装ElasticSearch以及ES-head插件 安装Elasticsearch 由于Elasticsearch客户端版本和ElasticSearch版本有对应关系&#xff0c;所以建议安装之前先考虑安装哪个版本的ElasticSearch。 ElasticSearch、Spring Data Elasticsearch、SpringBoot、Spring版本对应关系 安…

OpenCV for Python 学习第五天:图片属性的获取

上一篇博文当中&#xff0c;我们学习了如何获取图片的通道&#xff0c;我们了解了通道的分离方法split()和通道的组合方法merge()。那么我们今天就来对图片的属性做一个深入的了解。 文章目录 图片属性OpenCV中属性介绍图片属性的获取 图片属性 图片属性是指描述和定义一张图片…

爬虫与反爬虫的攻防对抗

一、爬虫的简介 1 概念 爬虫最早源于搜索引擎&#xff0c;它是一种按照一定的规则&#xff0c;自动从互联网上抓取信息的程序&#xff0c;又被称为爬虫&#xff0c;网络机器人等。按爬虫功能可以分为网络爬虫和接口爬虫&#xff0c;按授权情况可以分为合法爬虫和恶意爬虫。恶…

【NLP】从预训练模型中获取Embedding

从预训练模型中获取Embedding 背景说明下载IMDB数据集进行分词下载并预处理GloVe词嵌入数据构建模型训练模型并可视化结果结果对比其他代码 在NLP领域中&#xff0c;构建大规模的标注数据集非常困难&#xff0c;以至于仅用当前语料无法有效完成特定任务。可以采用迁移学习的方法…

hbuilder创建基于vue2的uniapp小程序项目

参考vant官网&#xff1a;https://vant-contrib.gitee.io/vant/v3/#/zh-CN/quickstart#an-zhuang官网 参考别人博客&#xff1a;https://www.yii666.com/blog/465379.html 1.创建项目 1.1 hbuilder进去右上角点击文件–新建–项目 1.2 vue2项目如下图 2.安装依赖 2.1 2.2…

Linux搭建SVN环境(最新版)

最新版本号(svn-1.14) https://opensource.wandisco.com/centos/7 更新版本库 sudo tee /etc/yum.repos.d/wandisco-svn.repo <<-EOF [WandiscoSVN] nameWandisco SVN Repo baseurlhttp://opensource.wandisco.com/centos/$releasever/svn-1.14/RPMS/$basearch/ enabled…

TypeScript 学习笔记(七):条件类型

条件类型 TS中的条件类型就是在类型中添加条件分支&#xff0c;以支持更加灵活的泛型&#xff0c;满足更多的使用场景。内置条件类型是TS内部封装好的一些类型处理&#xff0c;使用起来更加便利。 一、基本用法 当T类型可以赋值给U类型时&#xff0c;则返回X类型&#xff0c…

一探究竟:人工智能、机器学习、深度学习

一、人工智能 1.1 人工智能是什么&#xff1f; 1956年在美国Dartmounth 大学举办的一场研讨会中提出了人工智能这一概念。人工智能&#xff08;Artificial Intelligence&#xff09;&#xff0c;简称AI&#xff0c;是计算机科学的一个分支&#xff0c;它企图了解智能的实质&am…

拦截器是什么

拦截器 package com.qf.config;import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;public class MyIntercep…

VSCode下载安装(保姆级--一步到胃)

前言 Visual Studio Code&#xff08;简称“VSCode” &#xff09;是Microsoft在2015年4月30日Build开发者大会上正式宣布一个运行于 Mac OS X、Windows和 Linux 之上的&#xff0c;针对于编写现代Web和云应用的跨平台源代码编辑器&#xff0c;可在桌面上运行&#xff0c;并且…

零售行业门店综合管理系统怎么做?店务系统有什么功能?

线下门店则变成了零售行业的重要战场。今时不同往日&#xff0c;现在线下门通常得需要兼多种角色&#xff0c;无论是对于门店员工还是管理者来说经营难度和工作强度都在显著增加。像传统落后的门店管理存在着库存失衡&#xff0c;服务效率低&#xff0c;信息滞后且准确度低等问…

使用IDEA社区版创建SpringBoot项目

文章目录 1.关于IDEA社区版的版本2.下载Spring Boot Helper3.创建项目4.配置Maven国内源4.1找不到settings.xml的情况4.2找得到settings.xml的情况 4.3删除repository目录下的所有文件和目录5.加载项目6.解决org.springframework.boot:spring-boot-starter-parent:pom:2.7.13.R…

学员管理系统——面向对象

文章目录 前言基本思路Student.pymain.pyStudentManage.py菜单 menu()根据菜单实现程序的大概逻辑add_student() 添加学员信息delete_student() 删除学员信息modify_studnet() 修改学员信息search_student() 查找学员信息print_student() 显示所有学员信息save_student() 保存学…

使用qt的webengine让客户端嵌入网页

前提 在windows下&#xff0c;qt下 界面 用qt的界面设计拉上一些东西&#xff0c;一个跑按钮&#xff0c;一个刷新按钮&#xff0c;一个弹出框按钮&#xff0c;地址栏是为了填入新的https地址&#xff0c;一个verticalLayout是为了限定webengine的显示&#xff0c;需要包含 …

UI界面中的图标设计趋势与最佳实践

作为UI设计师&#xff0c;在日常的工作中&#xff0c;避免不了做图标规范。今天跟大家聊一聊&#xff0c;UI设计中的图标设计。 规范的重要性不用多说了&#xff0c;没有规范多个设计师绘制的图标会有很多差异&#xff0c;描边粗细、角度、圆角度等等。今天的文章和大家聊一下…