教你 Shiro + SpringBoot 整合 JWT

本篇文章將教大家在 shiro + springBoot 的基礎(chǔ)上整合 JWT (JSON Web Token)
如果對 shiro 如何整合 springBoot 還不了解的可以先去看我的上一篇文章 《教你 Shiro 整合 SpringBoot独令,避開各種坑》

附上源碼:https://github.com/HowieYuan/shiro

JWT

JSON Web Token(JWT)是一個(gè)非常輕巧的規(guī)范来庭。這個(gè)規(guī)范允許我們使用 JWT 在用戶和服務(wù)器之間傳遞安全可靠的信息疮鲫。

我們利用一定的編碼生成 Token弦叶,并在 Token 中加入一些非敏感信息,將其傳遞燕侠。

一個(gè)完整的 Token : eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

在本項(xiàng)目中,我們規(guī)定每次請求時(shí)七问,需要在請求頭中帶上 token 茫舶,通過 token 檢驗(yàn)權(quán)限,如沒有讥耗,則說明當(dāng)前為游客狀態(tài)(或者是登陸 login 接口等)

JWTUtil

我們利用 JWT 的工具類來生成我們的 token疹启,這個(gè)工具類主要有生成 token 和 校驗(yàn) token 兩個(gè)方法

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

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

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

user

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


role

每個(gè)用戶有對應(yīng)的角色(user挤忙,admin)谈喳,權(quán)限(normal,vip)赏僧,而 user 角色默認(rèn)權(quán)限為 normal扭倾, admin 角色默認(rèn)權(quán)限為 vip(當(dāng)然,user 也可以是 vip)

過濾器

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

