教你 Shiro + SpringBoot 整合 JWT


知音專欄
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);

數據庫表

教你 Shiro + SpringBoot 整合 JWT

user

role: 角色;permission: 權限;ban: 封號狀態

教你 Shiro + SpringBoot 整合 JWT

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(全部人能夠訪問)
不作權限處理

測試結果

教你 Shiro + SpringBoot 整合 JWT
不帶 token

教你 Shiro + SpringBoot 整合 JWT
帶上 token

教你 Shiro + SpringBoot 整合 JWT
帶上錯誤的 token

教你 Shiro + SpringBoot 整合 JWT
遊客,無 token

教你 Shiro + SpringBoot 整合 JWT
訪問無權限的接口(vip)

教你 Shiro + SpringBoot 整合 JWT
該用戶已被封號

推薦大而全的【後端技術精選】

相關文章
相關標籤/搜索