溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

springboot+shiro+jwt

發(fā)布時間:2020-06-09 08:22:59 來源:網(wǎng)絡 閱讀:24752 作者:yushiwh 欄目:軟件技術(shù)

JWTUtil

我們利用 JWT 的工具類來生成我們的 token,這個工具類主要有生成 token 和 校驗 token 兩個方法

生成 token 時,指定 token 過期時間 EXPIRE_TIME 和簽名密鑰 SECRET,然后將 date 和 username 寫入 token 中,并使用帶有密鑰的 HS256 簽名算法進行簽名

Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWT.create()
    .withClaim("username", username)
    //到期時間
    .withExpiresAt(date)
    //創(chuàng)建一個新的JWT,并使用給定的算法進行標記
    .sign(algorithm);

數(shù)據(jù)庫表

springboot+shiro+jwt

role: 角色;permission: 權(quán)限;ban: 封號狀態(tài)

springboot+shiro+jwt

每個用戶有對應的角色(user,admin),權(quán)限(normal,vip),而 user 角色默認權(quán)限為 normal, admin 角色默認權(quán)限為 vip(當然,user 也可以是 vip)

過濾器

在上一篇文章中,我們使用的是 shiro 默認的權(quán)限攔截 Filter,而因為 JWT 的整合,我們需要自定義自己的過濾器 JWTFilter,JWTFilter 繼承了 BasicHttpAuthenticationFilter,并部分原方法進行了重寫

