微服务多模块:Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT (附源码)仅需一招,520彻底拿捏你

news2024/12/27 10:10:11

  可能有些人会觉得这篇似曾相识,没错,这篇是由原文章进行二次开发的。

前阵子有些事情,但最近看到评论区说原文章最后实现的是单模块的验证,由于过去太久也懒得验证,所以重新写了一个完整的可以跑得动的一个。

OK,回到正题,以下是真正对应的微服务多模块的一个方法,使用到的技术有:基于微服务的Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT

对使用到的微服务技术进行在项目中的说明:

Security:负责登录验证(文章中没有实现授权,在过滤器中直接返回null,如果想实现授权,可以在返回null的地方添加授权信息类似ROLE_ADMIN,同时在Security的配置文件那里添加授权信息即可)。

Redis:负责缓存token跟用户数据。

Gateway:对前端提供的接口,由它多个模块进行接口调用。

OpenFeign:提供给security查询数据库中的用户信息。

Nacos:注册服务中心,注册服务的信息,使OpenFeign可以调用其他服务模块。

注意:虽然是原文章的二次编写,但是很多都不同,建议直接跟着这篇走。

目录

1.项目结构

2.Common模块

pom.xml

2.1 RedisConfig

2.2 RedisUtil

2.3 ResponseUtil

2.4  TokenUtil

         2.5  CorConfig

3.model模块

3.1 pom

3.2 User

         3.3 UserFeign

4.service模块

  4.1 目录结构​编辑

  4.2 service_user模块

    4.2.1 pom.xml

    4.2.2 application.yml

    4.2.3 Service_UserApp启动类

4.3 其他service模块

5.spring_security模块

5.1 pom

5.2 DiyUserDetails(UserDetails)

5.3 WebSecurityConfig(WebSecurityConfigurerAdapter)

5.4 TokenOncePerRequestFilter(OncePerRequestFilter)

5.5 LoginAuthenticationEntryPoint(AuthenticationEntryPoint)

5.6 LoginInFailHandler(AuthenticationFailureHandler)

5.7 LoginInSuccessHandler(AuthenticationSuccessHandler)

5.8 LogOutSuccessHandler(LogoutSuccessHandler)

5.9 NothingAccessDeniedHandler(AccessDeniedHandler)

         5.10 MyUserDetailService(UserDetailsService)

6.gateway模块

6.1 pom

6.2 application.yml

7.测试


1.项目结构

涉及的模块有

(1)common(Redis配置文件、Redis工具、Token工具、返回给前端信息的工具;即如下文件RedisConfig、RedisUtil、TokenUtil、ResponseUtil);

(2)gateway

(3)model(实体类,Feign的客户端);

(4)service(用户模块、课程模块);

(5)spring_security(security的过滤器跟配置文件)。

下面小编将全部一一介绍并且源码展示出来。

2.Common模块

pom.xml

        <!--springboot_redis缓存框架 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>model</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

2.1 RedisConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;
/*
 * Redis配置
 * 解决redis在业务逻辑处理层上不出错,缓存序列化问题
 * */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Resource
    RedisConnectionFactory redisConnectionFactory;
    @Bean
    public RedisTemplate<String,Object> redisTemplate(){
        RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
//Json序列化配置
        //1、String的序列化
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        //2、json解析任意的对象(Object),变成json序列化
        Jackson2JsonRedisSerializer<Object> serializer=new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper mapper=new ObjectMapper(); //用ObjectMapper进行转义
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //该方法是指定序列化输入的类型,就是将数据库里的数据按照一定类型存储到redis缓存中。
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(serializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(serializer);

        return redisTemplate;
    }
}

2.2 RedisUtil

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    public static StringRedisTemplate stringRedisTemplateStatic;
    @PostConstruct //在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
    public void initStringRedisTemplate(){
        stringRedisTemplateStatic=this.stringRedisTemplate;
    }

    private static final DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /*
     * 保存token信息到redis,也可直接在创建token中使用该方法
     * */
    public static  void redis_SaveTokenInfo(String token,String username){
        //以username做key
        LocalDateTime localDateTime=LocalDateTime.now();
        stringRedisTemplateStatic.opsForHash().put(username,"token",token);
        stringRedisTemplateStatic.opsForHash().put(username,"refreshTime", //有效时间
                df.format(localDateTime.plus(7*24*60*60*1000, ChronoUnit.MILLIS)));
        stringRedisTemplateStatic.opsForHash().put(username,"expiration",  //过期时间 5分钟 300秒
                df.format(localDateTime.plus(300*1000, ChronoUnit.MILLIS)));
        stringRedisTemplateStatic.expire(username,7*24*60*60*1000, TimeUnit.SECONDS);
    }

    /*
     * 检查redis是否存在token
     * */
    public static boolean hasToken(String username){
        return stringRedisTemplateStatic.opsForHash().getOperations().hasKey(username);
    }
}

