SpringSecurity(SpringBoot2.X版本实现)

资料来源于 SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权

侵权删

目录

介绍

快速开始

认证

认证流程

登录校验流程

SpringSecurity完整流程

认证流程详解

代码实现

准备工作

mysql

mybatis-plus

redis

统一返回类

核心代码

密码加密存储

自定义登录接口

认证过滤器

退出登录

测试

授权

权限系统的作用

授权基本流程

授权实现

限制访问资源所需权限

封装权限信息

从数据库查询权限信息

RBAC权限模型

自定义失败处理

跨域

更多细节

其它权限校验方法

hasAuthority方法执行的源码

其他权限校验方法

自定义权限校验方法

基于配置的权限控制

CSRF

认证成功处理器

认证失败处理器

登出成功处理器



介绍

Spring Security 是一个功能强大且灵活的身份验证和访问控制框架,用于保护基于 Java 的企业应用程序。它提供了全面的安全解决方案,包括身份认证、授权、攻击防范、会话管理等功能,可以帮助开发者构建安全可靠的应用程序。

以下是 Spring Security 的一些主要特性和用途:

  1. 身份认证(Authentication):Spring Security 支持多种身份认证方式,包括基于表单、HTTP 基本认证、HTTP Digest 认证、OpenID、OAuth 等。开发者可以根据应用程序的需求选择合适的认证方式。

  2. 授权(Authorization):Spring Security 提供了灵活的授权机制,可以基于角色(Role)、权限(Permission)、表达式(Expression)等对用户进行访问控制。开发者可以根据应用程序的权限模型来定义授权规则,确保用户只能访问其具有权限的资源。

  3. 攻击防范:Spring Security 集成了各种安全防护机制,包括防止 CSRF(跨站请求伪造)、点击劫持、会话固定攻击、SQL 注入、XSS(跨站脚本攻击)等常见攻击。开发者可以通过配置简单的安全策略来保护应用程序免受这些攻击。

  4. 会话管理:Spring Security 提供了对用户会话的管理功能,包括基于内存、基于数据库、基于集群的会话管理等。开发者可以灵活地配置会话管理策略,确保用户会话的安全性和可靠性。

  5. 记住我(Remember Me):Spring Security 支持“记住我”功能,允许用户在下次访问应用程序时自动登录,而无需重新输入用户名和密码。

  6. 集成性:Spring Security 可以与 Spring 框架及其他常见的 Java Web 框架(如 Spring Boot、Spring MVC、Spring WebFlux)无缝集成,为开发者提供方便易用的安全解决方案。

总之,Spring Security 是一个功能强大、灵活且易于使用的安全框架,为 Java 开发者提供了全面的安全解决方案,帮助他们构建安全可靠的企业级应用程序。

而认证和授权也是SpringSecurity作为安全框架的核心功能,本文主要介绍SpringSecurity中认证和授权的基本操作。

快速开始

我们先简单实现一个基于SpringBoot2框架的SpringSecurity项目。

第1步:先在springboot项目的pom文件夹中加入SpringSecurity的依赖

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>

第2步:创建一个后台接口

@RestController
@RequestMapping("/hello")
public class TestController {@GetMappingpublic String hello(){System.out.println("hello");return "hello";}
}

到这里,SpringSecurity的简单实现就完成了,是不是特别简单

然后我们尝试去访问后台的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台,我们必须登陆之后才能对接口进行访问。

在浏览器输入:localhost:7000/hello,如果没有登录过,就会被SpringSecurity拦截跳到登录页面

 用户名默认为user,密码在控制台中给出

输入用户名密码之后才能正确访问我们自己定义的接口。

认证

认证流程

登录校验流程

SpringSecurity完整流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor:负责权限校验的过滤器。

我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

认证流程详解

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法authenticate()

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法,SpringSecurity初始是从内存中获取的,这个接口后面需要我们实现去自己的数据库中获取。

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中,这个也是需要我们自己实现的。

代码实现

准备工作

mysql

从之前的分析我们可以知道,我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。