該過濾器主要有三步:

  1. 檢驗請求頭是否帶有 token ((HttpServletRequest) request).getHeader("Token") != null
  2. 如果帶有 token,執(zhí)行 shiro 的 login() 方法,將 token 提交到 Realm 中進行檢驗;如果沒有 token,說明當前狀態(tài)為游客狀態(tài)(或者其他一些不需要進行認證的接口)

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        //判斷請求的請求頭是否帶上 "Token"
        if (((HttpServletRequest) request).getHeader("Token") != null) {
            //如果存在,則進入 executeLogin 方法執(zhí)行登入,檢查 token 是否正確
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                //token 錯誤
                responseError(response, e.getMessage());
            }
        }
        //如果請求頭不存在 Token,則可能是執(zhí)行登陸操作或者是游客狀態(tài)訪問,無需檢查 token,直接返回 true
        return true;
    }
    
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Token");
        JWTToken jwtToken = new JWTToken(token);
        // 提交給realm進行登入,如果錯誤他會拋出異常并被捕獲
        getSubject(request, response).login(jwtToken);
        // 如果沒有拋出異常則代表登入成功,返回true
        return true;
    }
  3. 如果在 token 校驗的過程中出現(xiàn)錯誤,如 token 校驗失敗,那么我會將該請求視為認證不通過,則重定向到 /unauthorized/**

另外,我將跨域支持放到了該過濾器來處理

Realm 類

依然是我們的自定義 Realm ,對這一塊還不了解的可以先看我的上一篇 shiro 的文章

  • 身份認證

    if (username == null || !JWTUtil.verify(token, username)) {
    throw new AuthenticationException("token認證失敗!");
    }
    String password = userMapper.getPassword(username);
    if (password == null) {
    throw new AuthenticationException("該用戶不存在!");
    }
    int ban = userMapper.checkUserBanStatus(username);
    if (ban == 1) {
    throw new AuthenticationException("該用戶已被封號!");
    }

    拿到傳來的 token ,檢查 token 是否有效,用戶是否存在,以及用戶的封號情況

  • 權(quán)限認證
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //獲得該用戶角色
    String role = userMapper.getRole(username);
    //每個角色擁有默認的權(quán)限
    String rolePermission = userMapper.getRolePermission(username);
    //每個用戶可以設置新的權(quán)限
    String permission = userMapper.getPermission(username);
    Set<String> roleSet = new HashSet<>();
    Set<String> permissionSet = new HashSet<>();
    //需要將 role, permission 封裝到 Set 作為 info.setRoles(), info.setStringPermissions() 的參數(shù)
    roleSet.add(role);
    permissionSet.add(rolePermission);
    permissionSet.add(permission);
    //設置該用戶擁有的角色和權(quán)限
    info.setRoles(roleSet);
    info.setStringPermissions(permissionSet);

    利用 token 中獲得的 username,分別從數(shù)據(jù)庫查到該用戶所擁有的角色,權(quán)限,存入 SimpleAuthorizationInfo 中

ShiroConfig 配置類

設置好我們自定義的 filter,并使所有請求通過我們的過濾器,除了我們用于處理未認證請求的 /unauthorized/**

@Bean
public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

    // 添加自己的過濾器并且取名為jwt
    Map<String, Filter> filterMap = new HashMap<>();
    //設置我們自定義的JWT過濾器
    filterMap.put("jwt", new JWTFilter());
    factoryBean.setFilters(filterMap);
    factoryBean.setSecurityManager(securityManager);
    Map<String, String> filterRuleMap = new HashMap<>();
    // 所有請求通過我們自己的JWT Filter
    filterRuleMap.put("/**", "jwt");
    // 訪問 /unauthorized/** 不通過JWTFilter
    filterRuleMap.put("/unauthorized/**", "anon");
    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
    return factoryBean;
}

權(quán)限控制注解 @RequiresRoles, @RequiresPermissions

這兩個注解為我們主要的權(quán)限控制注解, 如

// 擁有 admin 角色可以訪問
@RequiresRoles("admin")
// 擁有 user 或 admin 角色可以訪問
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
// 擁有 vip 和 normal 權(quán)限可以訪問
@RequiresPermissions(logical = Logical.AND, value = {"vip", "normal"})
// 擁有 user 或 admin 角色,且擁有 vip 權(quán)限可以訪問
@GetMapping("/getVipMessage")
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
@RequiresPermissions("vip")
public ResultMap getVipMessage() {
    return resultMap.success().code(200).message("成功獲得 vip 信息!");
}

當我們寫的接口擁有以上的注解時,如果請求沒有帶有 token 或者帶了 token 但權(quán)限認證不通過,則會報 UnauthenticatedException 異常,但是我在 ExceptionController 類對這些異常進行了集中處理

@ExceptionHandler(ShiroException.class)
public ResultMap handle401() {
    return resultMap.fail().code(401).message("您沒有權(quán)限訪問!");
}

這時,出現(xiàn) shiro 相關(guān)的異常時則會返回

{
    "result": "fail",
    "code": 401,
    "message": "您沒有權(quán)限訪問!"
}

除了以上兩種,還有 @RequiresAuthentication ,@RequiresUser 等注解

功能實現(xiàn)

用戶角色分為三類,管理員 admin,普通用戶 user,游客 guest;admin 默認權(quán)限為 vip,user 默認權(quán)限為 normal,當 user 升級為 vip 權(quán)限時可以訪問 vip 權(quán)限的頁面。

具體實現(xiàn)可以看源代碼(開頭已經(jīng)給出地址)

登陸

登陸接口不帶有 token,當?shù)顷懨艽a,用戶名驗證正確后返回 token。

@PostMapping("/login")
public ResultMap login(@RequestParam("username") String username,
                       @RequestParam("password") String password) {
    String realPassword = userMapper.getPassword(username);
    if (realPassword == null) {
        return resultMap.fail().code(401).message("用戶名錯誤");
    } else if (!realPassword.equals(password)) {
        return resultMap.fail().code(401).message("密碼錯誤");
    } else {
        return resultMap.success().code(200).message(JWTUtil.createToken(username));
    }
}
{
    "result": "success",
    "code": 200,
    "message": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MjUxODQyMzUsInVzZXJuYW1lIjoiaG93aWUifQ.fG5Qs739Hxy_JjTdSIx_iiwaBD43aKFQMchx9fjaCRo"
}

異常處理

    // 捕捉shiro的異常
    @ExceptionHandler(ShiroException.class)
    public ResultMap handle401() {
        return resultMap.fail().code(401).message("您沒有權(quán)限訪問!");
    }

    // 捕捉其他所有異常
    @ExceptionHandler(Exception.class)
    public ResultMap globalException(HttpServletRequest request, Throwable ex) {
        return resultMap.fail()
                .code(getStatus(request).value())
                .message("訪問出錯,無法訪問: " + ex.getMessage());
    }

權(quán)限控制

  • UserController(user 或 admin 可以訪問)
    在接口上帶上 @RequiresRoles(logical = Logical.OR, value = {"user", "admin"})

    • vip 權(quán)限
      再加上@RequiresPermissions("vip")
  • AdminController(admin 可以訪問)
    在接口上帶上 @RequiresRoles("admin")

  • GuestController(所有人可以訪問)
    不做權(quán)限處理

測試結(jié)果

springboot+shiro+jwt

springboot+shiro+jwt

springboot+shiro+jwt

springboot+shiro+jwt

springboot+shiro+jwt

springboot+shiro+jwt

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI