SpringBoot 整合 JWT 驗證+解決域名問題
什麼是JWT JSON Web Token(JWT)是一個開放的標準(RFC 7519),它定義了一個緊湊且自包含的方式,用於在各方之間以JSON物件安全地傳輸資訊。這些資訊可以通過數字簽名進行驗證和信任。可以使用祕密(使用HMAC演算法)或使用RSA的公鑰/私鑰對來對JWT進行簽名。 具體的jwt介紹可以檢視官網的介紹:https://jwt.io/introduction/
jwt請求流程 引用官網的圖片
中文介紹: 1. 使用者使用賬號和麵發出post請求; 2. 伺服器使用私鑰建立一個jwt; 3. 伺服器返回這個jwt給瀏覽器; 4. 瀏覽器將該jwt串在請求頭中像伺服器傳送請求; 5. 伺服器驗證該jwt; 6. 返回響應的資源給瀏覽器。
jwt組成 jwt含有三部分:頭部(header)、載荷(payload)、簽證(signature)
頭部(header) 頭部一般有兩部分資訊:宣告型別、宣告加密的演算法(通常使用HMAC SHA256) 頭部一般使用base64加密:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 解密後:
{ "typ":"JWT", "alg":"HS256" } 載荷(payload) 該部分一般存放一些有效的資訊。jwt的標準定義包含五個欄位: - iss:該JWT的簽發者 - sub: 該JWT所面向的使用者 - aud: 接收該JWT的一方 - exp(expires): 什麼時候過期,這裡是一個Unix時間戳 - iat(issued at): 在什麼時候簽發的 這個只是JWT的定義標準,不強制使用。另外自己也可以新增一些公開的不涉及安全的方面的資訊。
簽證(signature) JWT最後一個部分。該部分是使用了HS256加密後的資料;包含三個部分: - header (base64後的) - payload (base64後的) - secret 私鑰
secret是儲存在伺服器端的,jwt的簽發生成也是在伺服器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。
在SpringBoot專案中應用 首先需要新增JWT的依賴:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.6.0</version> </dependency>
接下來在配置檔案中新增JWT的配置資訊:
##jwt配置 audience: clientId: 098f6bcd4621d373cade4e832627b4f6 base64Secret: MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY= name: restapiuser expiresSecond: 172800
配置資訊的實體類,以便獲取jwt的配置:
@Data @ConfigurationProperties(prefix = "audience") @Component public class Audience {
private String clientId; private String base64Secret; private String name; private int expiresSecond;
} JWT驗證主要是通過攔截器驗證,所以我們需要新增一個攔截器來驗證請求頭中是否含有後臺頒發的token,這裡請求頭的格式:這裡bearer;後面就是伺服器頒發的token
public class JwtFilter extends GenericFilterBean {
@Autowired private Audience audience;
/** * Reserved claims(保留),它的含義就像是程式語言的保留字一樣,屬於JWT標準裡面規定的一些claim。JWT標準裡面定好的claim有:
iss(Issuser):代表這個JWT的簽發主體; sub(Subject):代表這個JWT的主體,即它的所有人; aud(Audience):代表這個JWT的接收物件; exp(Expiration time):是一個時間戳,代表這個JWT的過期時間; nbf(Not Before):是一個時間戳,代表這個JWT生效的開始時間,意味著在這個時間之前驗證JWT是會失敗的; iat(Issued at):是一個時間戳,代表這個JWT的簽發時間; jti(JWT ID):是JWT的唯一標識。 * @param req * @param res * @param chain * @throws IOException * @throws ServletException */ @Override public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; //等到請求頭資訊authorization資訊 final String authHeader = request.getHeader("authorization");
if ("OPTIONS".equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); chain.doFilter(req, res); } else {
if (authHeader == null || !authHeader.startsWith("bearer;")) { throw new LoginException(ResultEnum.LOGIN_ERROR); } final String token = authHeader.substring(7);
try { if(audience == null){ BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext()); audience = (Audience) factory.getBean("audience"); } final Claims claims = JwtHelper.parseJWT(token,audience.getBase64Secret()); if(claims == null){ throw new LoginException(ResultEnum.LOGIN_ERROR); } request.setAttribute(Constants.CLAIMS, claims); } catch (final Exception e) { throw new LoginException(ResultEnum.LOGIN_ERROR); }
chain.doFilter(req, res); } } } 註冊JWT攔截器,可以在配置類中,也可以直接在SpringBoot的入口類中
@Bean public FilterRegistrationBean jwtFilter() { final FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new JwtFilter()); //新增需要攔截的url List<String> urlPatterns = Lists.newArrayList(); urlPatterns.add("/article/insert"); registrationBean.addUrlPatterns(urlPatterns.toArray(new String[urlPatterns.size()])); return registrationBean; }
登入處理,也就是jwt的頒發
@PostMapping("login") public ResultVo login(@RequestParam(value = "usernameOrEmail", required = true) String usernameOrEmail, @RequestParam(value = "password", required = true) String password, HttpServletRequest request) { Boolean is_email = MatcherUtil.matcherEmail(usernameOrEmail); User user = new User(); if (is_email) { user.setEmail(usernameOrEmail); } else { user.setUsername(usernameOrEmail); } User query_user = userService.get(user); if (query_user == null) { return ResultVOUtil.error("400", "使用者名稱或郵箱錯誤"); } //驗證密碼 PasswordEncoder encoder = new BCryptPasswordEncoder(); boolean is_password = encoder.matches(password, query_user.getPassword()); if (!is_password) { //密碼錯誤,返回提示 return ResultVOUtil.error("400", "密碼錯誤"); }
String jwtToken = JwtHelper.createJWT(query_user.getUsername(), query_user.getId(), query_user.getRole().toString(), audience.getClientId(), audience.getName(), audience.getExpiresSecond()*1000, audience.getBase64Secret());
String result_str = "bearer;" + jwtToken; return ResultVOUtil.success(result_str); }
這裡將jwt的頒發處理抽離出來了,JWT工具類:
public class JwtHelper {
/** * 解析jwt */ public static Claims parseJWT(String jsonWebToken, String base64Security){ try { Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security)) .parseClaimsJws(jsonWebToken).getBody(); return claims; } catch(Exception ex) { return null; } }
/** * 構建jwt */ public static String createJWT(String name, String userId, String role, String audience, String issuer, long TTLMillis, String base64Security) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis);
//生成簽名金鑰 byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security); Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//新增構成JWT的引數 JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT") .claim("role", role) .claim("unique_name", name) .claim("userid", userId) .setIssuer(issuer) .setAudience(audience) .signWith(signatureAlgorithm, signingKey); //新增Token過期時間 if (TTLMillis >= 0) { long expMillis = nowMillis + TTLMillis; Date exp = new Date(expMillis); builder.setExpiration(exp).setNotBefore(now); }
//生成JWT return builder.compact(); } }
最後,jwt可能會出現跨域的問題,所以最好新增一下對跨域的處理
@Configuration public class CorsConfig {
@Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("OPTIONS"); config.addAllowedMethod("HEAD"); config.addAllowedMethod("GET"); config.addAllowedMethod("PUT"); config.addAllowedMethod("POST"); config.addAllowedMethod("DELETE"); config.addAllowedMethod("PATCH"); source.registerCorsConfiguration("/**", config); final FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; }
@Bean public WebMvcConfigurer mvcConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedMethods("GET", "PUT", "POST", "GET", "OPTIONS"); } }; } } --------------------- 作者:wqh3520 來源:CSDN 原文:https://blog.csdn.net/wqh8522/article/details/78953379