创建一个数据库表

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (`id` int(11) NOT NULL COMMENT '用户Id',`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户手机号码',`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户邮箱',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

在pom文件中引入对应依赖

        <dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>

在配置文件中配置数据库信息

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://xxxxxx:3306/testusername: xxxxpassword: xxxx

定义实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User implements Serializable {private Integer id;private String name;private String password;private String phone;private String email;
}
mybatis-plus

导入依赖

<properties><mybatis-plus.version>3.5.3.1</mybatis-plus.version>
</properties>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version>
</dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>${mybatis-plus.version}</version>
</dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-extension</artifactId><version>${mybatis-plus.version}</version>
</dependency>

使用

定义mapper

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

定义service

public interface UserService extends IService<User> {}

定义serviceImpl

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{}
redis

导入依赖

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

在配置文件中配置redis连接信息

spring:redis:host: 47.115.217.159 #默认端口是6379就可以不写

重写redis的序列化器

//redis的序列化器
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class<?> clazz){return TypeFactory.defaultInstance().constructType(clazz);}
}

 形式参数可能会爆红,不用管他,可以运行

@Configuration
public class RedisConfig {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}
}
统一返回类
public class R<T> {/*** 状态码*/private Integer code;/*** 提示信息,如果有错误时,前端可以获取该字段进行提示*/private String msg;/*** 查询到的结果数据,*/private T data;public R(Integer code, String msg) {this.code = code;this.msg = msg;}public R(Integer code, T data) {this.code = code;this.data = data;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}public R(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}
}



核心代码

创建一个类实现UserDetailsService接口,重写其中的方法。根据用户名从数据库中查询用户信息

@Service
public class UserDetailServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//获取用户信息User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getName, username));if(Objects.isNull(user)){throw new RuntimeException("用户不存在");}//权限信息//封装成UserDetails对象返回LoginUser loginUser = new LoginUser(user);return loginUser;}
}

因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class LoginUser implements UserDetails {private User user;//返回用户的权限信息@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}//返回用户的密码信息@Overridepublic String getPassword() {return user.getPassword();}//返回用户的用户名@Overridepublic String getUsername() {return user.getName();}//用户的帐户是否未过期@Overridepublic boolean isAccountNonExpired() {return true;}//用户的帐户是否未被锁定@Overridepublic boolean isAccountNonLocked() {return true;}//用户的凭据(密码)是否未过期@Overridepublic boolean isCredentialsNonExpired() {return true;}//用户是否启用@Overridepublic boolean isEnabled() {return true;}
}

注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如

这样登陆的时候就可以用“李四”作为用户名,1234作为密码来登陆了。

不过我们往数据库中存储密码肯定不会以明文形式存储的,后面我们会使用MD5加密的方式,也就不需要{noop}了。

密码加密存储

实际项目中我们不会把密码明文存储在数据库中。

默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。

我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。

我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

我们可以定义一个SpringSecurity的配置类,给容器中注入一个PasswordEncoder 的组件即可

@Configuration
public class SecurityConfig {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}

自定义登录接口

接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。(形式参数可能会爆红,不用管他,运行没问题的)

@Configuration
public class SecurityConfig {@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}}

认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

封装对应的JwtUtils

导入依赖

        <dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version></dependency>

编写配置类

public class JwtUtil {private static final String KEY = "随便写";//接收业务数据,生成token并返回public static String genToken(Map<String, Object> claims) {return JWT.create().withClaim("claims", claims).withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)).sign(Algorithm.HMAC256(KEY));}//接收token,验证token,并返回业务数据public static Map<String, Object> parseToken(String token) {return JWT.require(Algorithm.HMAC256(KEY)).build().verify(token).getClaim("claims").asMap();}}

编写用户登录的controller类

@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;/*** 用户登录* @param user* @return*/@PostMapping("/login")public R UserLogin(@RequestBody User user){return userService.userLogin(user);}
}

service实现类

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic R userLogin(User user) {UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getName(),user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);if(Objects.isNull(authenticate)){throw new RuntimeException("用户名或密码错误");}//使用userId生成tokenLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String id = loginUser.getUser().getId().toString();String token = JwtUtil.genToken(new HashMap<>() {{put("id", id);}});//将用户信息存入redisredisTemplate.opsForValue().set("loginUser:"+id,loginUser);//将携带token的值返回给前端Map<String,String> map=new HashMap<>(){{put("token",token);}};return new R(200,"登录成功",map);}
}

认证过滤器

我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。

使用userid去redis中获取对应的LoginUser对象。

然后封装Authentication对象存入SecurityContextHolder

