Java架構筆記:用JWT對SpringCloud進行認證和鑑權

JWT(JSON WEB TOKEN)是基於RFC 7519標準定義的一種可以安全傳輸的小巧和自包含的JSON對象。由於數據是使用數字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。

JWT通常由頭部(Header),負載(Payload),簽名(Signature)三個部分組成,中間以.號分隔,其格式爲Header.Payload.Signature

Header:聲明令牌的類型和使用的算法

alg:簽名的算法

typ:token的類型,比如JWT

Payload:也稱爲JWT Claims,包含用戶的一些信息

系統保留的聲明(Reserved claims):

  • iss (issuer):簽發人
  • exp (expiration time):過期時間
  • sub (subject):主題
  • aud (audience):受衆用戶
  • nbf (Not Before):在此之前不可用
  • iat (Issued At):簽發時間
  • jti (JWT ID):JWT唯一標識,能用於防止JWT重複使用

公共的聲明(public):

見 http://www.iana.org/assignments/jwt/jwt.xhtml

私有的聲明(private claims):

根據業務需要自己定義的數據

Signature:簽名

簽名格式:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

JWT的特點

  • JWT默認是不加密的,不能把用戶敏感類信息放在Payload部分。
  • JWT 不僅可以用於認證,也可以用於交換信息。
  • JWT的最大缺點是服務器不保存會話狀態,所以在使用期間不可能取消令牌或更改令牌的權限。
  • JWT本身包含認證信息,爲了減少盜用,JWT的有效期不宜設置太長。
  • 爲了減少盜用和竊取,JWT不建議使用HTTP協議來傳輸代碼,而是使用加密的HTTPS協議進行傳輸。

首次生成token比較慢,比較耗CPU,在高併發的情況下需要考慮CPU佔用問題。

生成的token比較長,可能需要考慮流量問題。

認證原理

客戶端向服務器申請授權,服務器認證以後,生成一個token字符串並返回給客戶端,此後客戶端在請求受保護的資源時攜帶這個token,服務端進行驗證再從這個token中解析出用戶的身份信息。

JWT的使用方式

一種做法是放在HTTP請求的頭信息Authorization字段裏面,格式如下:

Authorization: <token>

需要將服務器設置爲接受來自所有域的請求,用Access-Control-Allow-Origin: *

另一種做法是,跨域的時候,JWT就放在POST請求的數據體裏面。

對JWT實現token續簽的做法

1、額外生成一個refreshToken用於獲取新token,refreshToken需存儲於服務端,其過期時間比JWT的過期時間要稍長。

2、用戶攜帶refreshToken參數請求token刷新接口,服務端在判斷refreshToken未過期後,取出關聯的用戶信息和當前token。

3、使用當前用戶信息重新生成token,並將舊的token置於黑名單中,返回新的token。

創建用於登錄認證的工程auth-service

1、 創建pom.xml文件

Xml代碼

<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/maven-v4_0_0.xsd"> 
 <modelVersion>4.0.0</modelVersion> 
 <groupId>com.seasy.springcloud</groupId> 
 <artifactId>auth-service</artifactId> 
 <version>1.0.0</version> 
 <packaging>jar</packaging> 
 
 <parent> 
 <groupId>org.springframework.boot</groupId> 
 <artifactId>spring-boot-starter-parent</artifactId> 
 <version>2.0.8.RELEASE</version> 
 <relativePath/> 
 </parent> 
 
 <properties> 
 <java.version>1.8</java.version> 
 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 
 </properties> 
 
 <dependencies> 
 <dependency> 
 <groupId>org.springframework.boot</groupId> 
 <artifactId>spring-boot-starter-web</artifactId> 
 </dependency> 
 <dependency> 
 <groupId>org.springframework.boot</groupId> 
 <artifactId>spring-boot-starter-actuator</artifactId> 
 </dependency> 
 
 <!-- spring cloud --> 
 <dependency> 
 <groupId>org.springframework.cloud</groupId> 
 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 
 </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.7.0</version> 
 </dependency> 
 </dependencies> 
 
 <dependencyManagement> 
 <dependencies> 
 <dependency> 
 <groupId>org.springframework.cloud</groupId> 
 <artifactId>spring-cloud-dependencies</artifactId> 
 <version>Finchley.RELEASE</version> 
 <type>pom</type> 
 <scope>import</scope> 
 </dependency> 
 </dependencies> 
 </dependencyManagement> 
