[ JWT ] SpringBoot 集成 JWT 實現 token 鑑權

SpringBoot 集成 JWT 實現 token 鑑權

完整項目地址:https://gitee.com/aoxiaobao/JWT-study.git


pom依賴

  • 首先當然是引入pom依賴
<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

一個簡單的小例子


  • 創建 token
    • 通過使用 Jwts.builder()
    • token 添加一些簡單的用戶信息
    • 選擇一個加密算法
    • 就可以創建出一個 token
  • 解析 token
    • 通過 Jwts.parser()
    • 獲取其中加密的用戶信息
public class TestJwt {
    public static void main(String[] args) {
        System.out.println(createJwt());

        parseJwt(createJwt());
    }
    public static String createJwt(){
        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("888888") // 添加id
                .setSubject("藏冰") // 添加用戶名
                .setIssuedAt(new Date()) // 添加當前時間
                .signWith(SignatureAlgorithm.HS256,"aowei"); // 設置加密算法,密鑰
        String token = jwtBuilder.compact(); // 獲取token
        return token;
    }
    public static void parseJwt(String token){
        Claims claims = Jwts.parser()
                .setSigningKey("aowei")
                .parseClaimsJws(token).getBody();

        System.out.println(claims.getId());
        System.out.println(claims.getSubject());
        System.out.println(claims.getIssuedAt());
    }
}

JwtUtils 工具類


  • 將上面的簡單例子進一步完善,
  • 私鑰簽名的失效時間,單獨提取出來
  • 還可以在 token 中添加自定義的 Map 格式數據
  • 最後添加上自定義異常的異常處理

public class JwtUtils {
    // 簽名私鑰
    public static final String AUTH_HEADER_KEY = "Authorization";
    // 簽名的失效時間
    private static final long TTL = 10000 ;

    private static Logger log = LoggerFactory.getLogger(JwtUtils.class);
    /**
     * 設置認證token
     * id:登錄用戶id
     * subject:登錄用戶名
     */

    public static String createJwt(String id, String name, Map<String,Object> map) throws CustomException {
        // 設置失效時間
        long now = System.currentTimeMillis(); // 當前毫秒數
        long exp = now + TTL;

        // 創建jwtBuilder
        String token = null;
        try {
            JwtBuilder jwtBuilder = Jwts.builder()
                    .setId(id) // 添加id
                    .setSubject(name) // 添加用戶名
                    .setIssuedAt(new Date()) // 添加當前時間
                    .signWith(SignatureAlgorithm.HS256,AUTH_HEADER_KEY); // 設置加密算法,密鑰

            // 根據map設置claims
            for (Map.Entry<String,Object> entry : map.entrySet()){
                jwtBuilder.claim(entry.getKey(),entry.getValue());
            }

            // 指定失效時間
            jwtBuilder.setExpiration(new Date(exp));

            // 創建token
            token = jwtBuilder.compact();
        } catch (Exception e) {
            log.error("簽名失敗", e);
            throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
        }
        return token;
    }

    /**
     * 解析token字符串,獲取clamis
     */
    public static Claims parseJwt(String token) throws CustomException {

        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(AUTH_HEADER_KEY)
                    .parseClaimsJws(token).getBody();
            return claims;
        } catch (ExpiredJwtException e) {
            log.error("===== Token過期 =====", e);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED);
        } catch (Exception e) {
            log.error("===== token解析異常 =====", e);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
        }
    }
}

自定義JWT攔截器


  • 創建一個 token 攔截器

  • 繼承 HandlerInterceptorAdapter

  • 有三個常用的方法,這裏只需要重寫 preHandle 方法即可

  • 重寫 preHandle方法(進入到控制器方法之前執行的內容)

    • 返回值是 boolean類型
    • true: 可以繼續執行控制器方法
    • false: 攔截請求
  • postHandler(執行控制器方法之後執行的內容)

  • afterHandler(響應結束之前執行的內容)

  • 這裏攔截器要實現的功能有

    • 簡化獲取token數據的代碼編寫
    • 統一的用戶權限校驗(是否登錄)
    • 判斷用戶是否具有當前訪問接口的權限
/**
 * 自定義攔截器
 *  繼承HandlerInterceptorAdapter
 *  preHandle: 進入到控制器方法之前執行的內容
 *      boolean:
 *          true: 可以繼續執行控制器方法
 *          false: 攔截
 *  postHandler: 執行控制器方法之後執行的內容
 *  afterHandler: 響應結束之前執行的內容
 *
 *  簡化獲取token數據的代碼編寫
 *      統一的用戶權限校驗(是否登錄)
 *  判斷用戶是否具有當前訪問接口的權限
 * @author aowei
 */
@Component
@Slf4j
public class JwtInterceptor extends HandlerInterceptorAdapter {

