??可能有些人會(huì)覺得這篇似曾相識(shí),沒錯(cuò),這篇是由原文章進(jìn)行二次開發(fā)的。
前陣子有些事情,但最近看到評(píng)論區(qū)說原文章最后實(shí)現(xiàn)的是單模塊的驗(yàn)證,由于過去太久也懶得驗(yàn)證,所以重新寫了一個(gè)完整的可以跑得動(dòng)的一個(gè)。
OK,回到正題,以下是真正對(duì)應(yīng)的微服務(wù)多模塊的一個(gè)方法,使用到的技術(shù)有:基于微服務(wù)的Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT。
對(duì)使用到的微服務(wù)技術(shù)進(jìn)行在項(xiàng)目中的說明:
Security:負(fù)責(zé)登錄驗(yàn)證(文章中沒有實(shí)現(xiàn)授權(quán),在過濾器中直接返回null,如果想實(shí)現(xiàn)授權(quán),可以在返回null的地方添加授權(quán)信息類似ROLE_ADMIN,同時(shí)在Security的配置文件那里添加授權(quán)信息即可)。
Redis:負(fù)責(zé)緩存token跟用戶數(shù)據(jù)。
Gateway:對(duì)前端提供的接口,由它多個(gè)模塊進(jìn)行接口調(diào)用。
OpenFeign:提供給security查詢數(shù)據(jù)庫(kù)中的用戶信息。
Nacos:注冊(cè)服務(wù)中心,注冊(cè)服務(wù)的信息,使OpenFeign可以調(diào)用其他服務(wù)模塊。
注意:雖然是原文章的二次編寫,但是很多都不同,建議直接跟著這篇走。
目錄
1.項(xiàng)目結(jié)構(gòu)
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 目錄結(jié)構(gòu)?編輯
? 4.2 service_user模塊
? ? 4.2.1 pom.xml
? ? 4.2.2?application.yml
? ? 4.2.3 Service_UserApp啟動(dòng)類
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.測(cè)試
1.項(xiàng)目結(jié)構(gòu)
涉及的模塊有
(1)common(Redis配置文件、Redis工具、Token工具、返回給前端信息的工具;即如下文件RedisConfig、RedisUtil、TokenUtil、ResponseUtil);
(2)gateway;
(3)model(實(shí)體類,F(xiàn)eign的客戶端);
(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在業(yè)務(wù)邏輯處理層上不出錯(cuò),緩存序列化問題
* */
@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解析任意的對(duì)象(Object),變成json序列化
Jackson2JsonRedisSerializer<Object> serializer=new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper mapper=new ObjectMapper(); //用ObjectMapper進(jìn)行轉(zhuǎn)義
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//該方法是指定序列化輸入的類型,就是將數(shù)據(jù)庫(kù)里的數(shù)據(jù)按照一定類型存儲(chǔ)到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 //在項(xiàng)目啟動(dòng)的時(shí)候執(zhí)行該方法,也可以理解為在spring容器初始化的時(shí)候執(zhí)行該方法。
public void initStringRedisTemplate(){
stringRedisTemplateStatic=this.stringRedisTemplate;
}
private static final DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/*
* 保存token信息到redis,也可直接在創(chuàng)建token中使用該方法
* */
public static void redis_SaveTokenInfo(String token,String username){
//以u(píng)sername做key
LocalDateTime localDateTime=LocalDateTime.now();
stringRedisTemplateStatic.opsForHash().put(username,"token",token);
stringRedisTemplateStatic.opsForHash().put(username,"refreshTime", //有效時(shí)間
df.format(localDateTime.plus(7*24*60*60*1000, ChronoUnit.MILLIS)));
stringRedisTemplateStatic.opsForHash().put(username,"expiration", //過期時(shí)間 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="操作失敗,請(qǐng)稍候重試。";
//返回碼(200)
private int code;
//返回消息
private String message;
@ApiModelProperty(value = "返回?cái)?shù)據(jù)(單條或多條)")
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;
}
//對(duì)response寫入Object數(shù)據(jù)
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()) //簽發(fā)時(shí)間
.setIssuer("Lino_white") //簽發(fā)者
.setExpiration(new Date(System.currentTimeMillis() + 300* 1000)) //過期時(shí)間 5分鐘 自行設(shè)置
.signWith(SignatureAlgorithm.HS256, APP_SECRET) //簽名算法跟密鑰
.claim("identity", user.getIdentity()) //可添加額外的屬性
.compact();
return token;
}
//重新生成新的Token,異常時(shí)間由傳入的參數(shù)決定
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()) //簽發(fā)時(shí)間
.setIssuer("Lino_white") //簽發(fā)者
.setExpiration(expirationTime) //過期時(shí)間
.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) // 設(shè)置標(biāo)識(shí)名
.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)//是否有驗(yàn)證,有就打開
.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 = "實(shí)體:用戶")
@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 目錄結(jié)構(gòu)
? 4.2 service_user模塊
? ? 4.2.1 pom.xml
注意:service_user接入了security模塊。
<!--openfeign 遠(yuǎn)程接口調(diào)用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--nacos 注冊(cè)中心-->
<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文檔實(shí)現(xiàn)接入-->
<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啟動(dòng)類
@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控制器
在任意一個(gè)控制器中,添加如下代碼,該接口將用于OpenFeign的遠(yuǎn)程接口調(diào)用,由security模塊中的自定義類MyUserDetailService去進(jìn)行調(diào)用。(MyUserDetailService的代碼在介紹security模塊中會(huì)出現(xiàn))
@GetMapping("/api/user/{username}")
public User findUserByName(@PathVariable("username") String username){
User userByName = userService.findUserByName(username);
return userByName;
}
4.3?其他service模塊
對(duì)于其他模塊,相對(duì)應(yīng)跟service_user模塊一樣,進(jìn)行如下操作即可:
(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
????????防止出現(xiàn)運(yùn)行異常報(bào)錯(cuò)信息,對(duì)于同一個(gè)服務(wù)的FeignClient來說,配置該屬性不會(huì)造成覆蓋,詳情可以查看該文章: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 {
//用戶權(quán)限列表
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)
注意:前面將對(duì)service_user的遠(yuǎn)程接口定義為/api/user/{username},所以在過濾方面要放行該路徑,否則security無法調(diào)用數(shù)據(jù)庫(kù)查詢用戶信息,導(dǎo)致程序報(bào)錯(cuò)。
對(duì)此,可以查看該文章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) //啟動(dòng)方法級(jí)別的權(quán)限認(rèn)證
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailService myUserDetailService;
@Bean
//配置密碼加密器
public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
//配置哪些請(qǐng)求不攔截
//TODO 將需要Feign的方法前綴都用上api,得到api/select/user/{user_id}這樣的路徑不受限制
// 由于api路徑是由服務(wù)模塊自己去調(diào)用的,所以gateway不用做路徑請(qǐng)求的處理
@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()
//該過濾器設(shè)置在用戶名、密碼、權(quán)限過濾器之前。這樣每次訪問接口都會(huì)經(jīng)過此過濾器,我們可以獲取請(qǐng)求路徑,并判定當(dāng)請(qǐng)求路徑為/login時(shí)進(jìn)入驗(yàn)證碼驗(yàn)證流程。
// 使用jwt的Authentication,來解析過來的請(qǐng)求是否有token
.addFilterBefore(new TokenOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class)
//登錄后,訪問沒有權(quán)限處理類
.exceptionHandling().accessDeniedHandler(new NothingAccessDeniedHandler())
//匿名訪問,沒有權(quán)限的處理類
.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對(duì)于過期時(shí)間的定義不同。
token過期時(shí)間為3分鐘,redis上存儲(chǔ)的異常時(shí)間為5分鐘,并且redis上存儲(chǔ)的刷新時(shí)間為7天
在下面的配置文件中,僅僅對(duì)token進(jìn)行分析而已,可以根據(jù)需要在這里做驗(yàn)證碼校驗(yàn)。
token的過期時(shí)間在以下代碼中是這樣做的,當(dāng)token過期時(shí)間3分鐘到了,判斷redis上存儲(chǔ)的異常時(shí)間是否到了5分鐘,沒到5分鐘就返回一個(gè)新的token給前端,前端拿到該token就可以繼續(xù)訪問;如果到了5分鐘,則會(huì)停止訪問并通知前端 “用戶已經(jīng)過期,請(qǐng)重新登錄”。
小編有個(gè)想法(還沒做):在這里可以重新定義過期時(shí)間,比如用戶每次訪問時(shí)都進(jìn)行判斷:當(dāng)token的過期時(shí)間小于1分鐘后就刷新redis的異常時(shí)間,這樣可以使當(dāng)token要過期時(shí),就有新的token出現(xiàn),但這樣操作也存在缺點(diǎn):就是要消耗內(nèi)存資源,每次都得去讀取token是否臨近過期時(shí)間了。對(duì)于這塊,可以針對(duì)自己的情況去做調(diào)整。
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;
/**
* 在用戶名、密碼、權(quán)限過濾器之前的過濾器
* 在請(qǐng)求過來的時(shí)候,解析請(qǐng)求頭中的token,再解析token得到用戶信息,再存到SecurityContextHolder中
*
* TODO 下面過濾器僅做了針對(duì)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 可在這里判斷請(qǐng)求過來的路徑是否為login,方式為post,來在這里進(jìn)行驗(yàn)證碼有效驗(yàn)證
* 驗(yàn)證成功則直接chain(request,response)繼續(xù)走過濾
* */
String requestURI = request.getRequestURI();
System.out.println("開始請(qǐng)求,請(qǐng)求路徑:"+requestURI+" 請(qǐng)求方式:"+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可能時(shí)間已過,需要重新創(chuàng)建一個(gè)token
//先對(duì)比redis中的過期時(shí)間,redis的過期時(shí)間隨著用戶的操作而更新,token可能沒有及時(shí)更新
//判斷是否一樣,一樣的話就是token失效了,跳轉(zhuǎn)重新登錄,
// 不一樣就是redis過期時(shí)間更新了,生成新的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瀏覽器過期時(shí)間:" + tokenExpirationTime);
System.out.println("redis過期時(shí)間:" + expiration);
//
/*
* redis<token || token=redis || redis <now 則token失效,跳轉(zhuǎn)登錄
* token<redis
* */
if (expirationDate_redisTime.getTime() < expirationDate_tokenTime.getTime() ||
expirationDate_tokenTime.getTime() == expirationDate_redisTime.getTime() ||
expirationDate_redisTime.getTime() < nowTime.getTime()) {
//時(shí)間相同,跳轉(zhuǎn)登錄
ResponseUtil.reponseOutDiy(response, 401, "用戶已過期,請(qǐng)重新登錄");
System.out.println("*********Token過期(End)失效***********");
return;
} else {
//時(shí)間不同,生成新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);
}
//通過數(shù)據(jù)庫(kù)查詢數(shù)據(jù),創(chuàng)建token
System.out.println("這里之前開始的時(shí)間:" + 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后過期時(shí)間:" + expiration1);
System.out.println("—————————————————End—————————————————————");
ResponseUtil.reponseOutDiy(response, 200, token);
System.out.println("*********Token過期(End)新Token***********");
return;
}
} else {
//TODO 新增,如果redis沒有username,說明未登錄
throw new RuntimeException("未登錄");
}
}
}
//避免每次請(qǐng)求都請(qǐng)求數(shù)據(jù)庫(kù)查詢用戶信息,從緩存中查詢
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 未修改 這里的權(quán)限先空著
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;
/**
* 匿名未登錄的時(shí)候訪問,需要登錄的資源的調(diào)用類
* @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("當(dāng)前未登錄,無法訪問 ::"+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,"當(dāng)前未登錄,無法訪問");
}
}
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;
/**
* 登錄賬號(hào)密碼錯(cuò)誤等情況下,會(huì)調(diào)用的處理類
* @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("認(rèn)證失敗————————————");
ResponseUtil.reponseOutDiy(httpServletResponse,401,"登錄失敗,請(qǐng)重試");
}
}
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的作用:
* 登錄成功處理類,登錄成功后會(huì)調(diào)用里面的方法
* @author: white文
* @time: 2023/5/18 16:02
*/
@Slf4j
@Component
public class LoginInSuccessHandler implements AuthenticationSuccessHandler {
/**
* 用戶通過TokenLoginFilter(UsernamePasswordAuthenticationFilter)后,
* 驗(yàn)證成功到這里進(jìn)行
* 1.獲取當(dāng)前用戶
* 2.token創(chuàng)建
* 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格式進(jìn)行提示
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;
/**
* 沒有權(quán)限,被拒絕訪問時(shí)的調(diào)用類
* @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("沒有權(quán)限");
ResponseUtil.reponseOutDiy(httpServletResponse,403,"當(dāng)前您沒有該權(quán)限");
}
}
5.10?MyUserDetailService(UserDetailsService)
注意:在這里調(diào)用了model模塊中的UserFeign文件,實(shí)現(xiàn)讀取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;
/**
* 從數(shù)據(jù)庫(kù)讀取用戶信息(用戶名,密碼,身份)進(jìn)行身份認(rèn)證
*/
@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("數(shù)據(jù)庫(kù)的username:"+user.getUsername());
//根據(jù)當(dāng)前用戶名查詢用戶權(quán)限
List<String> authorities=new ArrayList<>();
authorities.add("ROLE_"+user.getIdentity());
DiyUserDetails details=new DiyUserDetails();
BeanUtils.copyProperties(user,details);
details.setAuthorities(authorities);
//如果數(shù)據(jù)庫(kù)密碼無加密,用下列
//details.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
System.out.println("********結(jié)束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.測(cè)試
這里是使用postman工具進(jìn)行測(cè)試的,在這里之前已經(jīng)在數(shù)據(jù)庫(kù)有用戶名跟密碼都為111的數(shù)據(jù),并且密碼已是加密形式。
1.首次訪問/admin/findAll,gateway會(huì)調(diào)用到8001端口下的user模塊,如下圖
?首次訪問/course/findAll,gateway會(huì)調(diào)用到8002端口下的course模塊,如下圖
2.POST訪問/login,并且提供相關(guān)參數(shù)(數(shù)據(jù)庫(kù)存在用戶111和加密過的密碼111),得到token,如下圖:
這時(shí),redis數(shù)據(jù)庫(kù)就有了用戶名為 user的數(shù)據(jù)?
3.復(fù)制剛才返回給前端的token,在Authorization的Type中,選擇Bearer Token,粘貼上剛才的Token,點(diǎn)擊Send發(fā)送
?5.再次請(qǐng)求8002端口下的source模塊
?再次請(qǐng)求8001端口下的user模塊
6.當(dāng)token過期后,redis中的異常時(shí)間還沒到,則會(huì)返回給前端一個(gè)新的token,拿著新token繼續(xù)請(qǐng)求即可。
7.當(dāng)token過期并且redis的異常時(shí)間也過了之后,用戶就需要重新登錄。
8.退出則為/logout 。
??同時(shí),redis數(shù)據(jù)庫(kù)中用戶名為user的key值也被刪除掉。文章來源:http://www.zghlxwxcb.cn/news/detail-451827.html
至此,結(jié)束!祝大家520快樂?。?!文章來源地址http://www.zghlxwxcb.cn/news/detail-451827.html
到了這里,關(guān)于微服務(wù)多模塊:Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT (附源碼)僅需一招,520徹底拿捏你的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!