知音專欄git
程序員的出路程序員
寫程序時該追求什麼,什麼是次要的?github
如何準備Java初級和高級的技術面試面試
本篇文章將教你們在 shiro + springBoot 的基礎上整合 JWT (JSON Web Token)算法
若是對 shiro 如何整合 springBoot 還不瞭解的能夠先去看個人上一篇文章 《教你 Shiro 整合 SpringBoot,避開各類坑》spring
本文的示例代碼:https://github.com/HowieYuan/shiro。數據庫
JWT後端
JSON Web Token(JWT)是一個很是輕巧的規範。這個規範容許咱們使用 JWT 在用戶和服務器之間傳遞安全可靠的信息。跨域
咱們利用必定的編碼生成 Token,並在 Token 中加入一些非敏感信息,將其傳遞。安全
一個完整的 Token :
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
在本項目中,咱們規定每次請求時,須要在請求頭中帶上 token ,經過 token 檢驗權限,如沒有,則說明當前爲遊客狀態(或者是登錄 login 接口等)
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) //建立一個新的JWT,並使用給定的算法進行標記 .sign(algorithm);
數據庫表
user
role: 角色;permission: 權限;ban: 封號狀態
role
每一個用戶有對應的角色(user,admin),權限(normal,vip),而 user 角色默認權限爲 normal, admin 角色默認權限爲 vip(固然,user 也能夠是 vip)
過濾器
在上一篇文章中,咱們使用的是 shiro 默認的權限攔截 Filter,而由於 JWT 的整合,咱們須要自定義本身的過濾器 JWTFilter,JWTFilter 繼承了 BasicHttpAuthenticationFilter,並部分原方法進行了重寫。
該過濾器主要有三步:
1.檢驗請求頭是否帶有 token ((HttpServletRequest) request).getHeader("Token") != null。
2.若是帶有 token,執行 shiro 的 login() 方法,將 token 提交到 Realm 中進行檢驗;若是沒有 token,說明當前狀態爲遊客狀態(或者其餘一些不須要進行認證的接口)
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException { //判斷請求的請求頭是否帶上 "Token" if (((HttpServletRequest) request).getHeader("Token") != null) { //若是存在,則進入 executeLogin 方法執行登入,檢查 token 是否正確 try { executeLogin(request, response); return true; } catch (Exception e) { //token 錯誤 responseError(response, e.getMessage()); } } //若是請求頭不存在 Token,則多是執行登錄操做或者是遊客狀態訪問,無需檢查 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 校驗的過程當中出現錯誤,如 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 是否有效,用戶是否存在,以及用戶的封號狀況。
權限認證
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //得到該用戶角色 String role = userMapper.getRole(username); //每一個角色擁有默認的權限 String rolePermission = userMapper.getRolePermission(username); //每一個用戶能夠設置新的權限 String permission = userMapper.getPermission(username); Set<String> roleSet = new HashSet<>(); Set<String> permissionSet = new HashSet<>(); //須要將 role, permission 封裝到 Set 做爲 info.setRoles(), info.setStringPermissions() 的參數 roleSet.add(role); permissionSet.add(rolePermission); permissionSet.add(permission); //設置該用戶擁有的角色和權限 info.setRoles(roleSet); info.setStringPermissions(permissionSet);
利用 token 中得到的 username,分別從數據庫查到該用戶所擁有的角色,權限,存入 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; }
權限控制註解
@RequiresRoles, @RequiresPermissions,這兩個註解爲咱們主要的權限控制註解, 如 // 擁有 admin 角色能夠訪問 @RequiresRoles("admin") // 擁有 user 或 admin 角色能夠訪問 @RequiresRoles(logical = Logical.OR, value = {"user", "admin"}) // 擁有 vip 和 normal 權限能夠訪問 @RequiresPermissions(logical = Logical.AND, value = {"vip", "normal"}) // 擁有 user 或 admin 角色,且擁有 vip 權限能夠訪問 @GetMapping("/getVipMessage") @RequiresRoles(logical = Logical.OR, value = {"user", "admin"}) @RequiresPermissions("vip") public ResultMap getVipMessage() { return resultMap.success().code(200).message("成功得到 vip 信息!"); }
當咱們寫的接口擁有以上的註解時,若是請求沒有帶有 token 或者帶了 token 但權限認證不經過,則會報 UnauthenticatedException 異常,可是我在 ExceptionController 類對這些異常進行了集中處理
@ExceptionHandler(ShiroException.class) public ResultMap handle401() { return resultMap.fail().code(401).message("您沒有權限訪問!"); }
這時,出現 shiro 相關的異常時則會返回
{ "result": "fail", "code": 401, "message": "您沒有權限訪問!" }
除了以上兩種,還有 @RequiresAuthentication ,@RequiresUser 等註解。
功能實現
用戶角色分爲三類,管理員 admin,普通用戶 user,遊客 guest;admin 默認權限爲 vip,user 默認權限爲 normal,當 user 升級爲 vip 權限時能夠訪問 vip 權限的頁面。
具體實現能夠看源代碼(開頭已經給出地址)
登錄
登錄接口不帶有 token,當登錄密碼,用戶名驗證正確後返回 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("您沒有權限訪問!"); } // 捕捉其餘全部異常 @ExceptionHandler(Exception.class) public ResultMap globalException(HttpServletRequest request, Throwable ex) { return resultMap.fail() .code(getStatus(request).value()) .message("訪問出錯,沒法訪問: " + ex.getMessage()); }
權限控制
UserController(user 或 admin 能夠訪問)
在接口上帶上 @RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
vip 權限
再加上@RequiresPermissions("vip")
AdminController(admin 能夠訪問)
在接口上帶上 @RequiresRoles("admin")
GuestController(全部人能夠訪問)
不作權限處理
測試結果
不帶 token
帶上 token
帶上錯誤的 token
遊客,無 token
訪問無權限的接口(vip)
該用戶已被封號
推薦大而全的【後端技術精選】