1. 程式人生 > >SpringBoot系列之前後端介面安全技術JWT

SpringBoot系列之前後端介面安全技術JWT

@[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; /** *
 *  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> 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.*; /** *
 *   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 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 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; /** *
 *  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 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 mapToGrantedAuthorities) { this.userId = id; this.username = username; this.password = password; this.authorities = mapToGrantedAuthorities; } @Override public Collection 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; /** *
 *  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 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; /** *
 *  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 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; /** *
 *  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 Signin Template for Bootstrap

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