微服务多模块:Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT (附源码)仅需一招,520彻底拿捏你
可能有些人会觉得这篇似曾相识,没错,这篇是由原文章进行二次开发的。
前阵子有些事情,但最近看到评论区说原文章最后实现的是单模块的验证,由于过去太久也懒得验证,所以重新写了一个完整的可以跑得动的一个。
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 {@ResourceRedisConnectionFactory redisConnectionFactory;@Beanpublic 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序列化方式采用jacksonredisTemplate.setValueSerializer(serializer);// hash的value序列化方式采用jacksonredisTemplate.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 {@Autowiredprivate 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做keyLocalDateTime 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 {@Overridepublic 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;@Overridepublic 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-usermain:allow-bean-definition-overriding: trueprofiles:active: devcloud:nacos:config:server-addr: 127.0.0.1:8848group: devdiscovery:cluster-name: WHITE
feign:client:config:default:connect-timeout: 10000read-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;@Overridepublic 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;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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 {@Autowiredprivate MyUserDetailService myUserDetailService;@Bean//配置密码加密器public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}//配置哪些请求不拦截//TODO 将需要Feign的方法前缀都用上api,得到api/select/user/{user_id}这样的路径不受限制// 由于api路径是由服务模块自己去调用的,所以gateway不用做路径请求的处理@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/api/**","/doc.html#/**","/swagger-resources");}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());}//配置安全策略@Overrideprotected 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 {@AutowiredStringRedisTemplate stringRedisTemplate = RedisUtil.stringRedisTemplateStatic;@Overrideprotected 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);}//通过数据库查询数据,创建tokenSystem.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 {@Overridepublic 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 {@Overridepublic 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并返回*/@Overridepublic 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缓存tokenRedisUtil.redis_SaveTokenInfo(user,token);//写入responseresponse.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;@Overridepublic 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 {@Overridepublic 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{@Autowiredprivate UserFeign userFeign;@Overridepublic 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: gatewaycloud:gateway:routes:- id: useruri: http://localhost:8001predicates:- Path=/user/**,/admin/**,/api/user/**,/login,/logout- id: courseuri: http://localhost:8002predicates:- 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快乐!!!
相关文章:

微服务多模块:Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT (附源码)仅需一招,520彻底拿捏你
可能有些人会觉得这篇似曾相识,没错,这篇是由原文章进行二次开发的。 前阵子有些事情,但最近看到评论区说原文章最后实现的是单模块的验证,由于过去太久也懒得验证,所以重新写了一个完整的可以跑得动的一个。 OK&#…...

HNU数据结构与算法分析-作业4-图结构
1. (简答题) 【应用题】11.3 (a)画出所示图的相邻矩阵表示 (b)画出所示图的邻接表表示 (c)如果每一个指针需要4字节,每一项顶点的标号占用2字节,每一条边的权需要2字节࿰…...
AMPL IDE语法整理
文章目录 1、参数\集合和变量定义2、目标和约束的书写3、求解命令4、AMPL调用不同求解器的Options目录未完待续~ \qquad 最近再搞一些模型,需要用到AMPL中不同的求解器进行验证求解,故建立本博客,用于随时整理AMPL的相关语法和命令࿰…...
从0-1搭建支持gb28181协议搭建流媒体平台
系列文章目录 文章目录 系列文章目录前言一、搭建流程二、运行播放直播流效果: 总结前言 一个基于C++开发的国标GB28181流媒体信令服务器。 采用SipServer+ZLMediaKit。可以搭建一个…...
数据结构与算法之栈: Leetcode 682. 棒球比赛 (Typescript版)
棒球比赛 https://leetcode.cn/problems/baseball-game/ 描述 你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。 比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops&a…...

E-office Server_v9.0 漏洞分析
漏洞简介 泛微e-office是一款标准化的协同OA办公软件,实行通用化产品设计,充分贴合企业管理需求,本着简洁易用、高效智能的原则,为企业快速打造移动化、无纸化、数字化的办公平台。由于泛微 E-Office 未能正确处理上传模块中输入…...

MySQL数据库,JDBC连接数据库操作流程详细介绍
前言: 在学完 MySQL 和 Java 后,我们通常会尝试使用 Java编译器 连接 MySQL数据库,从而达到使用编译器来操作数据库的效果。连接的这个过程会用 JDBC 相关知识,因此我把 JDBC 包的下载及导入流程,以及 JDBC 的使用流程…...

libevent高并发网络编程 - 06_基于libevent的C++线程池实现
文章目录 1 功能简介线程池的初始化线程池执行流程 2 线程池类的设计线程类XThreadXThread.hXThread.cpp 线程池类XThreadPoolXThreadPool.hXThreadPool.cpp 任务基类taskXTask.h 3 自定义任务的例子自定义任务类ServerCMDServerCMD.hServerCMD.cpp 测试程序运行效果 1 功能简介…...

【Java EE 初阶】线程安全及死锁解决方案
目录 1.多线程下线程不安全的问题 1.使用多个线程对Array List集合进行添加操作并打印,查看结果 2.如何在多线程环境下使用线程安全的集合类 CopyOnWriteArrayList 3.多线程环境下使用队列 4.多线程环境下使用哈希表 1.HashTable线程安全 2.Concurrent Hash M…...

C语言函数大全-- _w 开头的函数(5)
C语言函数大全 本篇介绍C语言函数大全-- _w 开头的函数 1. _wspawnl 1.1 函数说明 函数声明函数功能int _wspawnl(int mode, const wchar_t* cmdname, const wchar_t* arglist, ...);启动一个新的进程并运行指定的可执行文件 参数: mode : 启动命令的…...

机械大专生能学会云计算吗,完全零基础的
机械大专生能学会云计算吗,完全零基础的 正常来说,大专及以上学历都能学会云计算,但是会和满足就业需求是两回事哈。如果你想通过学习就业,就需要根据当下相关岗位的普遍技术需求以及其他方面的要求,来针对性的学习和提…...

腾讯云EdgeOne为什么能让客户降本增效?
随着数字化时代的来临,各类线上互动场景不断出现,并成为人们日常工作生活中的一部分。然而,基于互联网提供线上娱乐、线上办公、线上购物等服务的企业,在复杂的全球网络环境下会遇到网络延迟不稳定的情况,海外环境更多…...

基于粒子群算法的微网经济优化调度——附Matalb代码
目录 摘要: 代码主要内容: 研究背景: 微电网模型: 粒子群算法: 运行结果: Matlab代码分享: 摘要: 提出了一种经济与环保相协调的微电网优化调度模型,针对光伏电池…...

Flink入门
目录 一、Flink简介 二、为什么选择Flink 三、与传统数据处理架构相比 四、Flinik批处理数据基础代码 五、Flink流处理基础代码 一、Flink简介 Apache Flink 是一个框架和分布式处理引擎,用于对无界和有界数 据流进行状态计算。 二、为什么选择Flink 流数据更…...

【Go微服务开发】gin+grpc+etcd 重构 grpc-todolist 项目
写在前面 最近稍微重构了之前写的 grpc-todolist 模块 项目地址:https://github.com/CocaineCong/grpc-todoList 1. 项目结构改变 与之前的目录有很大的区别 1.1 grpc_todolist 项目总体 1.1.1 改变前 grpc-todolist/ ├── api-gatway // 网关模块 ├── ta…...

单板硬件设计:存储器SD卡( NAND FLASH)
在单板设计中,无论是涉及到一个简易的CPU、MCU小系统或者是复杂的单板设计,都离不开存储器设计: 1、存储器介绍 存储器的分类大致可以划分如下: ROM和RAM指的都是半导体存储器,ROM在系统停止供电的时候仍然可以保持数…...

C++实现日期类Date(超详细)
个人主页:平行线也会相交💪 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 平行线也会相交 原创 收录于专栏【C之路】💌 本专栏旨在记录C的学习路线,望对大家有所帮助🙇 希望我们一起努力、成长&…...

实验室检验系统源码,集检验业务、质量控制、报告、统计分析、两癌等模块于一体
云 LIS 系统针对区域化 LIS 而设计,依托底层云架构,将传统的 LIS 功能模块进行“云化”。 该系统是集检验业务、科室管理、质量控制、报告、统计分析、两癌等模块于一体的数据检验信息平台。通过计算机联网,实现各类仪器数据结果的实时自动接…...

学习RHCSA的day.03
目录 2.6 Linux系统的目录结构 2.7 目录操作命令 2.8 文件操作命令 2.6 Linux系统的目录结构 1、Linux目录结构的特点 分区加载于目录结构: 使用树形目录结构来组织和管理文件。整个系统只有一个位于根分区的一个根目录(树根)、一棵树。…...

电子邮件协议(SMTP,MIME,POP3,IMAP)
SMTP 关键词: 电子邮件协议:SMTP简单邮件传输协议,负责将邮件上传到服务器,采用TCP的25端口,C/S工作。仅传送ASCII码文本 详细介绍: SMTP是一种提供可靠且有效的电子邮件传输的协议。SMTP是建立在FTP文件传输服务上…...

使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 ; List<Integer> evens new ArrayList…...

UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...

AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用
文章目录 一、背景知识:什么是 B-Tree 和 BTree? B-Tree(平衡多路查找树) BTree(B-Tree 的变种) 二、结构对比:一张图看懂 三、为什么 MySQL InnoDB 选择 BTree? 1. 范围查询更快 2…...