本篇文章將教大家在 shiro + springBoot 的基礎(chǔ)上整合 JWT (JSON Web Token)
如果對 shiro 如何整合 springBoot 還不了解的可以先去看我的上一篇文章 《教你 Shiro 整合 SpringBoot独令,避開各種坑》
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ù)庫表
role: 角色谣蠢;permission: 權(quán)限;ban: 封號狀態(tài)
每個(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)行了重寫
該過濾器主要有三步:
- 檢驗(yàn)請求頭是否帶有 token
((HttpServletRequest) request).getHeader("Token") != null
- 如果帶有 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;
}
- 如果在 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")
- vip 權(quán)限
AdminController(admin 可以訪問)
在接口上帶上@RequiresRoles("admin")
GuestController(所有人可以訪問)
不做權(quán)限處理