該過濾器主要有三步:

  1. 檢驗(yàn)請求頭是否帶有 token ((HttpServletRequest) request).getHeader("Token") != null
  2. 如果帶有 token占调,執(zhí)行 shiro 的 login() 方法移剪,將 token 提交到 Realm 中進(jìn)行檢驗(yàn)纵苛;如果沒有 token,說明當(dāng)前狀態(tài)為游客狀態(tài)(或者其他一些不需要進(jìn)行認(rèn)證的接口)
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        //判斷請求的請求頭是否帶上 "Token"
        if (((HttpServletRequest) request).getHeader("Token") != null) {
            //如果存在取试,則進(jìn)入 executeLogin 方法執(zhí)行登入怀吻,檢查 token 是否正確
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                //token 錯(cuò)誤
                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進(jìn)行登入兆龙,如果錯(cuò)誤他會拋出異常并被捕獲
        getSubject(request, response).login(jwtToken);
        // 如果沒有拋出異常則代表登入成功,返回true
        return true;
    }
  1. 如果在 token 校驗(yàn)的過程中出現(xiàn)錯(cuò)誤掂林,如 token 校驗(yàn)失敗坝橡,那么我會將該請求視為認(rèn)證不通過,則重定向到 /unauthorized/**

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

Realm 類

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

  • 身份認(rèn)證
if (username == null || !JWTUtil.verify(token, username)) {
    throw new AuthenticationException("token認(rèn)證失斃底琛踱蠢!");
}
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)限認(rèn)證
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//獲得該用戶角色
String role = userMapper.getRole(username);
//每個(gè)角色擁有默認(rèn)的權(quán)限
String rolePermission = userMapper.getRolePermission(username);
//每個(gè)用戶可以設(shè)置新的權(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);
//設(shè)置該用戶擁有的角色和權(quán)限
info.setRoles(roleSet);
info.setStringPermissions(permissionSet);

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

ShiroConfig 配置類

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

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

    // 添加自己的過濾器并且取名為jwt
    Map<String, Filter> filterMap = new HashMap<>();
    //設(shè)置我們自定義的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

這兩個(gè)注解為我們主要的權(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 信息答朋!");
}

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

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

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

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

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

功能實(shí)現(xiàn)

用戶角色分為三類阵漏,管理員 admin,普通用戶 user回还,游客 guest叹洲;admin 默認(rèn)權(quán)限為 vip,user 默認(rèn)權(quán)限為 normal蝗柔,當(dāng) user 升級為 vip 權(quán)限時(shí)可以訪問 vip 權(quán)限的頁面民泵。

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

登陸

登陸接口不帶有 token栈妆,當(dāng)?shù)顷懨艽a,用戶名驗(yàn)證正確后返回 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("用戶名錯(cuò)誤");
    } else if (!realPassword.equals(password)) {
        return resultMap.fail().code(401).message("密碼錯(cuò)誤");
    } 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("訪問出錯(cuò)昧旨,無法訪問: " + 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é)果

不帶 token

帶上 token

帶上錯(cuò)誤的 token

游客蒋得,無 token

訪問無權(quán)限的接口(vip)

該用戶已被封號
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末额衙,一起剝皮案震驚了整個(gè)濱河市窍侧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伟件,老刑警劉巖斧账,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嗓袱,居然都是意外死亡习绢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門梧却,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人大刊,你說我怎么就攤上這事『” “怎么了伴郁?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵焊傅,是天一觀的道長狈涮。 經(jīng)常有香客問我鸭栖,道長,這世上最難降的妖魔是什么松却? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任晓锻,我火速辦了婚禮飞几,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘窟社。我一直安慰自己绪钥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布匣吊。 她就那樣靜靜地躺著寸潦,像睡著了一般见转。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斩箫,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天乘客,我揣著相機(jī)與錄音,去河邊找鬼匈织。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纳决,可吹牛的內(nèi)容都是我干的弦追。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掸哑,長吁一口氣:“原來是場噩夢啊……” “哼零远!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起摔癣,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤择浊,失蹤者是張志新(化名)和其女友劉穎逾条,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體担孔,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吃警,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年酌心,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墩崩。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡完疫,死狀恐怖债蓝,靈堂內(nèi)的尸體忽然破棺而出饰迹,到底是詐尸還是另有隱情余舶,我是刑警寧澤锹淌,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站挟憔,受9級特大地震影響烟号,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜达传,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一宪赶、第九天 我趴在偏房一處隱蔽的房頂上張望脯燃。 院中可真熱鬧,春花似錦曲伊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赚哗。三九已至,卻和暖如春屿储,著一層夾襖步出監(jiān)牢的瞬間够掠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工赊堪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人哭廉。 一個(gè)月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像辽幌,于是被迫代替她去往敵國和親椿访。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 說明:本文很多觀點(diǎn)和內(nèi)容來自互聯(lián)網(wǎng)以及各種資料逛犹,如果侵犯了您的權(quán)益虽画,請及時(shí)聯(lián)系我荣病,我會刪除相關(guān)內(nèi)容。 權(quán)限管理 基...
    寇寇寇先森閱讀 7,593評論 8 76
  • 最近搞了下 Shiro 安全框架,找了一些網(wǎng)上的博客文章绍在,但是一到自己實(shí)現(xiàn)的時(shí)候就遇到了各種坑雹有,需要各種查資料看源...
    Howie_Y閱讀 35,731評論 8 182
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)溜宽,斷路器质帅,智...
    卡卡羅2017閱讀 134,652評論 18 139
  • 前言 本文主要講解的知識點(diǎn)有以下: Shiro授權(quán)的方式簡單介紹 與Spring整合 初始Shiro過濾器 一、S...
    Java3y閱讀 3,746評論 2 20
  • 戲說“風(fēng)" 風(fēng)是一種自然現(xiàn)象降铸。佛家哲學(xué)認(rèn)為它是世界的組成元素之一摇零。所謂地火水風(fēng)空。莊子認(rèn)為風(fēng)是天空中有大塊谅畅,天在大...
    若君子言閱讀 294評論 0 0