/*** 记得在security的配置文件中将这个过滤器配置在UsernamePasswordAuthenticationFilter之前执行*/
@Configuration
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//先从请求头中获取tokenString token = request.getHeader("token");/**如果token不存在,说明用户发请求时没有携带token,那么可能就是用户没有登录,过滤器就放行,* 交给后面的FilterSecurityInterceptor进行处理**/if (StringUtils.isEmpty(token)) {filterChain.doFilter(request, response);//这里一定要return,因为在返回结果的时候也会来到这里,如果没有return,就会继续执行下面的代码return;}//解析token,得到token信息,去redis中获取信息Map<String, Object> map = JwtUtil.parseToken(token);String id = map.get("id").toString();LoginUser loginUser;try {loginUser = (LoginUser) redisTemplate.opsForValue().get("loginUser:" + id);if(Objects.isNull(loginUser)){//运行到这里,说明redis中不存在对应的用户信息,抛出一个错误throw new RuntimeException("用户不存在");}} catch (Exception e) {//运行到这里,说明redis中不存在对应的用户信息,抛出一个错误throw new RuntimeException("用户不存在");}//将用户信息存入SecurityContextHolder/*** TODO 获取权限信息封装到Authentication中* 如果过滤器执行到这里,说明前端携带了token,并且redis中也存在对应的信息,说明这个用户是已经登录过的,所以是已认证状态* 如果是已认证状态,UsernamePasswordAuthenticationToken(loginUser,null,null),这里第一个参数是用户信息,* 第三个参数是权限信息*/UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}

在SpringSecurity的过滤器链中将我们自己写的过滤器加进去,并且要在用户名密码校验器之前。

在SpringSecurity的配置文件中配置

    @Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/user/login").anonymous()//未登录才能访问这个请求.anyRequest().authenticated();/*** 将JwtAuthenticationTokenFilter过滤器配置到security过滤器链之中,并且在UsernamePasswordAuthenticationFilter* 之前执行*/http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}

退出登录

对于退出登录功能,我们只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic R userLogout() {//再security的context中获取当前登录的用户Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();//删除redis中对应的数据redisTemplate.delete("loginUser:"+loginUser.getUser().getId());return new R(200,"退出成功");}
}

测试

我这里准备了三个请求

登录请求

hello请求

退出登录请求

首先在没有登录的情况下发送hello请求

发送登录请求

将token加入hello请求的header中,再次发送

将token加入到logout请求的header中,发送请求

退出之后再次写到token发送hello请求

ok,到这里,认证流程就走完了。

授权

权限系统的作用

比如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。

总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。

我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。

所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。

授权基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication,然后设置我们的资源所需要的权限即可。

授权实现

限制访问资源所需权限

SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。

但是要使用它我们需要先开启相关配置。

@SpringBootApplication
//开启限制访问资源权限
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {public static void main(String[] args) {ConfigurableApplicationContext run = SpringApplication.run(Application.class, args);System.out.println(run);}}

然后就可以使用对应的注解。@PreAuthorize

    @GetMapping//用户具有 admin 权限才能访问这个接口@PreAuthorize("hasAuthority('admin')")public String hello(){System.out.println("hello");return "hello";}

封装权限信息

我们前面在写UserDetailsServiceImpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。

我们先直接把权限信息写死封装到UserDetails中进行测试。

我们之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改

LoginUser.java中添加或修改如下代码

private List<String> permission;/*** 加上这个注解表示authorities这个属性不会进行序列化,* 因为我们后面需要将loginUser的对象序列化之后存储到redis中,* 但是对于GrantedAuthority类型的对象进行序列化会报错,所以排除这个属性*/@JSONField(serialize = false)private List<GrantedAuthority> authorities;public LoginUser(User user, List<String> permission) {this.user = user;this.permission = permission;}//返回用户的权限信息@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {//ctr+alt ---查询一个接口的事项类/*** 因为每一次框架内部调用这个方法都要进行转化,不如将authorities变成一个成员变量,并且加一个判断,* 不为空的时候直接返回即可,为空时再进行转化*/if(!Objects.isNull(authorities)){return authorities;}/*** 把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中*/authorities = permission.stream().map((item) -> {GrantedAuthority authority = new SimpleGrantedAuthority(item);return authority;}).collect(Collectors.toList());return authorities;}

