JWT(JSON WEB TOKEN)是目前最流行的认证解决方案。
一、创建用于登录认证的工程auth-service
1.1 pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.erbadagang.springcloud.gateway</groupId>
<artifactId>auth-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth-service</name>
<description>sc-auth-service project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.0.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2 JWT工具类
package com.erbadagang.springcloud.gateway.authservice.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
public class JWTUtil {
public static final String SECRET_KEY = "erbadagang-123456"; //秘钥
public static final long TOKEN_EXPIRE_TIME = 5 * 60 * 1000; //token过期时间
public static final long REFRESH_TOKEN_EXPIRE_TIME = 10 * 60 * 1000; //refreshToken过期时间
private static final String ISSUER = "issuer"; //签发人
/**
* 生成签名
*/
public static String generateToken(String username, String roles) {
Date now = new Date();
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY); //算法
String token = JWT.create()
.withIssuer(ISSUER) //签发人
.withIssuedAt(now) //签发时间
// .withSubject()
.withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRE_TIME)) //过期时间
.withClaim("username", username) //保存身份标识
.withClaim("roles", roles) //保存权限标识
.sign(algorithm);
return token;
}
/**
* 验证token
*/
public static boolean verify(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY); //算法
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.build();
verifier.verify(token);//如果校验有问题会抛出异常。
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
/**
* 从token获取username
*/
public static String getUsername(String token) {
try {
return JWT.decode(token).getClaim("username").asString();
} catch (Exception ex) {
ex.printStackTrace();
}
return "";
}
/**
* 从token获取roles
*/
public static String getRoles(String token) {
try {
return JWT.decode(token).getClaim("roles").asString();
} catch (Exception ex) {
ex.printStackTrace();
}
return "";
}
}
1.3 LoginController类
package com.erbadagang.springcloud.gateway.authservice.controller;
import com.erbadagang.springcloud.gateway.authservice.util.AuthResult;
import com.erbadagang.springcloud.gateway.authservice.util.JWTUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @description 登录后生成token,及刷新token。
* @ClassName: LoginController
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/31 21:39
* @Copyright:
*/
@RestController
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
/**
* 登录认证
*
* @param username 用户名
* @param password 密码
*/
@GetMapping("/login")
public AuthResult login(@RequestParam String username, @RequestParam String password) {
if ("admin".equals(username) && "admin".equals(password)) {
//查询出用户权限
String roles = "admin,query";
//生成token
String token = JWTUtil.generateToken(username, roles);
//生成refreshToken
String refreshToken = UUID.randomUUID().toString().replace("-", "");
//数据放入redis
redisTemplate.opsForHash().put(refreshToken, "token", token);
redisTemplate.opsForHash().put(refreshToken, "username", username);
redisTemplate.opsForHash().put(refreshToken, "roles", roles);
//设置token的过期时间
redisTemplate.expire(refreshToken, JWTUtil.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
return new AuthResult(0, "success", token, refreshToken);
} else {
return new AuthResult(1001, "username or password error");
}
}
/**
* 刷新token
*/
@GetMapping("/refreshToken")
public AuthResult refreshToken(@RequestParam String refreshToken) {
String username = (String) redisTemplate.opsForHash().get(refreshToken, "username");
String roles = (String) redisTemplate.opsForHash().get(refreshToken, "roles");
if (StringUtils.isEmpty(username)) {
return new AuthResult(1003, "refreshToken error");
}
//生成新的token
String newToken = JWTUtil.generateToken(username, roles);
redisTemplate.opsForHash().put(refreshToken, "token", newToken);
return new AuthResult(0, "success", newToken, refreshToken);
}
@GetMapping("/")
public String index() {
return "auth-service: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
返回信息包装类AuthResult
package com.erbadagang.springcloud.gateway.authservice.util;
import lombok.Data;
/**
* AuthResult作用是:
*
* @ClassName: AuthResult
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/31 21:04
* @Copyright:
*/
@Data
public class AuthResult {
private int code;
private String msg;
private String token;
private String refreshToken;
public AuthResult(int i, String s) {
this.code = i;
this.msg = s;
}
public AuthResult(int i, String success, String token, String refreshToken) {
this.code = i;
this.msg = success;
this.token = token;
this.refreshToken = refreshToken;
}
}
1.4 application配置信息
spring.application.name=auth-service
# 应用服务 WEB 访问端口
server.port=8080
#redis
spring.redis.database=0
spring.redis.timeout=3000
spring.redis.lettuce.pool.max-active=100
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-idle=8
#standalone
spring.redis.host=localhost
spring.redis.port=6379
1.5 启动类
package com.erbadagang.springcloud.gateway.authservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @description 启动类
* @ClassName: AuthServiceApplication
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/31 21:43
* @Copyright:
*/
@SpringBootApplication
public class AuthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args);
}
}
1.6 测试
启动redis和Springboot项目。
访问:http://localhost:8080/
,返回:auth-service: 2020-07-31 21:45:32
.
访问:http://localhost:8080/login?username=admin&password=admin
,返回:{"code":0,"msg":"success","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE1OTYyMDM0OTMsImlhdCI6MTU5NjIwMzE5MywidXNlcm5hbWUiOiJhZG1pbiJ9.UR69NUB2jZ9Hco-TYPLwjz0iig-GKqmvl5lWA0lOy6M","refreshToken":"64cfde875ccf457788ee707c1e3d8576"}
.
访问:http://localhost:8080/refreshToken?refreshToken=64cfde875ccf457788ee707c1e3d8576
,返回:{"code":0,"msg":"success","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE1OTYyMDM2NjIsImlhdCI6MTU5NjIwMzM2MiwidXNlcm5hbWUiOiJhZG1pbiJ9.j2ILPWjBAP72Z-oqMRV_3MhIsXW4cJchpH831sw35TE","refreshToken":"64cfde875ccf457788ee707c1e3d8576"}
.
二、改造SpringCloud Gateway工程
2.1 pom.xml文件添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.erbadagang.springcloud.gateway</groupId>
<artifactId>Gateway-JWT</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Gateway-JWT</name>
<description>gateway and shiro</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier><!-- jdk版本 -->
<exclusions>
<exclusion>
<artifactId>commons-lang</artifactId>
<groupId>commons-lang</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 创建全局过滤器JWTAuthFilter
package com.erbadagang.gateway.auth;
import com.erbadagang.gateway.dto.Response;
import lombok.Data;
import net.sf.json.JSONObject;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @description 过滤器,判断token是否合法,以及取出roles进行逻辑处理。
* @ClassName: JWTAuthFilter
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/31 23:27
* @Copyright:
*/
@Component
//读取 yml 文件下的 org.my.jwt
@ConfigurationProperties("org.my.jwt")
@Data
public class JWTAuthFilter implements GlobalFilter, Ordered {
private String[] skipAuthUrls;
@Override
public int getOrder() {
return -100;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
//跳过不需要验证的路径
if (null != skipAuthUrls && isSkipUrl(url)) {
return chain.filter(exchange);
}
//从请求头中取得token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isEmpty(token)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
Response res = new Response(401, "401 unauthorized");
byte[] responseByte = JSONObject.fromObject(res).toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(responseByte);
return response.writeWith(Flux.just(buffer));
}
//请求中的token是否有效
boolean verifyResult = JWTUtil.verify(token);
if (!verifyResult) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
Response res = new Response(1004, "invalid token");
byte[] responseByte = JSONObject.fromObject(res).toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(responseByte);
return response.writeWith(Flux.just(buffer));
} else {
String roles = JWTUtil.getRoles(token);
if (roles.indexOf("admin") >= 0) {//TODO 根据权限进行判断
System.out.println("roles = " + roles);
}
}
//如果各种判断都通过,执行chain上的其他业务逻辑
return chain.filter(exchange);
}
/**
* 判断当前访问的url是否开头URI是在配置的忽略url列表中
*
* @param url
* @return
*/
public boolean isSkipUrl(String url) {
for (String skipAuthUrl : skipAuthUrls) {
if (url.startsWith(skipAuthUrl)) {
return true;
}
}
return false;
}
}
Response
package com.erbadagang.gateway.dto;
import lombok.Data;
/**
* @description 返回数据包装类。
* @ClassName: Response
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/31 22:03
* @Copyright:
*/
@Data
public class Response {
private int code;
private String message;
private String data;
public Response() {
}
public Response(int code, String message) {
this.code = code;
this.message = message;
}
public Response(int code, String message, String data) {
this.code = code;
this.message = message;
this.data = data;
}
//省略getter、setter方法
//......
}
2.3 application配置信息
###################################
#服务启动端口的配置
###################################
server:
port: 8888
spring:
application:
name: my-gateway
cloud:
#################################
# gateway相关配置
#################################
gateway:
################################
# 配置允许跨域请求
################################
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
- GET
- POST
# 路由定义
routes:
- id: baidu
uri: https://www.baidu.com
predicates:
- Path=/baidu/**
filters:
- StripPrefix=1
# JWT的token生成及验证服务,可以对应到用户子服务(系统)的概念。
- id: auth-service
uri: http://localhost:8080/
predicates:
- Path=/auth-service/**
filters:
- StripPrefix=1
- id: sina
uri: https://www.sina.com.cn/
predicates:
- Path=/sina/**
filters:
- StripPrefix=1
org:
my:
jwt:
#跳过认证的路由
skip-auth-urls:
- /baidu
- /auth-service
2.4 测试
-
访问不需要认证的
baidu
,http://localhost:8888/baidu,正常调整到百度页面。
image.png 访问需要认证的
sina
,http://localhost:8888/sina,返回{"code":401,"data":"","message":"401 unauthorized"}
。生成token, 访问:http://localhost:8888/auth-service/login?username=admin&password=admin,返回
{
"code": 0,
"msg": "success",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6ImFkbWluLHF1ZXJ5IiwiaXNzIjoiaXNzdWVyIiwiZXhwIjoxNTk2MjEwMTE4LCJpYXQiOjE1OTYyMDk4MTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.yiAVGMJE8ILiA3h0YiS0ycxh4T6HMYsY6NwW2QmDrlc",
"refreshToken": "18335c70036a4f7795c170ab4252f1b6"
}
- 再访问需要认证的
sina
,http://localhost:8888/sina,本次通过postman,添加header信息Authorization
,值为刚才获取的token,可以正常获取到sina首页html。
带token访问
底线
本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址
下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。