2.3 ResponseUtil

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.apache.ibatis.annotations.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Data
public class ResponseUtil {
    public static int OK = 200;
    public static int ERROR = 404;
    public static String SUCCESS="操作成功!";
    public static String NO_SUCCESS="操作失败,请稍候重试。";
    //返回码(200)
    private int code;
    //返回消息
    private String message;

    @ApiModelProperty(value = "返回数据(单条或多条)")
    private Map<Object, Object> data = new HashMap<Object, Object>();


    public ResponseUtil(int code, String message) {
        this.code=code;
        this.message=message;
    }
    public ResponseUtil(int code, String message, Map<Object, Object> data) {
        this.code=code;
        this.message=message;
        this.data=data;
    }
    //对response写入Object数据
    public static void reponseOutDiy(HttpServletResponse response,int statusCode , Object result) {
        ObjectMapper mapper = new ObjectMapper();
        PrintWriter writer = null;
        response.setStatus(statusCode);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            writer = response.getWriter();
            mapper.writeValue(writer, result);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                writer.flush();
                writer.close();
            }
        }
    }

}

2.4  TokenUtil

import com.Lino_white.model.User; //model模块的user
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class TokenUtil {
    public static final String APP_SECRET ="Lino_white"; //随便取,你的Token密钥
    public static final String TOKEN_HEAD="Authorization";

    public static final String TOKEN_PREFIX = "Bearer ";
    public static String createToken(User user){
        String token = Jwts.builder()
                .setId(String.valueOf(user.getId()))
                .setSubject(user.getUsername())
                .setIssuedAt(new Date()) //签发时间
                .setIssuer("Lino_white") //签发者
                .setExpiration(new Date(System.currentTimeMillis() + 300* 1000)) //过期时间 5分钟 自行设置
                .signWith(SignatureAlgorithm.HS256, APP_SECRET) //签名算法跟密钥
                .claim("identity", user.getIdentity()) //可添加额外的属性
                .compact();
        return token;
    }

//重新生成新的Token,异常时间由传入的参数决定
    public static String createToken(User user,Date expirationTime){
        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            expirationTime= (Date) f.parse(f.format(expirationTime));
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
       
        String token = Jwts.builder()
                .setId(String.valueOf(user.getId()))
                .setSubject(user.getUsername())
                .setIssuedAt(new Date()) //签发时间
                .setIssuer("Lino_white") //签发者
                .setExpiration(expirationTime) //过期时间
                .signWith(SignatureAlgorithm.HS256, APP_SECRET) //签名算法跟密钥
                .claim("identity", user.getIdentity()) //可添加额外的属性
                .compact();
        return token;
    }

    //获得用户名
    public String getUsernameFromToken(String token){
        return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody().getSubject();
    }
    /**
     * 判断token是否存在与有效(1)
     */
    public boolean checkToken(String token){
        if (StringUtils.isEmpty(token)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }
    /**
     * 判断token是否存在与有效(2)
     */
    public boolean checkToken(HttpServletRequest request){
        try {
            String token = request.getHeader("token");
            return checkToken(token);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }
    //获得全部属性
    public Claims parseJwt(String token){
        Claims claims = Jwts.parser()
                .setSigningKey(APP_SECRET) // 设置标识名
                .parseClaimsJws(token)  //解析token
                .getBody();
        return claims;
    }
    //获得指定属性
    public String getTokenClaim(String token,String key){
        Claims body = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody();
        return String.valueOf(body.get(key));
    }
}

2.5  CorConfig

package com.goyes.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 解决跨域
 * @author: white
 */
@Configuration
public class CorConfig implements WebMvcConfigurer {


    @Override
    public void addCorsMappings(CorsRegistry registry) {
        System.out.println("开始解决跨域");
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
//                .allowCredentials(true)//是否有验证,有就打开
                .allowedHeaders("*")
                .maxAge(3600);
    }

}

3.model模块

3.1 pom

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>

3.2 User

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel(value = "实体:用户")
@TableName("user")
public class User implements Serializable {
    @ApiModelProperty("用户id")
    @TableId(value = "id",type = IdType.AUTO)
    private int id;
    @ApiModelProperty("用户名")
    private String username;
    @ApiModelProperty("密码")
    private String password;
    @TableField("identity")
    @ApiModelProperty("身份")
    private String identity;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", identity='" + identity + '\'' +
                '}';
    }
}

3.3 UserFeign

package com.goyes.model.client;

import com.goyes.model.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "service-user")
public interface UserFeign {
    @GetMapping("/api/user/{username}")
    public User findUserByName(@PathVariable("username") String username);

}

