SpringBoot系列之前後端介面安全技術JWT
阿新 • • 發佈:2020-07-10
@[TOC](SpringBoot系列之前後端介面安全技術JWT)
## 1. 什麼是JWT?
[JWT](https://jwt.io/introduction/)的全稱為Json Web Token (JWT),是目前最流行的跨域認證解決方案,是在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準((RFC 7519),JWT 是一種JSON風格的輕量級的授權和身份認證規範,可實現無狀態、分散式的Web應用授權
引用官方的說法是:
>JSON Web令牌(JWT)是一個開放標準(RFC 7519),它定義了一種緊湊且自包含的方式,用於在各方之間安全地將資訊作為JSON物件傳輸。由於此資訊是經過數字簽名的,因此可以進行驗證和信任。可以使用祕密(使用HMAC演算法)或使用RSA或ECDSA的公鑰/私鑰對對JWT進行簽名。
引用官網圖片,JWT生成的token格式如圖:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200710111831998.png)
## 2. JWT令牌結構怎麼樣?
JSON Web令牌以緊湊的形式由三部分組成,這些部分由點(.)分隔,分別是:
* 標頭(Header)
* 有效載荷(Playload)
* 簽名(Signature)
因此,JWT通常如下所示。
```xxxxx.yyyyy.zzzzz```
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200710111910454.png)
ok,詳細介紹一下這3部分組成
### 2.1 標頭(Header)
標頭通常由兩部分組成:令牌的型別(即JWT)和所使用的簽名演算法,例如HMAC SHA256或RSA。
* 宣告型別,這裡是JWT
* 加密演算法,自定義
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
然後進行Base64Url編碼得到jwt的第1部分
> Base64是一種基於64個可列印字元來表示二進位制資料的表示方法。由於2
的6次方等於64,所以每6個位元為一個單元,對應某個可列印字元。三個位元組有24
個位元,對應於4個Base64單元,即3個位元組需要用4個可列印字元來表示。JDK 中 提
供了非常方便的 B BA AS SE E6 64 4E En nc co od de er r和B BA AS SE E6 64 4D De ec co od de er r,用它們可以非常方便的完
成基於 BASE64 的編碼和解碼
### 2.2 有效載荷(Playload)
載荷就是存放有效資訊的地方。這個名字像是特指飛機上承載的貨品,這些有效資訊包
含三個部分:
* (1)標準中註冊的宣告
* iss (issuer):表示簽發人
* exp (expiration time):表示token過期時間
* sub (subject):主題
* aud (audience):受眾
* nbf (Not Before):生效時間
* iat (Issued At):簽發時間
* jti (JWT ID):編號
* (2)公共的宣告
公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊
* (3)私有的宣告
私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64是對稱解密的,意味著該部分資訊可以歸類為明文資訊。這些私有的宣告其實一般就是指自定義Claim
定義一個payload:
```json
{
"user_id":1,
"user_name":"nicky",
"scope":[
"ROLE_ADMIN"
],
"non_expired":false,
"exp":1594352348,
"iat":1594348748,
"enabled":true,
"non_locked":false
}
```
對其進行base64加密,得到payload:
```
eyJ1c2VyX2lkIjoxLCJ1c2VyX25hbWUiOiJuaWNreSIsInNjb3BlIjpbIlJPTEVfQURNSU4iXSwibm9uX2V4cGlyZWQiOmZhbHNlLCJleHAiOjE1OTQzNTIzNDgsImlhdCI6MTU5NDM0ODc0OCwiZW5hYmxlZCI6dHJ1ZSwibm9uX2xvY2tlZCI6ZmFsc2V9
```
### 2.3 簽名(Signature)
jwt的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:
* header (base64後的)
* payload (base64後的)
* secret
簽名,是整個資料的認證資訊。一般根據前兩步的資料,然後通過header中宣告的加密方式進行加鹽secret組合加密,然後就構成了jwt的第3部分
ok,一個jwt令牌的組成就介紹好咯,令牌是三個由點分隔的Base64-URL字串,可以在HTML和HTTP環境中輕鬆傳遞這些字串,與基於XML的標準(例如SAML)相比,它更緊湊。
下圖顯示了一個JWT,它已對先前的標頭和有效負載進行了編碼,並用一個祕密secret進行了簽名編碼的JWT:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200710113256890.png)
JWT官網提供的線上除錯工具:
[https://jwt.io/#debugger-io](https://jwt.io/#debugger-io)
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200709174413497.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70)
開源中國提供的base64線上加解密:
[https://tool.oschina.net/encrypt?type=3](https://tool.oschina.net/encrypt?type=3)
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200709165440434.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70)
## 3. JWT原理簡單介紹
引用官網的圖,用於顯示如何獲取JWT,並將其用於訪問API或資源:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200710114332135.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70)
* 1、客戶端(包括瀏覽器、APP等)向授權伺服器請求授權
* 2、授權伺服器驗證通過,授權伺服器會嚮應用程式返回訪問令牌
* 3、該應用程式使用訪問令牌來訪問受保護的資源(例如API)
## 4. JWT的應用場景
JWT 使用於比較小型的業務驗證,對於比較複雜的可以用OAuth2.0實現
引用官方的說法:
> * 授權:這是使用JWT的最常見方案。一旦使用者登入,每個後續請求將包括JWT,從而允許使用者訪問該令牌允許的路由,服務和資源。單一登入是當今廣泛使用JWT的一項功能,因為它的開銷很小並且可以在不同的域中輕鬆使用。
> * 資訊交換:JSON Web令牌是在各方之間安全地傳輸資訊的好方法。因為可以對JWT進行簽名(例如,使用公鑰/私鑰對),所以您可以確保發件人是他們所說的人。此外,由於簽名是使用標頭和有效負載計算的,因此您還可以驗證內容是否遭到篡改。
## 5. 與Cookie-Session對比
瞭解JWT之前先要了解傳統的Cookie-Session認證機制,這是單體應用最常用的,其大概流程:
* 1、使用者訪問客戶端(瀏覽器),伺服器通過session校驗使用者是否登入
* 2、 使用者沒登入返回登入頁面,輸入賬號密碼等驗證
* 3、 驗證通過建立session,返回sessionId給客戶端儲存到cookie
* 4、接著,使用者訪問其它同域連結,都會校驗sessionId,符合就允許訪問
ok,簡單介紹這套cookie-session機制,之前設計者開發這套機制是為了相容http的無狀態,這套機制有其優點,當然也有一些缺陷:
* 只適用於B/S架構的軟體,對於安卓app等客戶端不帶cookie的,不能和服務端進行對接
* 不支援跨域,因為Cookie為了保證安全性,只能允許同域訪問,不支援跨域
* CSRF攻擊,Cookie沒做好安全保證,有時候容易被竊取,受到跨站請求偽造的攻擊
ok,簡單介紹了cookie-session機制後,可以介紹一下jwt的認證
* 1、使用者訪問客戶端(瀏覽器、APP等等),伺服器通過token校驗
* 2、 使用者沒登入返回登入頁面,輸入賬號密碼等驗證
* 3、 驗證通過建立已簽名token,返回token給客戶端儲存,最常見的是儲存在localStorage中,但是也可以存在Session Storage和Cookie中
* 4、接著,使用者訪問其它連結,都會帶上token,伺服器解碼JWT,如果Token是有效的則處理這個請求
網上對於cookie-session機制和jwt的討論很多,可以自行網上找資料,我覺得這兩套機制各有優點,應該根據場景進行選用,JWT最明顯優點就是小巧輕便,安全性也比較好,但是也有其缺點。
* 比如對於業務繁雜的功能,如果一些資訊也丟在jwt的token裡,cookie有可能不能儲存。
* 續簽問題,jwt不能支援,傳統的cookie+session的方案天然的支援續簽,但是jwt由於服務端不儲存使用者狀態,因此很難完美解決續簽問題
* 密碼重置等問題,jwt因為資料不保存於服務端,如果使用者修改密碼,不過token還沒過期,這種情況,原來的token還是可以訪問系統的,這種肯定是不允許的,不過這種情況或許可以通過修改secret實現
## 6. Java的JJWT實現JWT
### 6.1 什麼是JJWT?
[JJWT](https://github.com/jwtk/jjwt)是一個提供端到端的JWT建立和驗證的Java庫。永遠免費和開源(Apache
License,版本2.0),JJWT很容易使用和理解。它被設計成一個以建築為中心的流暢界
面,隱藏了它的大部分複雜性。
### 6.2 實驗環境準備
環境準備:
* Maven 3.0+
* IntelliJ IDEA
技術棧:
* SpringBoot2.2.1
* Spring Security
新建一個SpringBoot專案,maven加入JJWT相關配置
```xml
io.jsonwebtoken
jjwt
${jjwt.version}
com.auth0
java-jwt
${java.jwt.version}
```
pom.xml:
```xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
com.example.springboot
springboot-jwt
0.0.1-SNAPSHOT
springboot-jwt
Demo project for Spring Boot
1.8
0.9.0
3.4.0
2.1.1
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
io.jsonwebtoken
jjwt
${jjwt.version}
com.auth0
java-jwt
${java.jwt.version}
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.springboot.version}
mysql
mysql-connector-java
5.1.27
runtime
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.security
spring-security-test
test
com.alibaba
fastjson
1.2.47
compile
org.springframework.boot
spring-boot-maven-plugin
```
application.yml:
```yaml
spring:
datasource:
url: jdbc:mysql://192.168.0.152:33306/jeeplatform?autoReconnect=true&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false
username: root
password: minstone
driver-class-name: com.mysql.jdbc.Driver
#新增Thymeleaf配置,除了cache在專案沒上線前建議關了,其它配置都可以不用配的,本部落格只是列舉一下有這些配置
thymeleaf:
# cache預設開啟的,這裡可以關了,專案上線之前,專案上線後可以開啟
cache: false
# 這個prefix可以註釋,因為預設就是templates的,您可以改成其它的自定義路徑
prefix: classpath:/templates/
suffix: .html
mode: HTML5
# 指定一下編碼為utf8
encoding: UTF-8
# context-type為text/html,也可以不指定,因為boot可以自動識別
servlet:
content-type: text/html
messages:
basename: i18n.messages
# cache-duration:
encoding: UTF-8
logging:
level:
org:
springframework:
security: DEBUG
com:
example:
springboot:
jwt:
mapper: DEBUG
```
專案工程:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200709165743210.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70)
### 6.3 jwt配置屬性讀取
新建jwt.yml:
```yaml
# jwt configuration
jwt:
# 存放Token的Header key值
token-key: Authorization
# 自定義金鑰,加鹽
secret: mySecret
# 超時時間 單位秒
expiration: 3600
# 自定義token 字首字元
token-prefix: Bearer-
# accessToken超時時間 單位秒
access-token: 3600
# 重新整理token時間 單位秒
refresh-token: 3600
# 允許訪問的uri
permit-all: /oauth/**,/login/**,/logout/**
# 需要校驗的uri
authenticate-uri: /api/**
```
JWTProperties .java
```java
package com.example.springboot.jwt.configuration;
import com.example.springboot.jwt.core.io.support.YamlPropertyResourceFactory;
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* > yamlSources = new YamlPropertySourceLoader().load(resourceName, encodedResource.getResource());
return yamlSources.get(0);
} else {
//返回預設的PropertySourceFactory
return new DefaultPropertySourceFactory().createPropertySource(name, encodedResource);
}
}
}
```
### 6.4 JWT Token工具類
```java
package com.example.springboot.jwt.core.jwt.util;
import com.alibaba.fastjson.JSON;
import com.example.springboot.jwt.configuration.JWTProperties;
import com.example.springboot.jwt.core.jwt.userdetails.JWTUserDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.*;
/**
* claims = generateClaims(user);
return generateToken(user.getUsername(),claims);
}
/**
* 生成acceptToken
* @param username
* @param claims
* @return
*/
public String generateToken(String username, Map claims) {
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setSubject(username)
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(generateExpirationDate(jwtProperties.getExpiration().toMillis()))
.signWith(SIGNATURE_ALGORITHM, jwtProperties.getSecret())
.compact();
}
/**
* 校驗acceptToken
* @param token
* @param userDetails
* @return
*/
public boolean validateToken(String token, UserDetails userDetails) {
JWTUserDetails user = (JWTUserDetails) userDetails;
return validateToken(token, user.getUsername());
}
/**
* 校驗acceptToken
* @param token
* @param username
* @return
*/
public boolean validateToken(String token, String username) {
try {
final String userId = getUserIdFromClaims(token);
return getClaimsFromToken(token) != null
&& userId.equals(username)
&& !isTokenExpired(token);
} catch (Exception e) {
throw new IllegalStateException("Invalid Token!"+e);
}
}
/**
* 校驗acceptToken
* @param token
* @return
*/
public boolean validateToken(String token) {
try {
return getClaimsFromToken(token) != null
&& !isTokenExpired(token);
} catch (Exception e) {
throw new IllegalStateException("Invalid Token!"+e);
}
}
/**
* 解析token 資訊
* @param token
* @return
*/
public Claims getClaimsFromToken(String token){
Claims claims = Jwts.parser()
.setSigningKey(jwtProperties.getSecret())
.parseClaimsJws(token)
.getBody();
return claims;
}
/**
* 從token獲取userId
* @param token
* @return
*/
public String getUserIdFromClaims(String token) {
String userId = getClaimsFromToken(token).getId();
return userId;
}
/**
* 從token獲取ExpirationDate
* @param token
* @return
*/
public Date getExpirationDateFromClaims(String token) {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration;
}
/**
* 從token獲取username
* @param token
* @return
*/
public String getUsernameFromClaims(String token) {
return getClaimsFromToken(token).get(CLAIM_KEY_USER_NAME).toString();
}
/**
* token 是否過期
* @param token
* @return
*/
public boolean isTokenExpired(String token) {
final Date expirationDate = getExpirationDateFromClaims(token);
return expirationDate.before(new Date());
}
/**
* 生成失效時間
* @param expiration
* @return
*/
public Date generateExpirationDate(long expiration) {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 生成Claims
* @Param user
* @return
*/
public Map generateClaims(JWTUserDetails user) {
Map claims = new HashMap<>(16);
claims.put(CLAIM_KEY_USER_ID, user.getUserId());
claims.put(CLAIM_KEY_USER_NAME, user.getUsername());
claims.put(CLAIM_KEY_ACCOUNT_ENABLED, user.isEnabled());
claims.put(CLAIM_KEY_ACCOUNT_NON_LOCKED, user.isAccountNonLocked());
claims.put(CLAIM_KEY_ACCOUNT_NON_EXPIRED, user.isAccountNonExpired());
if (!CollectionUtils.isEmpty(user.getAuthorities())) {
claims.put(CLAIM_KEY_AUTHORITIES , JSON.toJSON(getAuthorities(user.getAuthorities())));
}
return claims;
}
/**
* 獲取角色許可權
* @param authorities
* @return
*/
public List getAuthorities(Collection extends GrantedAuthority> authorities){
List list = new ArrayList<>();
for (GrantedAuthority ga : authorities) {
list.add(ga.getAuthority());
}
return list;
}
}
```
### 6.5 Spring Security引入
自定義UserDetails:
```java
package com.example.springboot.jwt.core.jwt.userdetails;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
/**
* mapToGrantedAuthorities) {
this.userId = id;
this.username = username;
this.password = password;
this.authorities = mapToGrantedAuthorities;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return isEnabled;
}
}
```
UserDetailsServiceImpl.java業務介面
```java
package com.example.springboot.jwt.service;
import com.example.springboot.jwt.core.jwt.userdetails.JWTUserDetails;
import com.example.springboot.jwt.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Service;
import java.util.Arrays;
import java.util.List;
/**
* getAuthority() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
}
```
自定義AuthenticationEntryPoint進行統一異常處理:
```java
package com.example.springboot.jwt.web.handler;
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;
import java.io.Serializable;
/**
* URI_CACHE_MAP = new ConcurrentHashMap();
private final List permitAllUris;
private final List authenticateUris;
@Autowired
JWTProperties jwtProperties;
@Autowired
JWTTokenUtil jwtTokenUtil;
@Autowired
@Qualifier("jwtUserService")
UserDetailsService userDetailsService;
public JWTAuthenticationTokenFilter(JWTProperties jwtProperties) {
this.permitAllUris = Arrays.asList(jwtProperties.getPermitAll().split(","));
this.authenticateUris = Arrays.asList(jwtProperties.getAuthenticateUri().split(","));
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
if (!isAllowUri(httpServletRequest)) {
final String _authHeader = httpServletRequest.getHeader(jwtProperties.getTokenKey());
log.info("Authorization:[{}]",_authHeader);
if (StringUtils.isEmpty(_authHeader) || ! _authHeader.startsWith(jwtProperties.getTokenPrefix())) {
throw new RuntimeException("Unable to get JWT Token");
}
final String token = _authHeader.substring(7);
log.info("acceptToken:[{}]",token);
if (!jwtTokenUtil.validateToken(token)) {
throw new RuntimeException("Invalid token");
}
if (jwtTokenUtil.validateToken(token)) {
String username = jwtTokenUtil.getUsernameFromClaims(token);
JWTUserDetails userDetails = (JWTUserDetails)userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private Boolean isAllowUri(HttpServletRequest request) {
String uri = request.getServletPath();
if (URI_CACHE_MAP.containsKey(uri)) {
// 快取有資料,直接從快取讀取
return URI_CACHE_MAP.get(uri);
}
boolean flag = checkRequestUri(uri);
// 資料丟到快取裡
URI_CACHE_MAP.putIfAbsent(uri, flag);
return flag;
}
private Boolean checkRequestUri(String requestUri) {
boolean filter = true;
final PathMatcher pathMatcher = new AntPathMatcher();
for (String permitUri : permitAllUris) {
if (pathMatcher.match(permitUri, requestUri)) {
// permit all的連結直接放過
filter = true;
}
}
for (String authUri : authenticateUris) {
if (pathMatcher.match(authUri, requestUri)) {
filter = false;
}
}
return filter;
}
}
```
WebMvcConfigurer類註冊過濾器:
```java
package com.example.springboot.jwt.configuration;
import com.example.springboot.jwt.web.filter.JWTAuthenticationTokenFilter;
import com.example.springboot.jwt.web.handler.SecurityHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Signin Template for Bootstrap
* JWT配置類 ** *
* @author nicky.ma * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/06 11:37 修改內容: **/ @Component @PropertySource(value = "classpath:jwt.yml",encoding = "utf-8",factory = YamlPropertyResourceFactory.class) @ConfigurationProperties(prefix = "jwt") @Data @ToString public class JWTProperties { /** * 存放Token的Header key值 */ private String tokenKey; /* * 自定義金鑰,加鹽 */ private String secret; /* * 超時時間 單位秒 */ private Duration expiration =Duration.ofMinutes(3600); /* * 自定義token 字首字元 */ private String tokenPrefix; /* * accessToken超時時間 單位秒 */ private Duration accessToken =Duration.ofMinutes(3600); /* * 重新整理token時間 單位秒 */ private Duration refreshToken =Duration.ofMinutes(3600); /* * 允許訪問的uri */ private String permitAll; /* * 需要校驗的uri */ private String authenticateUri; } ``` SpringBoot2.2.1版本使用`@ConfigurationProperties`註解是不能讀取yaml檔案的,只能讀取properties,所以自定義PropertySourceFactory ```java package com.example.springboot.jwt.core.io.support; import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.core.env.PropertySource; import org.springframework.core.io.support.DefaultPropertySourceFactory; import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.lang.Nullable; import java.io.IOException; import java.util.List; import java.util.Optional; /** *
* YAML配置檔案讀取工廠類 **
*
* @author nicky.ma * 修改記錄 * 修改後版本: 修改人: 修改日期: 2019/11/13 15:44 修改內容: **/ public class YamlPropertyResourceFactory implements PropertySourceFactory { /** * Create a {@link PropertySource} that wraps the given resource. * * @param name the name of the property source * @param encodedResource the resource (potentially encoded) to wrap * @return the new {@link PropertySource} (never {@code null}) * @throws IOException if resource resolution failed */ @Override public PropertySource> createPropertySource(@Nullable String name, EncodedResource encodedResource) throws IOException { String resourceName = Optional.ofNullable(name).orElse(encodedResource.getResource().getFilename()); if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml")) { //yaml資原始檔 List
* JWT工具類 ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/06 13:57 修改內容: **/ @Component @Slf4j public class JWTTokenUtil { private static final String CLAIM_KEY_USER_ID = "user_id"; private static final String CLAIM_KEY_USER_NAME ="user_name"; private static final String CLAIM_KEY_ACCOUNT_ENABLED = "enabled"; private static final String CLAIM_KEY_ACCOUNT_NON_LOCKED = "non_locked"; private static final String CLAIM_KEY_ACCOUNT_NON_EXPIRED = "non_expired"; private static final String CLAIM_KEY_AUTHORITIES = "scope"; //簽名方式 private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; @Autowired JWTProperties jwtProperties; /** * 生成acceptToken * @param userDetails * @return */ public String generateToken(UserDetails userDetails) { JWTUserDetails user = (JWTUserDetails) userDetails; Map
* JWTUserDetails ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/06 14:45 修改內容: **/ @Data @AllArgsConstructor @NoArgsConstructor public class JWTUserDetails implements UserDetails { /** * 使用者ID */ private Long userId; /** * 使用者密碼 */ private String password; /** * 使用者名稱 */ private String username; /** * 使用者角色許可權 */ private Collection extends GrantedAuthority> authorities; /** * 賬號是否過期 */ private Boolean isAccountNonExpired = false; /** * 賬戶是否鎖定 */ private Boolean isAccountNonLocked = false; /** * 密碼是否過期 */ private Boolean isCredentialsNonExpired = false; /** * 賬號是否啟用 */ private Boolean isEnabled = true; /** * 上次密碼重置時間 */ private Instant lastPasswordResetDate; public JWTUserDetails(Long id, String username, String password, List
* UserDetailsServiceImpl ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/06 18:10 修改內容: **/ @Service("jwtUserService") @Slf4j public class UserDetailsServiceImpl implements UserDetailsService { @Autowired @Qualifier("userMapper") UserMapper userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { JWTUserDetails user = userRepository.findByUsername(username); if(user == null){ log.info("登入使用者[{}]沒註冊!",username); throw new UsernameNotFoundException("登入使用者["+username + "]沒註冊!"); } return new JWTUserDetails(1L,user.getUsername(), user.getPassword(), getAuthority()); } private List
* JWTAuthenticationEntryPoint ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/09 14:46 修改內容: **/ @Component public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // 出錯時候 httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } } ``` ### 6.6 JWT授權過濾器 ```java package com.example.springboot.jwt.web.filter; import com.example.springboot.jwt.configuration.JWTProperties; import com.example.springboot.jwt.core.jwt.userdetails.JWTUserDetails; import com.example.springboot.jwt.core.jwt.util.JWTTokenUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** *
* JWTAuthenticationTokenFilter ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/06 16:04 修改內容: **/ @Slf4j public class JWTAuthenticationTokenFilter extends OncePerRequestFilter { private static final ConcurrentMap
* MyWebMvcConfigurer ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/07 13:52 修改內容: **/ @Configuration public class MyWebMvcConfigurer implements WebMvcConfigurer { @Autowired private JWTProperties jwtProperties; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SecurityHandlerInterceptor()) .addPathPatterns("/**"); } @Bean public JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter() { return new JWTAuthenticationTokenFilter(jwtProperties); } @Bean public FilterRegistrationBean jwtFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(jwtAuthenticationTokenFilter()); return registrationBean; } } ``` ### 6.7 Spring Security配置類 ```java package com.example.springboot.jwt.configuration; import com.example.springboot.jwt.core.encode.CustomPasswordEncoder; import com.example.springboot.jwt.web.filter.JWTAuthenticationTokenFilter; import com.example.springboot.jwt.web.handler.JWTAuthenticationEntryPoint; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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; /** *
* SecurityConfiguration ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/04/30 15:58 修改內容: **/ @Configuration @EnableWebSecurity @Order(1) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("jwtUserService") private UserDetailsService userDetailsService; @Autowired private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(new CustomPasswordEncoder()); auth.parentAuthenticationManager(authenticationManagerBean()); } @Override public void configure(WebSecurity web) throws Exception { //解決靜態資源被攔截的問題 web.ignoring().antMatchers("/asserts/**"); web.ignoring().antMatchers("/favicon.ico"); } @Override protected void configure(HttpSecurity http) throws Exception { http // 配置登入頁並允許訪問 .formLogin().loginPage("/login").permitAll() // 登入成功被呼叫 //.successHandler(new MyAuthenticationSuccessHandler()) // 配置登出頁面 .and().logout().logoutUrl("/logout").logoutSuccessUrl("/") .and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**","/authenticate/**").permitAll() // 其餘所有請求全部需要鑑權認證 .anyRequest().authenticated() // 自定義authenticationEntryPoint .and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint ) // 不使用Session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 關閉跨域保護; .and().csrf().disable(); // JWT 過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder bcryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` ### 6.8 自定義登入頁面 ```html
Oauth2.0 Login
© 2019
中文 English ``` LoginController.java: ```java @GetMapping(value = {"/login"}) public ModelAndView toLogin(){ ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName("login"); return modelAndView; } @PostMapping(value = "/authenticate") @ResponseBody public ResponseEntity> authenticate( UserDto userDto, HttpServletRequest request, HttpServletResponse response) throws Exception { // ... 省略使用者登入校驗程式碼 UserDetails userDetails = userDetailsService.loadUserByUsername(userDto.getUsername()); String token = jwtTokenUtil.generateToken(userDetails); response.setHeader(jwtProperties.getTokenKey(),jwtProperties.getTokenPrefix()+token); return ResponseEntity.ok(token); } ``` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020070917194187.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70) 輸入賬號密碼,校驗通過,返回jwt的令牌token ``` eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX25hbWUiOiJuaWNreSIsInNjb3BlIjpbIlJPTEVfQURNSU4iXSwibm9uX2V4cGlyZWQiOmZhbHNlLCJleHAiOjE1OTQyODgyMzksImlhdCI6MTU5NDI4NDYzOCwiZW5hYmxlZCI6dHJ1ZSwibm9uX2xvY2tlZCI6ZmFsc2V9.bxGCCBSQE5cgVSl9Lve-vyDtITw1gL5i2-O-B5uEgno ``` 測試令牌,官方測試連結:[https://jwt.io/#debugger-io](https://jwt.io/#debugger-io) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200709174413497.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70) base64: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200709165440434.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70) ```java package com.example.springboot.jwt.web.controller; import com.example.springboot.jwt.configuration.JWTProperties; import com.example.springboot.jwt.core.jwt.util.JWTTokenUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; /** ** UserController ** *
* @author mazq * 修改記錄 * 修改後版本: 修改人: 修改日期: 2020/07/07 14:14 修改內容: **/ @RestController @RequestMapping(value = "api/user") public class UserController { @Autowired JWTProperties jwtProperties; @Autowired JWTTokenUtil jwtTokenUtil; @GetMapping("/auth-info") public ResponseEntity authInfo(HttpServletRequest request) { String authHeader = request.getHeader(jwtProperties.getTokenKey()); String token = authHeader.substring(7); return ResponseEntity.ok(jwtTokenUtil.getUsernameFromClaims(token)); } } ``` 複製生成的jwt令牌,設定Request Header ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200709165524814.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MjczOTE=,size_16,color_FFFFFF,t_70) * 附錄: https://www.javainuse.com/spring/boot-jwt 程式碼例子下載:[下載](https://github.com/u014427391/springbootexamples/tree/master/springbo