在UserServiceDetailService.java中修改成如下代码

@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);User user = userMapper.selectOne(wrapper);if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}//根据用户查询权限信息 添加到LoginUser中,这里先写死List<String> list = new ArrayList<>(Arrays.asList("test"));return new LoginUser(user,list);}
}

然后进行测试,第一次测试访问hello接口肯定是访问不到的,因为我们给用户设置的权限是test,hello接口需要admin权限才能请求。

把用户的权限设置成admin,就可以访问成功

@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);User user = userMapper.selectOne(wrapper);if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}//根据用户查询权限信息 添加到LoginUser中,这里先写死List<String> list = new ArrayList<>(Arrays.asList("admin"));return new LoginUser(user,list);}
}

在实际应用中,用户的权限应该从数据库中获取,所以就引出了RBAC模型。

从数据库查询权限信息

RBAC权限模型

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

在数据库中定义这些表。(学习阶段的话也不用把表属性设置的太复杂,保留主要字段就行,下面的就比较复杂)

/*Table structure for table `sys_menu` */DROP TABLE IF EXISTS `sys_menu`;CREATE TABLE `sys_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',`path` varchar(200) DEFAULT NULL COMMENT '路由地址',`component` varchar(255) DEFAULT NULL COMMENT '组件路径',`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',`create_by` bigint(20) DEFAULT NULL,`create_time` datetime DEFAULT NULL,`update_by` bigint(20) DEFAULT NULL,`update_time` datetime DEFAULT NULL,`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';/*Table structure for table `sys_role` */DROP TABLE IF EXISTS `sys_role`;CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(128) DEFAULT NULL,`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',`create_by` bigint(200) DEFAULT NULL,`create_time` datetime DEFAULT NULL,`update_by` bigint(200) DEFAULT NULL,`update_time` datetime DEFAULT NULL,`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';/*Table structure for table `sys_role_menu` */DROP TABLE IF EXISTS `sys_role_menu`;CREATE TABLE `sys_role_menu` (`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;/*Table structure for table `sys_user` */DROP TABLE IF EXISTS `sys_user`;CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` varchar(64) DEFAULT NULL COMMENT '邮箱',`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` varchar(128) DEFAULT NULL COMMENT '头像',`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';/*Table structure for table `sys_user_role` */DROP TABLE IF EXISTS `sys_user_role`;CREATE TABLE `sys_user_role` (`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

根据用户ID查询用户权限的sql语句

SELECT DISTINCT m.`perms`
FROMsys_user_role urLEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHEREuser_id = 2AND r.`status` = 0AND m.`status` = 0

然后在代码中添加Menu对应的实体类以及Mapper文件,在UserDetailServiceImpl.java中把我们定死的那个权限修改成从数据库中查询即可,其他地方不需要改变。

Menu实体类(我在上面的sql建表语句中删除了一些字段,所以这里的实体类中的属性跟上面的sql建表语句中的对不上)

@TableName("sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu implements Serializable {private static final long serialVersionUID = -54979041104113736L;@TableIdprivate Integer id;private String menuName;private String status;private String perms;private String delFlag;private String remark;}

MenuMapper.java

@Mapper
public interface MenuMapper extends BaseMapper<Menu> {List<String> selectPermsByUserId(Long id);
}

MenuMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.itheima.mapper.MenuMapper"><select id="selectPermsByUserId" resultType="java.lang.String">SELECTDISTINCT m.`perms`FROMsys_user_role urLEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`WHEREuser_id = #{userid}AND r.`status` = 0AND m.`status` = 0</select></mapper>

UserDetalisService.java

@Service
public class UserDetailServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate MenuMapper menuMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//获取用户信息User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getName, username));if(Objects.isNull(user)){throw new RuntimeException("用户不存在");}//权限信息List<String> list = menuMapper.selectPermsByUserId(user.getId().longValue());//封装成UserDetails对象返回LoginUser loginUser = new LoginUser(user,list);return loginUser;}
}

自定义失败处理

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

实现方式如下:

先引入一个工具类,用来渲染响应的数据格式