4.service模块

  4.1 目录结构

  4.2 service_user模块

    4.2.1 pom.xml

注意:service_user接入了security模块。

        <!--openfeign 远程接口调用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--nacos 注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--nacos 配置中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!--nacos 客户端-->
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

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

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>model</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

        <!--加入各service模块,swagger文档实现接入-->
        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>service_other</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>service_course</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.goyes.service_comment</groupId>
            <artifactId>service_comment</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

        <!--接入security模块-->
        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>spring_security</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

    4.2.2 application.yml

server:
  port: 8001
spring:
  application:
    name: service-user
  main:
    allow-bean-definition-overriding: true
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        group: dev
      discovery:
        cluster-name: WHITE
feign:
  client:
    config:
      default:
        connect-timeout: 10000
        read-timeout: 10000

    4.2.3 Service_UserApp启动类

@SpringBootApplication
@EnableSwagger2WebMvc
@EnableDiscoveryClient
@EnableFeignClients
@EnableCaching
public class Service_UserApp
{
    public static void main( String[] args )
    {
        SpringApplication.run(Service_UserApp.class, args);
    }
}

    4.2.4 ApiController控制器

在任意一个控制器中,添加如下代码,该接口将用于OpenFeign的远程接口调用,由security模块中的自定义类MyUserDetailService去进行调用。(MyUserDetailService的代码在介绍security模块中会出现)

    @GetMapping("/api/user/{username}")
    public User findUserByName(@PathVariable("username") String username){
        User userByName = userService.findUserByName(username);
        return userByName;
    }

4.3 其他service模块

对于其他模块,相对应跟service_user模块一样,进行如下操作即可:

(1)在pom.xml中引入security模块

        <!--接入security模块-->
        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>spring_security</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

(2)在application.xml中添加以下代码

spring:
  main:
    allow-bean-definition-overriding: true

        防止出现运行异常报错信息,对于同一个服务的FeignClient来说,配置该属性不会造成覆盖,详情可以查看该文章:Consider renaming one of the beans:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

5.spring_security模块

5.1 pom

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--security安全框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--springboot_redis缓存框架 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>model</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.goyes</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

5.2 DiyUserDetails(UserDetails)

import com.Lino_white.model.User;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
@Data
@EqualsAndHashCode(callSuper = false)
public class DiyUserDetails extends User implements UserDetails, Serializable {

    //用户权限列表
    private Collection<String> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities1 = new ArrayList<>();
        for(String permissionValue : authorities) {
            if(StringUtils.isEmpty(permissionValue)) continue;
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
            authorities1.add(authority);
        }

        return authorities1;
    }

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

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

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

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

5.3 WebSecurityConfig(WebSecurityConfigurerAdapter)

注意:前面将对service_user的远程接口定义为/api/user/{username},所以在过滤方面要放行该路径,否则security无法调用数据库查询用户信息,导致程序报错。

对此,可以查看该文章feign.FeignException$Unauthorized

package com.goyes.spring_security.config;


import com.goyes.spring_security.filter.TokenAuthenticationFilter;
import com.goyes.spring_security.filter.TokenLoginFilter;
import com.goyes.spring_security.filter.TokenOncePerRequestFilter;
import com.goyes.spring_security.handler.*;
import com.goyes.spring_security.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