</project> 

2、JWT工具類

Java代碼

public class JWTUtil { 
 public static final String SECRET_KEY = "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){ 
 Date now = new Date(); 
 Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY); //算法 
 
 String token = JWT.create() 
 .withIssuer(ISSUER) //簽發人 
 .withIssuedAt(now) //簽發時間 
 .withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRE_TIME)) //過期時間 
 .withClaim("username", username) //保存身份標識 
 .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 ""; 
 } 
} 

3、LoginController類

Java代碼

@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)){ 
 //生成token 
 String token = JWTUtil.generateToken(username); 
 
 //生成refreshToken 
 String refreshToken = StringUtil.getUUIDString(); 
 
 //數據放入redis 
 redisTemplate.opsForHash().put(refreshToken, "token", token); 
 redisTemplate.opsForHash().put(refreshToken, "username", username); 
 
 //設置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"); 
 if(StringUtil.isEmpty(username)){ 
 return new AuthResult(1003, "refreshToken error"); 
 } 
 
 //生成新的token 
 String newToken = JWTUtil.generateToken(username); 
 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")); 
 } 
} 

4、application配置信息

Java代碼

spring.application.name=auth-service 
server.port=4040 
 
eureka.instance.hostname=${spring.cloud.client.ip-address} 
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port} 
eureka.instance.prefer-ip-address=true 
 
eureka.client.service-url.defaultZone=http://root:123456@${eureka.instance.hostname}:7001/eureka/ 
 
#redis 
spring.redis.database=0 
spring.redis.timeout=3000ms 
spring.redis.lettuce.pool.max-active=100 
spring.redis.lettuce.pool.max-wait=-1ms 
spring.redis.lettuce.pool.min-idle=0 
spring.redis.lettuce.pool.max-idle=8 
 
#standalone 
spring.redis.host=192.168.134.134 
spring.redis.port=7001 
 
#sentinel 
#spring.redis.sentinel.master=mymaster 
#spring.redis.sentinel.nodes=192.168.134.134:26379,192.168.134.134:26380 

5、啓動類

Java代碼

@SpringBootApplication 
@EnableEurekaClient 
public class Main{ 
 public static void main(String[] args){ 
 SpringApplication.run(Main.class, args); 
 } 
} 

改造SpringCloud Gateway工程

1、在pom.xml文件添加依賴

Xml代碼

<!-- 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.7.0</version> 
</dependency> 

2、創建全局過濾器JWTAuthFilter

Java代碼

@Component 
public class JWTAuthFilter implements GlobalFilter, Ordered{ 
 @Override 
 public int getOrder() { 
 return -100; 
 } 
 
 @Override 
 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 
 String url = exchange.getRequest().getURI().getPath(); 
 
 //忽略以下url請求 
 if(url.indexOf("/auth-service/") >= 0){ 
 return chain.filter(exchange); 
 } 
 
 //從請求頭中取得token 
 String token = exchange.getRequest().getHeaders().getFirst("Authorization"); 
 if(StringUtil.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是否在redis中存在 
 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)); 
 } 
 
 return chain.filter(exchange); 
 } 
} 

3、關鍵的application配置信息

Java代碼

spring: 
 application: 
 name: service-gateway 
 cloud: 
 gateway: 
 discovery: 
 locator: 
 enabled: true 
 lowerCaseServiceId: true 
 routes: 
 #認證服務路由 
 - id: auth-service 
 predicates: 
 - Path=/auth-service/** 
 uri: lb://auth-service 
 filters: 
 - StripPrefix=1 

最後針對上面的高頻常問面試題我還總結出了適合於1-5年以上開發經驗的java程序員面試涉及到的絕大部分面試題答案做成了文檔學習筆記文件以及架構視頻資料免費分享給大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術資料),希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習,也可以關注我一下以後會有更多幹貨分享。免費獲取方式:加入Java進階之路:878249276,私聊羣主或管理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章