public class WebUtils
{/*** 将字符串渲染到客户端** @param response 渲染对象* @param string 待渲染的字符串* @return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;}
}
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {R result = new R(HttpStatus.FORBIDDEN.value(), "权限不足");String json = JSON.toJSONString(result);WebUtils.renderString(response,json);}
}

然后自定义实现类

//自定义授权失败处理
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {R result = new R(HttpStatus.FORBIDDEN.value(), "权限不足");String json = JSON.toJSONString(result);WebUtils.renderString(response,json);}
}
//自定义失败处理
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");String json = JSON.toJSONString(result);WebUtils.renderString(response,json);}
}

然后将这两个自定义实现类注入到Security的配置文件中

    @Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {//其他配置............................................//添加自定义异常处理器http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);return http.build();}

跨域

浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。

所以我们就要处理一下,让前端能进行跨域请求。

对于springboot的跨域处理,大家可以去看我的这一篇文章

springboot中解决CORS的几种方式

这里可以使用这一种方式

@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}
}

然后再Security的配置文件中开启CORS

@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {//其他内容...............................http.cors();return http.build();}

更多细节

其它权限校验方法

我们前面都是使用@PreAuthorize注解,然后在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。

这里我们先不急着去介绍这些方法,我们先去理解hasAuthority的原理,然后再去学习其他方法你就更容易理解,而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。

hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority,大家只要断点调试既可知道它内部的校验原理。

它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。

hasAuthority方法执行的源码

首先shift+shift点进搜索框,输入SecurityExpressionRoot,进入这个类

找到hasAuthority方法,打上端点,启动Security项目,进行调试

hasAuthority方法中又调用了一个方法,这个方法就在他下面 hasAnyAuthority

hasAnyAuthority方法调用了hasAnyAuthorName这个方法,我们继续追踪,看看这个方法里面的逻辑

debug进去之后,我们可以看到这个方法接收一个prefix,以及多个字符串的roles,我们在对应的接口方法中只校验一个,这里自然也只有一个

然后这个方法会调用getAuthoritySet这个方法,顾名思义,这个方法就是去得到当前登录用户的权限,继续追踪进这个方法

可以看到,这里使用了authentication.getAuthorities()来得到登录用户的权限信息。

大家还记不记得我们前面在我们自定义的一个过滤器中定义了如下代码

再结合LoginUser中的这个方法

大家就是不是就知道是怎么获取用户的权限信息的了。

继续debug

得到了用户的权限信息

这里的权限信息是我们从数据库中查询得到了,不要因为这里也是admin就跟上面接口的搞混了,这两个不是同一个对象哈。

继续debug

这一步将上面的用户权限信息封装为一个Set集合

继续debug

回到前面的这个hasAnyAuthorityName

这里就来判断用户的权限Set集合中是否包含接口上定义的权限,如果包含,返回true

这里肯定是包含的,所以就直接返回true,校验就通过了,当前用户就可以访问这个接口,后面debug过程的就是一些处理过程,与Security业务没什么关系,这里就不继续进行追踪了。

其他权限校验方法

hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

    @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")public String hello(){return "hello";}

hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

   @PreAuthorize("hasRole('system:dept:list')")public String hello(){return "hello";}

hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

    @PreAuthorize("hasAnyRole('admin','system:dept:list')")public String hello(){return "hello";}
自定义权限校验方法

我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。

@Component("ex")
public class SGExpressionRoot {
​public boolean hasAuthority(String authority){//获取当前用户的权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();List<String> permissions = loginUser.getPermissions();//判断用户权限集合中是否存在authorityreturn permissions.contains(authority);}
}

在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法

    @RequestMapping("/hello")@PreAuthorize("@ex.hasAuthority('system:dept:list')")public String hello(){return "hello";}

基于配置的权限控制

   我们也可以在配置类中使用使用配置的方式对资源进行权限控制。

    @Overrideprotected void configure(HttpSecurity http) throws Exception {http.antMatchers("/testCors").hasAuthority("system:dept:list222")}

CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

CSRF攻击与防御(写得非常好)_注销账号可以防范csrf吗-CSDN博客

SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

认证成功处理器

实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。

我们也可以自己去自定义成功处理器进行成功后的相应处理。

@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {
​@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println("认证成功了");}
}
​
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​@Autowiredprivate AuthenticationSuccessHandler successHandler;
​@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().successHandler(successHandler);
​http.authorizeRequests().anyRequest().authenticated();}
}
​

认证失败处理器

实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录失败处理器。

我们也可以自己去自定义失败处理器进行失败后的相应处理。

@Component
public class SGFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {System.out.println("认证失败了");}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​@Autowiredprivate AuthenticationSuccessHandler successHandler;
​@Autowiredprivate AuthenticationFailureHandler failureHandler;
​@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
//                配置认证成功处理器.successHandler(successHandler)
//                配置认证失败处理器.failureHandler(failureHandler);
​http.authorizeRequests().anyRequest().authenticated();}
}
 

登出成功处理器

@Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println("注销成功");}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​@Autowiredprivate AuthenticationSuccessHandler successHandler;
​@Autowiredprivate AuthenticationFailureHandler failureHandler;
​@Autowiredprivate LogoutSuccessHandler logoutSuccessHandler;
​@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
//                配置认证成功处理器.successHandler(successHandler)
//                配置认证失败处理器.failureHandler(failureHandler);
​http.logout()//配置注销成功处理器.logoutSuccessHandler(logoutSuccessHandler);
​http.authorizeRequests().anyRequest().authenticated();}
}
 

结束........................................

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

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

相关文章

Mesh网格obj文件构成解析

众所周知&#xff0c;Mesh网格是三维重建的常用手法&#xff0c;通过顶点-三角面的形式来完成对三维物体的表达。其中&#xff0c;最常见的Mesh网格文件格式就是obj格式。看起来复杂的三维形状其实在数值表示上是很简单的&#xff0c;大家跟我一起来做个小实验就好&#xff1a;…

echarts散点图自定义tooltip,鼠标放上去展示多行数据

先放效果图 如图&#xff0c;就是鼠标悬停在散点上&#xff08;这里的散点我替换成了图片&#xff0c;具体做法参考这篇文章&#xff1a;echarts散点图的散点用自定义图片替代-CSDN博客&#xff09;时&#xff0c;可以展示多行数据。之前查找资料的时候&#xff0c;很多用字符串…

练习unittest+Fixture实现

练习01 创建⼀个⽬录 case, 作⽤就是⽤来存放⽤例脚本,在这个⽬录中创建 5 个⽤例代码⽂件 , test_case1.py使⽤ TestLoader 去执⾏⽤例 将来的代码 ⽤例都是单独的⽬录中存放的 test_项⽬_模块_功能.py test_case1.py # 1. 导包 unittest import unittest # 2. 定义测试类, 只…

面试经典150题(114-118)

leetcode 150道题 计划花两个月时候刷完之未完成后转&#xff0c;今天完成了5道(114-118)150 gap 了一周&#xff0c;以后就不记录时间了。。 114.(70. 爬楼梯) 题目描述&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不…

旅行社旅游线路预定管理系统asp.net

旅行社旅游线路预定管理系统 首页 国内游 境外游 旅游景点 新闻资讯 酒店信息―留言板 后台管理 后台管理导航菜单系统管理修改密码留言管理注册会员管理基础数据设置国别设置有份设地区设置 旅行社管理友情链接管理添加友情链接友情链接管理新闻资讯管理添加新闻资讯新闻资讯管…

LayerNormalization 和 RMSNormalization的计算方法和区别

目录 问题来源 Layer Normalization 与 RMSNormalization 的详细计算方法 Layer Normalization&#xff08;层归一化&#xff09; RMSNormalization&#xff08;均方根归一化&#xff09; Layer Normalization与RMSNormalization的异同 Layer Normalization RMSNormaliza…

24 OpenCV直方图反向投影

文章目录 参考反向投影作用calceackProject 反向投影mixchannels 通道图像分割示例 参考 直方图反向投影 反向投影 反向投影是反映直方图模型在目标图像中的分布情况简单点说就是用直方图模型去目标图像中寻找是否有相似的对象。通常用HSV色彩空间的HS两个通道直方图模型 作用…

基于时空上下文(STC)的运动目标跟踪算法,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

Midjourney 和 Dall-E 的优劣势比较

Midjourney 和 Dall-E 的优劣势比较 Midjourney 和 Dall-E 都是强大的 AI 绘画工具&#xff0c;可以根据文本描述生成图像。 它们都使用深度学习模型来理解文本并将其转换为图像。 但是&#xff0c;它们在功能、可用性和成本方面存在一些差异。 Midjourney 优势: 可以生成更…

js判断对象是否有某个属性

前端判断后端接口是否返回某个字段的时候 <script>var obj { name: "John", age: 30 };console.log(obj.hasOwnProperty("name")); // 输出 trueconsole.log(obj.hasOwnProperty("email")); // 输出 falselet obj11 { name: "Joh…

9. 编程常见错误归类

编程常见错误归类 9.1 编译型错误9.2 链接型错误9.3 运行时错误 9.1 编译型错误 编译型错误⼀般都是语法错误&#xff0c;这类错误⼀般看错误信息就能找到⼀些蛛丝马迹的&#xff0c;双击错误信息也能初步的跳转到代码错误的地方或者附近。编译错误&#xff0c;随着语言的熟练…

力扣栈题:删除最外层括号

char* removeOuterParentheses(char* s) {int stack 0;int num0;for(int i0;i<strlen(s);i){if(s[i](){stack;if(stack>1){s[num]s[i];}}else{--stack;if(stack>0){s[num]s[i];}}}s[num]\0;return s; } 思路&#xff1a;迭代加栈&#xff0c;如果不是第一个的左括号则…

苍穹外卖-day10:Spring Task、订单状态定时处理、来单提醒(WebSocket的应用)、客户催单(WebSocket的应用)

苍穹外卖-day10 课程内容 Spring Task订单状态定时处理WebSocket来单提醒客户催单 功能实现&#xff1a;订单状态定时处理、来单提醒和客户催单 订单状态定时处理&#xff1a; 来单提醒&#xff1a; 客户催单&#xff1a; 1. Spring Task 1.1 介绍 Spring Task 是Spring框…

win32汇编弹出对话框

之前书上有一个win32 asm 的odbc例子&#xff0c;它有一个窗体&#xff0c;可以执行sql&#xff1b;下面看一下弹出一个录入数据的对话框&#xff1b; 之前它在.code段包含2个单独的asm文件&#xff0c;增加第三个&#xff0c;增加的这个里面是弹出对话框的窗口过程&#xff0…

哪些AI知识库比较好用?企业高管必看!

在科技进步的时代&#xff0c;工作效率和知识管理是企业面临的两大挑战。而AI知识库&#xff0c;正是解决这个问题的利剑。接下来&#xff0c;我将与你分享三款好用的AI知识库平台&#xff0c;感兴趣就往下看吧。 首先&#xff0c;我们不得不提的是Helplook。这是一个根据人工智…

使用Python进行数据库连接与操作SQLite和MySQL【第144篇—SQLite和MySQL】

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 使用Python进行数据库连接与操作&#xff1a;SQLite和MySQL 在现代应用程序开发中&#xf…

spring-boot-starter-thymeleaf加载外部html文件

在Spring MVC中&#xff0c;我们可以使用Thymeleaf模板引擎来实现加载外部HTML文件。 1.Thymeleaf介绍 Thymeleaf是一种现代化的服务器端Java模板引擎&#xff0c;用于构建漂亮、可维护且易于测试的动态Web应用程序。它适用于与Spring框架集成&#xff0c;并且可以与Spring M…

每日OJ题_牛客HJ12 字符串反转(IO型OJ)

目录 牛客HJ12 字符串反转 解析代码 牛客HJ12 字符串反转 字符串反转_牛客题霸_牛客网 解析代码 #include <iostream> using namespace std; int main() {string str "";cin >> str;int left 0, right str.size() - 1;while (left < right){ch…

Flink源码解析(1)TM启动

网络传输模型 首先在看之前,回顾一下akka模型: Flink通讯模型—Akka与Actor模型-CSDN博客 注:ActorRef就是actor的引用,封装好了actor 下面是jm和tm在通讯上的概念图: RpcGateway 不理解网关的作用,可以先移步看这里:网关_百度百科 (baidu.com) 用于定义RPC协议,是…

#每天一道面试题# 什么是MySQL的回表查询

MySQL中的索引按照物理存储的方式分为聚集索引和非聚集索引&#xff1b; 聚集索引索引和数据存储在一起&#xff0c;B树的叶子节点就是表数据&#xff0c;如果通过聚集索引查询数据&#xff0c;直接就可以查询出我们想要的数据&#xff1b;非聚集索引B树的叶子节点存储的是主键…