@Configuration
@EnableWebSecurity //开启Security功能
@EnableGlobalMethodSecurity(prePostEnabled = true) //启动方法级别的权限认证
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;

    @Bean
    //配置密码加密器
    public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}

    //配置哪些请求不拦截
    //TODO 将需要Feign的方法前缀都用上api,得到api/select/user/{user_id}这样的路径不受限制
    // 由于api路径是由服务模块自己去调用的,所以gateway不用做路径请求的处理
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/**","/doc.html#/**","/swagger-resources");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
    }

    //配置安全策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("读取配置*****************WHITE");
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                //该过滤器设置在用户名、密码、权限过滤器之前。这样每次访问接口都会经过此过滤器,我们可以获取请求路径,并判定当请求路径为/login时进入验证码验证流程。
                // 使用jwt的Authentication,来解析过来的请求是否有token
                .addFilterBefore(new TokenOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class)

                //登录后,访问没有权限处理类
                .exceptionHandling().accessDeniedHandler(new NothingAccessDeniedHandler())
                //匿名访问,没有权限的处理类
                .authenticationEntryPoint(new LoginAuthenticationEntryPoint())

                .and()
                .formLogin()
                .successHandler(new LoginInSuccessHandler())
                .failureHandler(new LoginInFailHandler())
                .and()
                .logout()
                .logoutSuccessHandler(new LogOutSuccessHandler())
                // 配置取消session管理
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable();
    }
}

5.4 TokenOncePerRequestFilter(OncePerRequestFilter)

注意:在这里TokenUtil跟RedisUtil对于过期时间的定义不同。

token过期时间为3分钟,redis上存储的异常时间为5分钟,并且redis上存储的刷新时间为7天

在下面的配置文件中,仅仅对token进行分析而已,可以根据需要在这里做验证码校验。

token的过期时间在以下代码中是这样做的,当token过期时间3分钟到了,判断redis上存储的异常时间是否到了5分钟,没到5分钟就返回一个新的token给前端,前端拿到该token就可以继续访问;如果到了5分钟,则会停止访问并通知前端 “用户已经过期,请重新登录”。

小编有个想法(还没做):在这里可以重新定义过期时间,比如用户每次访问时都进行判断:当token的过期时间小于1分钟后就刷新redis的异常时间,这样可以使当token要过期时,就有新的token出现,但这样操作也存在缺点:就是要消耗内存资源,每次都得去读取token是否临近过期时间了。对于这块,可以针对自己的情况去做调整。

package com.goyes.spring_security.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.goyes.common.utils.RedisUtil;
import com.goyes.common.utils.ResponseUtil;
import com.goyes.common.utils.TokenUtil;
import com.goyes.model.User;
import com.goyes.spring_security.model.DiyUserDetails;
import io.github.classgraph.json.JSONUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import jdk.nashorn.internal.parser.JSONParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.json.JsonParser;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import springfox.documentation.spring.web.json.Json;
import sun.security.util.SecurityConstants;
//import sun.security.util.SecurityConstants;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.crypto.Data;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;

/**
 * 在用户名、密码、权限过滤器之前的过滤器
 * 在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中
 *
 * TODO 下面过滤器仅做了针对token解析,包括token异常、过期、重新颁布等
 * @author white
 */
@Component
public class TokenOncePerRequestFilter extends OncePerRequestFilter {

    @Autowired
    StringRedisTemplate stringRedisTemplate = RedisUtil.stringRedisTemplateStatic;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        /*
        * TODO 可在这里判断请求过来的路径是否为login,方式为post,来在这里进行验证码有效验证
        *  验证成功则直接chain(request,response)继续走过滤
        * */
        String requestURI = request.getRequestURI();
        System.out.println("开始请求,请求路径:"+requestURI+"  请求方式:"+request.getMethod());
        User user = null;
        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String authHeader = request.getHeader(TokenUtil.TOKEN_HEAD);

