文章目录
前言
我们已经接入了网关,所有的外部访问需要通过网关才能访问到我们的微服务,这一章我们在网关层进行统一的安全认证,保障服务安全和数据安全。
在前面的的【第3章】SpringBoot实战篇之登录接口(含JWT和拦截器) 已经实现了对用户身份的认证,思路是差不多的,我们这里通过网关层的过滤器来简单实现下。
一、公共模块
我们需要在在公共模块实现JWT以工具类的形式出现,方便其他模块的使用
1. 引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2. 工具类
package org.example.common.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static final String SECRET="167589447321";
public JwtUtils() {
}
/**
* 生成token
* @param claims 用户信息
* @return String
*/
public static String create(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withIssuer("auth0")
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证token
* @param token token
* @return boolean
*/
public static boolean verify(String token) {
DecodedJWT decodedJWT;
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// specify any specific claim validations
.withIssuer("auth0")
// reusable verifier instance
.build();
decodedJWT = verifier.verify(token);
} catch (JWTVerificationException exception){
// Invalid signature/claims
return false;
}
return true;
}
/**
* 解析token数据
* @param token token
* @return Map
*/
public static Map<String, Object> getClaims(String token) {
return JWT.require(Algorithm.HMAC256(SECRET))
.withIssuer("auth0")
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
二、用户服务
这里我们新增用户服务,用于管理用户信息和生成token信息
1. 新建模块
2. 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>base-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
3. 启动类
package org.example.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* Create by zjg on 2024/7/21
*/
@EnableDiscoveryClient
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
4. 基本配置
server:
port: 9007
spring:
application:
name: user-service
cloud:
nacos:
config:
file-extension: yaml
server-addr: ${NACOS_SERVER_ADDR}
namespace: ${NACOS_NAMESPACE}
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
shared-configs[0]:
data-id: base-discovery.yaml
group: DEFAULT_GROUP
refresh: true
5. 登录接口
package org.example.user.controller;
import org.example.common.util.JwtUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Create by zjg on 2024/7/21
*/
@RestController
@RequestMapping("/user/")
public class UserController {
@RequestMapping("login")
public String login(@RequestParam("username") String username,@RequestParam("password") String password){
String message="用户名/密码不正确";
String admin="admin";
if(admin.equals(username)&&admin.equals(password)){
Map<String, Object> claims=new HashMap<>();
claims.put("username",username);
return JwtUtils.create(claims);
}
return message;
}
}
三、网关服务
1. 引入依赖
<dependency>
<groupId>org.example</groupId>
<artifactId>base-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
2. 应用配置
spring:
cloud:
gateway:
routes:
- id: provider-service
uri: lb://provider-service
predicates:
- Path=/provider/**
- BlackRemoteAddr=192.168.1.1/24,127.0.0.1,192.168.0.102
- id: consumer-service
uri: lb://consumer-service
predicates:
- Path=/consumer/**
filters:
- BlackList=/consumer/hello1,/consumer/hello2
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
3. 自定义过滤器
package org.example.gateway.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.common.model.Result;
import org.example.common.util.JwtUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Create by zjg on 2024/7/21
*/
@Component
public class LoginGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String uri = request.getURI().getPath();
ObjectMapper objectMapper = new ObjectMapper();
if(uri.equals("/user/login")||uri.equals("/user/login/")){
MultiValueMap<String, String> queryParams = request.getQueryParams();
if(queryParams.containsKey("username")&&queryParams.containsKey("password")){
return chain.filter(exchange);
}else {
response.setStatusCode(HttpStatus.BAD_REQUEST);
Result result = Result.error(HttpStatus.BAD_REQUEST.value(), "登录失败", "用户名和密码不能为空!");
try {
return response.writeWith(Flux.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(result))));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
HttpHeaders headers = request.getHeaders();
String authorization = headers.getFirst("Authorization");
if(Boolean.FALSE.equals(StringUtils.hasText(authorization))||Boolean.FALSE.equals(JwtUtils.verify(authorization.startsWith("Bearer")?authorization.substring(authorization.indexOf("Bearer")+7):authorization))){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
Result result = Result.error(HttpStatus.UNAUTHORIZED.value(), "认证失败", "token验证失败,请重新获取token后重试!");
try {
return response.writeWith(Flux.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(result))));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.add("Source-Mark", "Z2F0ZXdheQ==")).build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
return -1;
}
}
order值越小,优先级越高
四、单元测试
1.介绍
需要访问一个登录接口,获取token之后访问提供者的接口
大体思路:
- 用户请求登录接口>网关>用户>公共模块
- 用户请求提供者接口>网关>公共模块>提供者
2. 登录接口
3. 提供者接口
3.1 无token
3.2 有token
总结
网关层面已经实现了服务安全认证,进一步增加了我们微服务程序的安全性和稳定性。