    /**
     * 通過攔截器獲取token數據
     * 從token中解析獲取claims
     * 將claims綁定到request域中
     */

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 忽略帶JwtIgnore註解的請求, 不做後續token認證校驗
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
            if (jwtIgnore != null) {
                return true;
            }
        }

        // 通過request獲取請求token信息
        String authorization = request.getHeader("Authorization");
        // 判斷請求頭信息是否爲空,或是否以Bearer 開頭
        if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")){
            // 獲取token數據
            String token = authorization.replace("Bearer","");
            // 解析token獲取claims
            Claims claims = JwtUtils.parseJwt(token);
            if(claims != null){
                // 通過claims獲取到當前用戶的可訪問api權限字符串
                String apis = (String) claims.get("roles");
                // 通過handler
                String name = null;
                if (handler instanceof HandlerMethod) {
                    HandlerMethod h = (HandlerMethod) handler;
                    // 獲取接口上的requestmapping註解
                    RequestMapping annotation = h.getMethodAnnotation(RequestMapping.class);
                    // 獲取當前請求接口中的name屬性
                    name = annotation.name();
                }else {
                    throw new CustomException(ResultCode.INTERFACE_ADDRESS_INVALID);
                }
                if(apis.contains(name)){
                    request.setAttribute("userClaims",claims);
                    return true;
                }else{
                    throw new CustomException(ResultCode.PERMISSION_UNAUTHORISE);
                }
            }
        }
        throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
    }
}

配置攔截器


  • 攔截器寫好之後,還不能真正起到作用
  • 這裏需要配置一下 JWT 攔截器
  • 實現 WebMvcConfigurer 接口
  • addPathPatterns() 方法攔截請求
  • /**表示攔截所有的請求
  • 可以在 excludePathPatterns() 方法中指明不需要攔截的路徑
@Configuration
public class JwtConfig implements WebMvcConfigurer {

    /**
     * 添加攔截器的配置
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry){
        // 添加自定義攔截器
        registry.addInterceptor(new JwtInterceptor())
                .addPathPatterns("/**");
    }
    /**
     * 跨域支持
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
                .maxAge(3600 * 24);
    }
}

自定義註解


  • 通過自定義註解 @JwtIgnore
  • 實現放行某些接口(不攔截請求)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtIgnore {
//    String value() default "JWT";
}

代碼測試


  • 將用戶的角色也添加到 token
  • 在攔截器中解析 token 獲得用戶角色
  • 再從 handler 中獲取請求的 RequestMapping註解的name屬性
  • 可以在name屬性裏,寫上訪問該接口所需的權限名稱
  • 比較 用戶角色和 name 屬性,就可以判斷出用戶是否有訪問該接口的權限
@Slf4j
@RestController
public class Login {
    @Autowired
    UserService userService;
    @Autowired
    RoleService roleService;

    @RequestMapping(value = "/login",method = RequestMethod.POST)
    @JwtIgnore
    public Result login(String username, String password) throws CustomException {
        User user = userService.findByUsername(username);
        if(user == null || !user.getPassword().equals(password)){
            return new Result(ResultCode.FAIL);
        } else {
//             登錄成功
//             獲取到所有的可訪問api權限
//            StringBuilder sb = new StringBuilder();
//            for(Role role : user.getRole()){
//                for(Permission perm : role.getPermissions()){
//                    sb.append(perm.getCode().append(","));
//                }
//            }

            Role role = roleService.findByUsername(username);
            Map<String,Object> map = new HashMap<>();
            // 可訪問的角色
            map.put("roles",role.getRoleName());
            String token = JwtUtils.createJwt(username,username,map);
//            Claims claims = JwtUtils.parseJwt(token);
//            System.out.println("角色:"+claims.get("roles"));
//            System.out.println("用戶名:"+claims.getSubject());
//            System.out.println("用戶id:"+claims.getId());
            Result result = new Result(ResultCode.SUCCESS);
            result.setData(token);
            return result;
        }
    }

    // 測試需要權限的url(需要登錄且擁有all-admin的權限)
    @RequestMapping(value = "/test3",method = RequestMethod.GET,name="all-admin")
    public Result test3(){
        return new Result(ResultCode.SUCCESS);
    }

    // 測試需要權限的url(需要登錄且擁有test2的權限)
    @RequestMapping(value = "/test2",method = RequestMethod.GET,name="test2")
    public Result test1(){
        return new Result(ResultCode.SUCCESS);
    }

    // 測試被攔截的url(需要登錄)
    @RequestMapping(value = "/test1",method = RequestMethod.GET)
    public Result test2(){
        return new Result(ResultCode.SUCCESS);
    }

    // 測試不攔截的url(不需要登錄)
    @RequestMapping("/test")
    @JwtIgnore
    public Result test(){
        return new Result(ResultCode.SUCCESS);
    }
}

參考鏈接:https://www.jianshu.com/p/430cd44a2796


在這裏插入圖片描述

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