        //没有token不用理
        if (authHeader != null && authHeader.startsWith(TokenUtil.TOKEN_PREFIX)) {

            final String authToken = authHeader.replace(TokenUtil.TOKEN_PREFIX, "");
            //这里的authToken可能时间已过,需要重新创建一个token
            //先对比redis中的过期时间,redis的过期时间随着用户的操作而更新,token可能没有及时更新
            //判断是否一样,一样的话就是token失效了,跳转重新登录,
            // 不一样就是redis过期时间更新了,生成新的token返回给前端
            String username = null;
            Claims claims;
            try {
                claims = new TokenUtil().parseJwt(authToken);
                username = claims.getSubject();
            } catch (ExpiredJwtException e) {
                //token过期
                claims = e.getClaims();
                username = claims.getSubject();
                user = JSONObject.parseObject(String.valueOf(stringRedisTemplate.opsForHash().get(username, "user")), User.class);

                if (user == null) {
                    chain.doFilter(request, response);
                    return;
                } else {
                    if (RedisUtil.hasToken(username)) {
                        Object expiration = stringRedisTemplate.opsForHash().get(username, "expiration");
                        Object tokenExpirationTime = f.format(claims.getExpiration());
                        Date expirationDate_redisTime = null, expirationDate_tokenTime = null, nowTime;
                        try {
                            expirationDate_redisTime = (Date) f.parseObject(String.valueOf(expiration));
                            expirationDate_tokenTime = (Date) f.parseObject(String.valueOf(tokenExpirationTime));
                            nowTime = (Date) f.parseObject(f.format(new Date()));
                        } catch (ParseException ex) {
                            throw new RuntimeException(ex);
                        }

                        System.out.println("*********Token过期(Start)***********");
                        System.out.println("token浏览器过期时间:" + tokenExpirationTime);

                        System.out.println("redis过期时间:" + expiration);

                        //
                        /*
                         * redis<token || token=redis || redis <now  则token失效,跳转登录
                         * token<redis
                         * */
                        if (expirationDate_redisTime.getTime() < expirationDate_tokenTime.getTime() ||
                                expirationDate_tokenTime.getTime() == expirationDate_redisTime.getTime() ||
                                expirationDate_redisTime.getTime() < nowTime.getTime()) {

                            //时间相同,跳转登录
                            ResponseUtil.reponseOutDiy(response, 401, "用户已过期,请重新登录");
                            System.out.println("*********Token过期(End)失效***********");
                            return;
                        } else {
                            //时间不同,生成新token  需要用户id,身份,用户名
                            //response存入token   返回
                            Object expiration_redisTime = stringRedisTemplate.opsForHash().get(username, "expiration");

                            Date date;
                            try {
                                date = (Date) f.parseObject(String.valueOf(expiration_redisTime));
                            } catch (ParseException ex) {
                                throw new RuntimeException(ex);
                            }
                            //通过数据库查询数据,创建token
                            System.out.println("这里之前开始的时间:" + date);
                            String token = TokenUtil.createToken(user, date);
                            System.out.println("—————————————————start—————————————————————");
                            System.out.println("token:" + token);
                            RedisUtil.redis_SaveTokenInfo(user, token);
                            response.setHeader(TokenUtil.TOKEN_HEAD, TokenUtil.TOKEN_PREFIX + token);
                            request.setAttribute(TokenUtil.TOKEN_HEAD, TokenUtil.TOKEN_PREFIX + token);
                            Date expiration1 = new TokenUtil().parseJwt(token).getExpiration();
                            System.out.println("重新更新token后过期时间:" + expiration1);
                            System.out.println("—————————————————End—————————————————————");
                            ResponseUtil.reponseOutDiy(response, 200, token);
                            System.out.println("*********Token过期(End)新Token***********");
                            return;
                        }
                    } else {
                        //TODO 新增,如果redis没有username,说明未登录
                        throw new RuntimeException("未登录");
                    }
                }
            }
            //避免每次请求都请求数据库查询用户信息,从缓存中查询
            user = JSONObject.parseObject(String.valueOf(stringRedisTemplate.opsForHash().get(username, "user")), User.class);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                if (user != null) {
                    UsernamePasswordAuthenticationToken authentication =
                            // TODO 未修改  这里的权限先空着
                            new UsernamePasswordAuthenticationToken(user, user.getPassword(), null);
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        System.out.println("走过滤——————————————————————————");
        chain.doFilter(request, response);
    }
}

5.5 LoginAuthenticationEntryPoint(AuthenticationEntryPoint)

import com.Lino_white.common.ResponseUtil;
import com.Lino_white.common.TokenUtil;
import jdk.nashorn.internal.parser.Token;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 匿名未登录的时候访问,需要登录的资源的调用类
 * @author Lino_white
 */
@Component
public class LoginAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        String token =httpServletRequest.getHeader(TokenUtil.TOKEN_HEAD);
        System.out.println("当前未登录,无法访问 ::"+token);
        if (token!=null && token.contains(TokenUtil.TOKEN_PREFIX)) {
            token=token.replace(TokenUtil.TOKEN_PREFIX,"");
            String usernameFromToken = new TokenUtil().getUsernameFromToken(token);
            System.out.println("用户名:"+usernameFromToken);
        }
        ResponseUtil.reponseOutDiy(httpServletResponse,401,"当前未登录,无法访问");
    }
}

5.6 LoginInFailHandler(AuthenticationFailureHandler)

import com.Lino_white.common.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 登录账号密码错误等情况下,会调用的处理类
 * @author Lino_white
 */
@Component
public class LoginInFailHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        System.out.println("认证失败————————————");
        ResponseUtil.reponseOutDiy(httpServletResponse,401,"登录失败,请重试");
    }
}

5.7 LoginInSuccessHandler(AuthenticationSuccessHandler)

