什麼是JWT
Json Web Token (JWT)是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準(RFC 7519)該token被設計為緊湊且安全的 特別適用於分散式站點的單點登入(SSO)場景
隨著JWT的出現 使得校驗方式更加簡單便捷化JWT實際上就是一個字串 它由三部分組成:頭部 載荷和簽名用[.]分隔這三個部分 最終的格式類似於:xxxx.xxxx.xxxx
該伺服器直接根據token取出儲存的使用者資訊 即可對token的可用性進行校驗 使得單點登入更為簡單
JWT校驗的過程1、瀏覽器傳送使用者名稱和密碼 發起登入請求2、服務端驗證身份 根據演算法將使用者識別符號打包生成token字串 並且返回給瀏覽器3、當瀏覽器需要發起請求時 將token一起傳送給伺服器4、伺服器發現數據中攜帶有token 隨即進行解密和鑑權5、校驗成功 伺服器返回請求的資料JWT的使用專案使用:springboot+mysql+mybatisplus+lombok
不懂的可以先去學習一下,或者可以使用其他方法大同小異
第一步:建立SpringBoot專案我這裡用的Maven構建
第二步:導包修改pom.xml下面的dependencies節點加入如下
server: port: 8089spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/jwt?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false type: com.alibaba.druid.pool.DruidDataSource username: root password: rootmybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true type-aliases-package: com.kexun.springbootjwt.entity
第四步:建立資料庫(jwt)和表user結構如下
CREATE DATABASE `jwt` CREATE TABLE `user` ( `id` varchar(32) DEFAULT NULL, `username` varchar(20) DEFAULT NULL, `password` varchar(20) DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8
第五步:配置資料來源Druid配置類
@Configurationpublic class DruidConfig { @ConfigurationProperties(prefix = "spring.datasource") @Bean public DruidDataSource druidDataSource() { return new DruidDataSource(); }}
第六步:建立實體
@Datapublic class User { @TableId(value = "id", type = IdType.UUID) private String id; private String username; private String password;}
第七步:建立mapper
public interface UserMapper extends BaseMapper<User> {}
第八步:建立自定義註解標記的這個註解的
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface CheckToken { boolean required() default true;}
第九步:建立攔截器
public class LoginInterceptor implements HandlerInterceptor { private UserService userService; public LoginInterceptor(UserService userService) { this.userService = userService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 從 http 請求頭中取出 token String token = request.getHeader("token"); if (StringUtils.isEmpty(token)) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { String name = cookie.getName(); if ("x-token".equals(name)) { token = cookie.getValue(); } } } } // 如果不是對映到方法直接透過 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); //檢查有沒有需要使用者許可權的註解 if (method.isAnnotationPresent(CheckToken.class)) { CheckToken checkToken = method.getAnnotation(CheckToken.class); if (checkToken.required()) { // 執行認證 if (StringUtils.isEmpty(token)) { throw new RuntimeException("無token,請重新登入"); } // 獲取 token 中的 user id String userId; try { userId = JWT.decode(token).getClaim("id").asString(); } catch (JWTDecodeException j) { throw new RuntimeException("訪問異常!"); } User user = userService.getById(userId); if (user == null) { throw new RuntimeException("使用者不存在,請重新登入"); } Boolean verify = JwtUtils.isVerify(token, user); if (!verify) { throw new RuntimeException("非法訪問!"); } return true; } } return true; }}
第十步:註冊攔截器:
@Configurationpublic class WebConfig implements WebMvcConfigurer { @Autowired private UserService userService; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor(userService)).addPathPatterns("/**"); }}
異常處理類
@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result onRuntime(Exception e) { e.printStackTrace(); return Result.error(e.getMessage()); }}
返回包裝類
package com.kexun.springbootjwt.utils;public class Result { private static final long serialVersionUID = 1L; private int code; private String msg; private Object data; public static long getSerialVersionUID() { return serialVersionUID; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public Result(int code, String msg, Object data) { this.code = code; this.msg = msg; this.data = data; } public Result(int code, String msg) { this.code = code; this.msg = msg; } public Result() { } public static Result success() { return new Result(0, "ok", null); } public static Result success(String msg) { return new Result(0, msg, null); } public static Result success(String msg, Object data) { return new Result(0, msg, data); } public static Result error() { return new Result(500, "error", null); } public static Result error(String msg) { return new Result(500, msg, null); } public static Result error(String msg, Object data) { return new Result(500, msg, data); }}
JwtUtils
public class JwtUtils { /** * 使用者登入成功後生成Jwt * 使用Hs256演算法 私匙使用使用者密碼 * * @param ttlMillis jwt過期時間 * @param user 登入成功的user物件 * @return */ public static String createJWT(long ttlMillis, User user) { //指定簽名的時候使用的簽名演算法,也就是header那部分,jjwt已經將這部分內容封裝好了。 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //建立payload的私有宣告(根據特定的業務需要新增,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的) Map<String, Object> claims = new HashMap<String, Object>(); claims.put("id", user.getId()); claims.put("username", user.getUsername()); claims.put("password", user.getPassword()); //生成簽名的時候使用的秘鑰secret,這個方法本地封裝了的,一般可以從本地配置檔案中讀取,切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。 String key = user.getPassword(); //生成簽發人 String subject = user.getUsername(); //下面就是在為payload新增各種標準宣告和私有聲明瞭 //這裡其實就是new一個JwtBuilder,設定jwt的body JwtBuilder builder = Jwts.builder() //如果有私有宣告,一定要先設定這個自己建立的私有的宣告,這個是給builder的claim賦值,一旦寫在標準的宣告賦值之後,就是覆蓋了那些標準的宣告的 .setClaims(claims) //設定jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設定為一個不重複的值,主要用來作為一次性token,從而回避重放攻擊。 .setId(UUID.randomUUID().toString()) //iat: jwt的簽發時間 .setIssuedAt(now) //代表這個JWT的主體,即它的所有人,這個是一個json格式的字串,可以存放什麼userid,roldid之類的,作為什麼使用者的唯一標誌。 .setSubject(subject) //設定簽名使用的簽名演算法和簽名使用的秘鑰 .signWith(signatureAlgorithm, key); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); //設定過期時間 builder.setExpiration(exp); } return builder.compact(); } /** * Token的解密 * * @param token 加密後的token * @param user 使用者的物件 * @return */ public static Claims parseJWT(String token, User user) { //簽名秘鑰,和生成的簽名的秘鑰一模一樣 String key = user.getPassword(); //得到DefaultJwtParser Claims claims = Jwts.parser() //設定簽名的秘鑰 .setSigningKey(key) //設定需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } /** * 校驗token * 在這裡可以使用官方的校驗,我這裡校驗的是token中攜帶的密碼於資料庫一致的話就校驗透過 * * @param token * @param user * @return */ public static Boolean isVerify(String token, User user) { //簽名秘鑰,和生成的簽名的秘鑰一模一樣 String key = user.getPassword(); //得到DefaultJwtParser Claims claims = Jwts.parser() //設定簽名的秘鑰 .setSigningKey(key) //設定需要解析的jwt .parseClaimsJws(token).getBody(); if (claims.get("password").equals(user.getPassword())) { return true; } return false; }}
啟動類:
@SpringBootApplication@MapperScan("com.kexun.springbootjwt.mapper")public class SpringbootJwtApplication { public static void main(String[] args) { SpringApplication.run(SpringbootJwtApplication.class, args); }}
具體演示:
登入:http://127.0.0.1:8089/login?username=lidong&password=123456
鑑權:http://127.0.0.1:8089/checkToken
清空cookie在鑑權
授權失敗
附gitee原始碼地址:https://gitee.com/gdianqimeng/springboot-jwt.git