教你 Shiro + SpringBoot 整合 JWT

Howie_Y發表於2018-04-30

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

附上原始碼:github.com/HowieYuan/s…

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;
    }
複製程式碼
  1. 如果在 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)
該使用者已被封號

相關文章