package com.goyes.spring_security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.goyes.common.utils.RedisUtil;
import com.goyes.common.utils.ResponseUtil;
import com.goyes.common.utils.TokenUtil;
import com.goyes.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
 * @LoginInSuccessHandler.java的作用:
 * 登录成功处理类,登录成功后会调用里面的方法
 * @author: white文
 * @time: 2023/5/18 16:02
 */
@Slf4j
@Component
public class LoginInSuccessHandler implements AuthenticationSuccessHandler {


    /**
     * 用户通过TokenLoginFilter(UsernamePasswordAuthenticationFilter)后,
     * 验证成功到这里进行
     * 1.获取当前用户
     * 2.token创建
     * 3.并将其存入redis并返回
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登录成功,开始初始化token并缓存在redis");
                User user =(User) authentication.getPrincipal();
        String token = TokenUtil.createToken(user);
        //redis缓存token
        RedisUtil.redis_SaveTokenInfo(user,token);
        //写入response
        response.setHeader("token", TokenUtil.TOKEN_PREFIX+token);

        try {
            //登录成功,返回json格式进行提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out=response.getWriter();
            Map<String,Object> map=new HashMap<String,Object>(4);
            map.put("code",HttpServletResponse.SC_OK);
            map.put("message","这里全部都是自定义的!登录成功");
            map.put("token",token);
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

5.8 LogOutSuccessHandler(LogoutSuccessHandler)

import com.Lino_white.common.RedisUtil;
import com.Lino_white.common.ResponseUtil;
import com.Lino_white.common.TokenUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class LogOutSuccessHandler implements LogoutSuccessHandler {
    private StringRedisTemplate stringRedisTemplate= RedisUtil.stringRedisTemplateStatic;
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //用户退出登录
        System.out.println("LogoutSuccessHandler退出");
        String token=request.getHeader("token");
        if (token==null) token=request.getHeader(TokenUtil.TOKEN_HEAD);
        token=token.replace(TokenUtil.TOKEN_PREFIX,"");
        String username = new TokenUtil().getUsernameFromToken(token);
        Authentication au = SecurityContextHolder.getContext().getAuthentication();
        if (au!=null) new SecurityContextLogoutHandler().logout(request,response,au);
        Boolean delete = stringRedisTemplate.delete(username);
        if (delete) ResponseUtil.reponseOutDiy(response,200,"用户已成功退出");
    }
}

5.9 NothingAccessDeniedHandler(AccessDeniedHandler)

import com.Lino_white.common.ResponseUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 没有权限,被拒绝访问时的调用类
 * @author Lino_white
 */
@Component
public class NothingAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        System.out.println("没有权限");
        ResponseUtil.reponseOutDiy(httpServletResponse,403,"当前您没有该权限");
    }
}

5.10 MyUserDetailService(UserDetailsService)

注意:在这里调用了model模块中的UserFeign文件,实现读取service_user模块中的findUserByName方法。

package com.goyes.spring_security.service;

import com.goyes.model.User;
import com.goyes.model.client.UserFeign;
import com.goyes.spring_security.model.DiyUserDetails;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 从数据库读取用户信息(用户名,密码,身份)进行身份认证
 */
@Service
public class MyUserDetailService implements UserDetailsService{
    @Autowired
    private UserFeign userFeign;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("********开始loadUserByUsername********");
        User user = userFeign.findUserByName(username);
        System.out.println("浏览器的username:"+username);

        if (user==null) throw new UsernameNotFoundException(username);
        System.out.println("数据库的username:"+user.getUsername());
        //根据当前用户名查询用户权限
        List<String> authorities=new ArrayList<>();
        authorities.add("ROLE_"+user.getIdentity());
        DiyUserDetails details=new DiyUserDetails();
        BeanUtils.copyProperties(user,details);
        details.setAuthorities(authorities);
        //如果数据库密码无加密,用下列
        //details.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        System.out.println("********结束loadUserByUsername********");
        return details;
    }
}

6.gateway模块

6.1 pom

        <dependency>
           <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

6.2 application.yml

server:
  port: 10000
spring:
  application:
    name: gateway

  cloud:
    gateway:
      routes:
        - id: user
          uri: http://localhost:8001
          predicates:
            - Path=/user/**,/admin/**,/api/user/**,/login,/logout
        - id: course
          uri: http://localhost:8002
          predicates:
            - Path=/course/**

7.测试

这里是使用postman工具进行测试的,在这里之前已经在数据库有用户名跟密码都为111的数据,并且密码已是加密形式。

1.首次访问/admin/findAll,gateway会调用到8001端口下的user模块,如下图

 首次访问/course/findAll,gateway会调用到8002端口下的course模块,如下图

2.POST访问/login,并且提供相关参数(数据库存在用户111和加密过的密码111),得到token,如下图:

这时,redis数据库就有了用户名为 user的数据 

3.复制刚才返回给前端的token,在Authorization的Type中,选择Bearer Token,粘贴上刚才的Token,点击Send发送

 5.再次请求8002端口下的source模块

 再次请求8001端口下的user模块

6.当token过期后,redis中的异常时间还没到,则会返回给前端一个新的token,拿着新token继续请求即可。

7.当token过期并且redis的异常时间也过了之后,用户就需要重新登录。

8.退出则为/logout 。

  同时,redis数据库中用户名为user的key值也被删除掉。

至此,结束!祝大家520快乐!!!

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

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

相关文章

nvidia-smi 失效解决

服务器重启后&#xff0c;跑模型发现&#xff1a; RuntimeError: Attempting to deserialize object on a CUDA device but torch.cuda.is_available() is False. If you are running on a CPU-only machine, please use torch.load with map_locationtorch.device(cpu) to ma…

Linux常用命令——hping3命令

在线Linux命令查询工具 hping3 测试网络及主机的安全 补充说明 hping是用于生成和解析TCPIP协议数据包的开源工具。创作者是Salvatore Sanfilippo。目前最新版是hping3&#xff0c;支持使用tcl脚本自动化地调用其API。hping是安全审计、防火墙测试等工作的标配工具。hping优…

【开源项目】Nepxion Aquarius实现分布式锁、缓存、ID生成、限速的源码解读

项目地址 项目地址 https://toscode.gitee.com/nepxion/Aquarius 项目介绍 Nepxion Aquarius是一款基于Redis Zookeeper的分布式应用组件集合&#xff0c;包含分布式锁&#xff0c;缓存&#xff0c;ID生成器&#xff0c;限速限流器。它采用Nepxion Matrix AOP框架进行切面架构…

天工开物 #6 Git 分支管理与版本发布

Git 版本管理系统由 Linux 的作者 Linus Torvalds 于 2005 年创造&#xff0c;至今不到二十年。 起初&#xff0c;Git 用于 Linux Kernel 的协同开发&#xff0c;用于替代不再提供免费许可的 BitKeeper 软件。随后&#xff0c;这一提供轻量级分支的分布式版本管理系统得到了开源…

产品经理被气的脸都绿了!

见字如面&#xff0c;我是军哥&#xff01; 本来今天不想发文了&#xff0c;想躺平一下&#xff0c;毕竟今天周五了嘛。 可是今天早上一位买了我《技术人核心能力》的程序员学员发来私信&#xff0c;说他给产品经理上了一课&#xff0c;声称产品经理当时脸都绿了&#xff0c;并…

浅浅的理解MVI

MVI 的概念 官网解释&#xff1a; https://developer.android.google.cn/topic/architecture?hlzh-cn MVI在架构分层上和MVP没有本质区别&#xff0c;但区别主要体现在架构风格和编程思想上。 MVI 是 Model-View-Intent 的缩写&#xff0c;它也是一种响应式 流式处理思想的…

【Linux高级 I/O(2)】如何使用阻塞 I/O 与非阻塞 I/O?——select()函数

上次我们虽然使用非阻塞式 I/O 解决了阻塞式 I/O 情况下并发读取文件所出现的问题&#xff0c;但依然不够完美&#xff0c;使得程序的 CPU 占用率特别高。解决这个问题&#xff0c;就要用到本文将要介绍的 I/O 多路复用方法。 何为 I/O 多路复用 I/O 多路复用&#xff08;IO m…

数据结构-时间复杂度和空间复杂度

时间复杂度和空间复杂度 算法效率时间复杂度空间复杂度表示方法&#xff08;大O的线性表示&#xff09;举例说明时间复杂度举例说明空间复杂度举例说明冒泡排序的时间和空间复杂度递归情况下的时间和空间复杂度两个例子 算法效率 算法&#xff08;Algorithn&#xff09;是指用来…

Spring MVC的核心类和注解

DispatcherServlet DispatcherServlet作用 DispatcherServlet是Spring MVC的核心类&#xff0c;也是Spring MVC的流程控制中心&#xff0c;也称为Spring MVC的前端控制器&#xff0c;它可以拦截客户端的请求。拦截客户端请求之后&#xff0c;DispatcherServlet会根据具体规则…

chatgpt赋能Python-python3接口自动化

Python3接口自动化&#xff1a;提升测试效率的利器 Python是一种高级编程语言&#xff0c;广泛应用于Web开发、数据科学、机器学习等领域。近年来&#xff0c;Python在接口自动化测试领域也变得越来越受欢迎。 Python的易读性、可扩展性以及模块化的特性&#xff0c;使得它成为…

Elasticsearch环境搭建(Windows)

一、介绍 布式、RESTful 风格的搜索和分析。 Elasticsearch 是位于 Elastic Stack 核心的分布式搜索和分析引擎。Logstash 和 Beats 有助于收集、聚合和丰富您的数据并将其存储在 Elasticsearch 中。Kibana 使您能够以交互方式探索、可视化和分享对数据的见解&#xff0c;并管…

手撕code(1)—— 排序算法

文章目录 前言1 冒泡排序2 选择排序3 插入排序4 快速排序5 归并排序6 堆排序7 希尔排序 前言 算法动画 时间复杂度分析 从小到大排序 1 冒泡排序 被动的将最大值送到最右边 1、比较相邻的元素。如果第一个比第二个大&#xff0c;就交换他们两个。 2、对每一对相邻元素作同…

Vite+Vue+iClient for Leaflet引入并实现MapV/Eharts第三方可视化库示例

作者&#xff1a;gaogy 文章目录 背景一、使用Vite构建Vue3JavaScript项目二、搭建iClient for Leaflet开发环境三、第三方可视化库Echarts的使用四、第三方可视化库MapV的使用五、其他地图库 背景 最近很多小伙伴咨询关于基于Vue3框架搭建iClent开发环境并使用Echarts与MapV等…

OPEN AI角色插件通道开放接入支持各种细分领域对话场景模型一键接入AI 智能

相信还是有很多伙伴不了解OPEN AI平台 &#xff0c;这里在细说一下 大家知道ChatGPT, 或者百度文心一言 阿里通意千问 包括各种其他的AI 聊天或者画图&#xff0c;等应用层出不穷。 但是我们要自己实现自己 语言大模型&#xff0c;或者说是人工智能应用能不能。 有实力当然可…

C++小项目之文本编辑器mynote(1.0.0版本)

2023年5月19日&#xff0c;周五晚上&#xff1a; 今天晚上突然想写一个运行在命令行上的文本编辑器&#xff0c;因为平时写文本时老是要创建新的文本文件&#xff0c;觉得太麻烦了。捣鼓了一个晚上&#xff0c;才选出一个我觉得比较满意的。我把这个程序添加到了系统环境变量中…

C语言指针学习笔记

1-二维数组指针 int a[3][4]a代表二维数组首元素的地址&#xff0c;此首元素不是一个简单的整形元素&#xff0c;而是由4个整形元素组成的一维数组&#xff0c;因此a代表的是首行&#xff08;序号为0的行&#xff09;的起始地址。a1代表序号为1的行的起始地址。a指向a[0], …

一个月50场面试,跑的慢就抢在别人前面!

300万字&#xff01;全网最全大数据学习面试社区等你来&#xff01; 今天的主人公也是一个应届生新人拿到满意offer的案例。 下面是一些聊天记录和面经&#xff0c;这名同学做的非常好的一个点&#xff0c;他把个人项目中的所用到的技术栈和项目具体的业务流程图以及用到的技术…

2年再见面

我和张哥是在两年前吃过饭&#xff0c;那时候我是在大学城上班。 两年前&#xff0c;张哥在微信上跟我说话&#xff0c;说要来深圳找我&#xff0c;问我什么时间方便&#xff0c;请我吃个便饭。两年前&#xff0c;公众号还比较火热。有挺多人找我做事情&#xff0c;找我做事情之…

桥梁安全监测,智能化桥梁结构健康监测方案

桥梁是现代城市交通网络中不可或缺的组成部分&#xff0c;但由于长期受到自然环境和人为因素的影响&#xff0c;桥梁的安全问题一直备受关注。传统的桥梁检测方式主要是靠人力进行巡查&#xff0c;这种方式效率低下、成本高&#xff0c;而且难以全面掌握桥梁结构的真实情况。随…

回顾 | Let's Learn .NET-通过 Semantic Kernel .NET SDK 管理你的 OpenAI 项目

点击蓝字 关注我们 编辑&#xff1a;Alan Wang 排版&#xff1a;Rani Sun Lets Learn .NET 系列 “Lets Learn .NET” 是面向全球的 .NET 初学者学习系列&#xff0c;旨在通过不同语言&#xff0c;帮助不同地区的开发者掌握最新的 .NET 开发知识与技能。 在 ChatGPT 与 OpenAI…