使用Shiro + JWT 的快速入門

1. 用戶認證

進入JWT主題之前,先來回顧一下用戶認證吧。用戶認證一般分爲:Session認證、Token認證(JWT是一種特殊的Token認證)

1.1 Session認證

Session認證用於一般的Web項目中。

1.1.1 Session原理

        HTTP協議是一種無狀態協議。即:每次服務端接收客戶端的請求時,都是一個全新的請求,服務端並不知道客戶端的歷史請求。例如說:當客戶端進行賬號和密碼通過了身份認證,接着向服務端發送下一個請求,又需要驗證。而Session和Cookie的主要目的就是爲了彌補 HTTP 的無狀態特性。

        客戶端第一次請求服務端時,服務端會爲這次請求開闢一塊內存空間,創建對象(Session 對象,存儲結構爲 ConcurrentHashMap),同時生成一個 sessionId ,並通過響應頭的 Set-Cookie:JSESSIONID=XXXXXXX 命令,向客戶端發送要求設置 Cookie 的響應;客戶端收到響應後,在本機客戶端設置了一個 JSESSIONID=XXXXXXX 的 Cookie 信息,該 Cookie 的過期時間爲瀏覽器會話結束。接下來客戶端每次向同一個網站發送請求時,請求頭都會帶上該 Cookie 信息(包含 sessionId ), 然後,服務器通過讀取請求頭中的 Cookie 信息,獲取名稱爲 JSESSIONID 的值,得到此次請求的 sessionId。

服務器可以利用 Session 來存儲客戶端在同一個會話期間的一些操作記錄。

1.1.2 Session認證流程

        接下來就來簡述下Session認證流程:當客戶端進行身份認證時,如果認證通過,就在服務端的Session中生成一條記錄,這個記錄裏面包含用戶信息,然後把 JSESSIONID發送給客戶端,客戶端收到以後,就把這個 JSESSIONID存儲在 Cookie 裏,下次這個用戶再向服務端發送請求的時候,就帶着這個 Cookie 。這樣服務端會驗證這個 Cookie 裏的信息,看能否找到對應的記錄。如果可以,說明用戶已經通過了身份驗證,就把用戶請求的數據返回給客戶端。

如下圖所示:
在這裏插入圖片描述

1.1.3 代碼實現

public User login(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
	String username = request.getParameter("username");
	String password = request.getParameter("password");
	User user = userService.login(username, password);
	if (user == null) {
		throw new AuthenticationException("用戶名或密碼錯誤");
	}
	// 返回這次請求關聯的當前會話,如果沒有會話則創建一個新的
	// 需要在服務器端記錄該session
	HttpSession session = request.getSession();
	session.setAttribute("user", user);
	return user;
}
public Object getUserInfo(HttpServletRequest request){
	// 從request中獲取Cookie,從Cookie中獲取sessionid
	// 根據sessionid獲取對應的Session對象從session中獲取關聯的用戶信息
	HttpSession session = request.getSession();
	Object user = session.getAttribute("user");
	return user;
}

1.1.4 缺點

  1. sessio需要存在服務器內存中,這樣就不能跨實例共享。當下一次請求被分發到另一個實例的時候,就會造成重新登錄。
  2. session依賴於瀏覽器的cookie機制,對於移動客戶端就很難支持。移動端使用token。
  3. 在高併發情況下,session放在內存中受限於內存的大小

1.2 Token認證

Token認證一般用於前後端分離的項目

1.2.1 Token認證流程

Token認證 在服務端不需要存儲用戶的登錄記錄。其大概的流程是:

  1. 客戶端使用用戶名跟密碼請求登錄接口
  2. 登錄接口收到請求並驗證用戶名和密碼
  3. 登錄接口驗證成功後,會生成一個uuid作爲token,將用戶信息作爲值,然後保存到redis緩存中jedis.set(token, user);
  4. 登錄接口返回用戶信息和token
  5. 瀏覽器將token保存到本地
  6. 當請求其它接口時就攜帶token值
  7. 接口根據token去緩存中查,如果找到了就調用接口,如果找不到報token錯誤(一般通過攔截器來實現檢查)

如下圖所示:
在這裏插入圖片描述

1.2.2 代碼實現

public String auth(String username, String password) throws AuthenticationException {
	User user = userService.login(username, password);
	if (user == null) {
		throw new AuthenticationException("用戶名或密碼錯誤");
	}
	String token = UUID.randomUUID().toString();
	redisClient.set(token, user);
	return token;
}
public Object getUserInfo(@RequestHeader("token") String token) throws AuthenticationException {
	User user = redisClient.get(token);
	if (user == null) {
		throw new AuthenticationException("token不可用");
	}
	return user;
}

實施 Token 驗證的方法挺多的,還有一些標準方法,比如 JWT。

2. JWT

2.1 JWT簡介

        JWT 全稱 Json Web Token。它是 RFC 7519 中定義的,用於安全地將信息作爲 Json 對象進行傳輸的一種形式。JWT 中存儲的信息是經過“數字簽名”的,因此可以被信任和理解。可以使用 HMAC 算法或使用 RSA/ECDSA 的公用/專用 密鑰對 JWT 進行簽名。

JWT的主要用途:

  1. 認證:一旦用戶登錄,後面每個請求都會包含 JWT,從而允許用戶訪問該令牌所允許的路由、服務和資源。單點登錄是當今廣泛使用 JWT 的一項功能,因爲它的開銷很小。
  2. 信息交換:JWT 是能夠安全傳輸信息的一種方式。通過使用公鑰/私鑰對 JWT 進行簽名認證。此外,由於簽名是使用 head 和 payload 計算的,因此你還可以驗證內容是否遭到篡改。

2.2 JWT格式

JWT 主要由三部分組成,每個部分用 . 進行分割,各個部分分別是:

  1. Header
  2. Payload
  3. Signature

如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

2.2.1 Header

Header是 JWT 的標頭,包括兩部分信息:

  • 令牌的類型:JWT
  • 加密算法:HMAC SHA256 或 RSA

如:

{"alg": "HS256", "typ": "JWT"}

然後將 Header 進行 base64編碼 構成了第一部分:

eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9

base64編碼
Base64是一種用64個字符來表示任意二進制數據的編碼方法

Java中可以使用java.util.Base64進行編碼、解碼。如:

public class Test {
    public static void main(String[] args) throws Exception{
        Base64.Encoder encoder = Base64.getEncoder();
        Base64.Decoder decoder = Base64.getDecoder();

        String header = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}";
        byte[] headerBytes = header.getBytes();
        // 編碼
        String encodeHeader = encoder.encodeToString(headerBytes);
        System.out.println(encodeHeader);
        // 解碼
        byte[] decode = decoder.decode(encodeHeader);
        System.out.println(new String(decode, "UTF-8"));
    }
}

在這裏插入圖片描述

2.2.2 Payload

Payload 中包含一個聲明。聲明是有關實體(通常是用戶)和其他數據的聲明。共有三種類型的聲明:registered, public 和 private 聲明

2.2.2.1 registered 聲明

registered 聲明:包含一組建議使用的預定義聲明,主要包括:

  • iss: jwt簽發者
  • sub: jwt所面向的用戶
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
  • nbf: 定義在什麼時間之前,該jwt都是不可用的
  • iat: jwt的簽發時間
  • jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊。

2.2.2.2 public 聲明

public 聲明:公共的聲明,可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息,但不建議添加敏感信息,因爲該部分在客戶端可解密。

2.2.2.3 private 聲明

private 聲明:自定義聲明,旨在在同意使用它們的各方之間共享信息,既不是註冊聲明也不是公共聲明。

如:

{"sub":"1234567890","name":"John Doe","iat":1516239022}
  • name:自定義字段
  • sub/iat:標準聲明

然後將 Payload進行 base64編碼 構成了第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

2.2.3 Signature

Signature表示簽證信息,它包含三個部分:

  • header (base64後的)
  • payload (base64後的)
  • secret

比如我們需要 HMAC SHA256 算法進行簽名

HMACSHA256(
	base64UrlEncode(header) + "." +
	base64UrlEncode(payload),
	secret
)

簽名用於驗證消息在此過程中沒有更改,並且對於使用私鑰進行簽名的令牌,它還可以驗證 JWT 的發送者的真實身份

2.3 JWT的應用場景

詳細地請查看:理解JWT的使用場景和優劣

應用場景:

  1. 一次性驗證:如:用戶註冊後需要發一封郵件讓其激活賬戶
  2. restful api的無狀態認證:使用 jwt 來做 restful api 的身份認證

好了,接下來就是用Shrio集成JWT咯!

3. Shiro 集成JWT

其實,這個項目的代碼是在這篇博客-----使用SpringBoot集成Shiro的簡易教程 的代碼上進行改編的。不過,爲了各位讀者方便,這裏還是會貼出源代碼的

3.1 項目信息

3.1.1 開發環境

IDEA:2018.2(lombok插件)
SpringBoot:2.3.1.RELEASE
Shiro:1.3.2
JWT:3.2.0

3.1.2 項目結構圖

在這裏插入圖片描述

3.1.3 功能描述

        在此項目中規定:每次請求時,需要在請求頭中帶上 token ,通過 token 來檢驗權限;如果沒有帶上token,則說明當前爲遊客狀態或者其他不需要驗證的接口

3.1.4 項目依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
<!-- java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.2.0</version>
</dependency>
<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>

3.2 集成JWT

這裏引入的依賴(如上)

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.2.0</version>
</dependency>

當然還有其他的依賴包,可查看:常用的生成jwt的工具類的jar包

JwtUtil
利用 JWT 的工具類來生成token。這個工具類有生成 token 和 校驗 token 以及從token中獲取信息 ,這三個方法:

@Slf4j
public class JwtUtil {
    // 過期時間 5min
    private static final Long EXPIRE_TIME = 5 * 60 * 1000L;
    // 密鑰
    private static final String SECRET = "SHIRO+JWT";

    // 生成token,5min後過期
    public static String createToken(String username) {}
    // 驗證token
    public static boolean verify(String token, String username) {}
    // 獲取token中的信息,無需secret解密也能獲得
    public static String getUsernameFromToken(String token) {}
}

createToken()
        生成 token 時,指定 token 過期時間 EXPIRE_TIME 和簽名密鑰 SECRET,然後將 expireDate 和 username 寫入 token 中,並使用帶有密鑰的 HS256 簽名算法進行簽名

public static String createToken(String username) {
    String token = null;
    try {
        // 過期時間
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        // 加密算法
        Algorithm algorithm = Algorithm.HMAC256(SECRET);
        token = JWT.create()
                .withClaim("username", username)
                .withExpiresAt(expireDate)
                .sign(algorithm);
    } catch (UnsupportedEncodingException e) {
        log.error("Failed to create token. {}", e.getMessage());
    }
    return token;
}

verify()
驗證token,如果驗證失敗,便會拋出異常

public static boolean verify(String token, String username) {
    boolean isSuccess = false;
    try {
        Algorithm algorithm = Algorithm.HMAC256(SECRET);
        JWTVerifier verifier = JWT.require(algorithm)
                .withClaim("username", username)
                .build();
        // 驗證token
        verifier.verify(token);
        isSuccess = true;
    } catch (UnsupportedEncodingException e) {
        log.error("Token is invalid. {}", e.getMessage());
    }
    return isSuccess;
}

getUsernameFromToken()
在 createToken()方法裏,有將 username 寫入 token 中。現在可從 token 裏獲取 username

public static String getUsernameFromToken(String token) {
    try {
        DecodedJWT decode = JWT.decode(token);
        String username = decode.getClaim("username").asString();
        return username;
    } catch (JWTDecodeException e) {
        log.error("Failed to Decode jwt. {}", e.getMessage());
        return null;
    }
}

User
User 類封裝用戶信息

@Data
public class User implements Serializable {
    private String id;
    private String username;
    private String password;
    // 角色
    private String role;
    // 權限
    private String permission;
    // 封號狀態  0:未封;1:禁用
    private Integer ban;

    public User(String id, String username, String password, String role, String permission, Integer ban) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.role = role;
        this.permission = permission;
        this.ban = ban;
    }
}

UserServiceImpl
UserServiceImpl是UserService接口的實現類。UserService接口中就只有getUserByName()方法:通過用戶名獲取用戶信息

這裏沒有使用數據庫,就模擬了部分靜態數據。

@Service
public class UserServiceImpl implements UserService {
    private static Map<String, User> userMap = new HashMap<>();
    static {
        userMap.put("zzc", new User("1", "zzc", "666", "user", "normal", 0));
        userMap.put("wzc", new User("2", "wzc", "888", "user", "vip", 0));
        userMap.put("yht", new User("3", "yht", "999", "admin", "vip", 0));
    }

    @Override
    public User getUserByName(String username) {
        return userMap.get(username);
    }
}

JwtFilter
        在這篇博客----使用SpringBoot集成Shiro的簡易教程 中,使用的是 shiro 默認的權限攔截 Filter。而因爲整合了 JWT ,我們需要自定義過濾器 JWTFilter。JWTFilter 繼承了 BasicHttpAuthenticationFilter,並部分原方法進行了重寫。

@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
    // 如果請求頭帶有token,則對token進行檢查;否則,直接放行
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 判斷請求頭是否帶有 token
        if (isLoginAttempt(request, response)) {
            // 如果存在 token ,則進入executeLogin()方法執行登入,並檢測 token 的正確性
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                log.error("Error! {}", e.getMessage());
                responseError(response, e.getMessage());
            }
        }
        // 如果不存在 token ,則可能是執行登錄操作/遊客訪問狀態,所以直接放行
        return true;
    }

    // 檢測 header中是否包含 token
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        return getTokenFromRequest(request) != null;
    }

   // 執行登入操作
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        String token = getTokenFromRequest(request);
        JwtToken jwtToken = new JwtToken(token);
        // 提交給 realm 進行登入,如果錯誤,會拋出異常並捕獲
        getSubject(request, response).login(jwtToken);
        // 如果沒有拋出異常,則代表登入成功,返回 true
        return true;
    }

    // 從請求中獲取 token
    private String getTokenFromRequest(ServletRequest request) {
        HttpServletRequest req = (HttpServletRequest) request;
        return req.getHeader("Token");
    }

    // 非法請求將跳轉到 "/unauthorized/**"
    private void responseError(ServletResponse response, String message) {
        try {
            HttpServletResponse resp = (HttpServletResponse) response;
            // 設置編碼,否則中文字符在重定向時會變爲空字符串
            message = URLEncoder.encode(message, "UTF-8");
            resp.sendRedirect("/unauthorized/" + message);
        } catch (UnsupportedEncodingException e) {
            log.error("Error! {}", e.getMessage());
        } catch (IOException e) {
            log.error("Error! {}", e.getMessage());
        }
    }
}

該過濾器主要有三步:

  1. 檢驗請求頭是否帶有 Token: ((HttpServletRequest) request).getHeader(“Token”)
  2. 如果帶有 Token ,則執行 Shiro 中的 login() 方法,該方法將導致:將 Token 提交到 Realm 中進行驗證(執行自定義的Reaml中的方法);如果沒有 Token,則說明當前狀態爲遊客狀態或者其他一些不需要進行認證的接口
  3. 如果在 Token 校驗的過程中出現錯誤,如:Token 校驗失敗,那麼我會將該請求視爲認證不通過,則重定向到 /unauthorized/**

JwtToken
        這裏我們自定義了一個AuthenticationToken----JwtToken。因爲在Reaml認證方法中,我們是對 Token進行認證的。至於 UsernamePasswordToken (Shiro 中自帶),我們需要 對 username 和 password 認證時就可以用它

public class JwtToken implements AuthenticationToken {
    private String token;
    
    public JwtToken(String token) {
        this.token = token;
    }
    
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}

UserRealm
自定義的Realm

@Slf4j
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = JwtUtil.getUsernameFromToken(principalCollection.toString());
        User user = userService.getUserByName(username);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // TODO 改爲從數據庫中獲取該用戶的角色
        authorizationInfo.addRole(user.getRole());
        authorizationInfo.addStringPermission(user.getPermission());
        return authorizationInfo;
    }

    // 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("身份認證");
        // 這裏的 token是從 JWTFilter 的 executeLogin() 方法傳遞過來的
        String token = (String) authenticationToken.getCredentials();
        // 解密
        String username = JwtUtil.getUsernameFromToken(token);
        // TODO 改爲從數據庫獲取對應用戶名密碼的用戶
        User user = userService.getUserByName(username);
        if (StringUtils.isEmpty(username) || !JwtUtil.verify(token, username)) {
            log.error("token 認證失敗");
            throw new AuthenticationException("token 認證失敗");
        }
        if (null == user) {
            log.error("賬號或密碼錯誤");
            throw new AuthenticationException("賬號或密碼錯誤");
        }
        if (1 == user.getBan()) {
            log.error("該用戶已被封號");
            throw new AuthenticationException("該用戶已被封號");
        }
        log.info("用戶{}認證成功!", user.getUsername());

        return new SimpleAuthenticationInfo(token, token, getName());
    }
}
  1. 認證:拿到從 executeLogin() 方法中傳過來的 Token,並對 Token 檢驗是否有效、用戶是否存在以及是否封號
  2. 授權:從 Token 中獲取 username ,然後根據 username 可獲取用戶信息(角色、權限等)並添加到 AuthorizationInfo 中。

【注意】:認證方法中返回的對象:SimpleAuthenticationInfo(Object principal, Object credentials, String realmName),第一個參數名稱是 principal,則對應着 授權方法的入參。

ShiroConfig
設置好我們自定義的 filter,並使所有請求通過我們的過濾器,除了我們用於處理未認證請求的 /unauthorized/**

@Configuration
public class ShiroConfig {

    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());

        // 關閉 shiro 自帶的 session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(evaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 設置自定義的攔截器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        Map<String, String> filterRuleMap = new HashMap<>(16);
        // 設置所有的請求經過自定義的 filter
        filterRuleMap.put("/**", "jwt");
        filterRuleMap.put("/unauthorized/**", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }

    // 對Shiro註解的支持
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;

    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}

上述Shiro的註解,如:@RequiresRoles、@RequiresPermissions。作用於接口之上的

@RequiresRoles("admin")
@GetMapping("/enter")
public JwtResultMap enter() {
    return resultMap.success().code(200).message("Admin Enter");
}

上述接口,只有擁有 admin 角色才能訪問。

// 擁有 user 或 admin 角色,且擁有 vip 權限可以訪問
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
@RequiresPermissions("vip")

        當接口有以上的註解時,如果請求頭中沒有帶有 token 或者帶了 token 但權限認證不通過,則會報 UnauthenticatedException 異常。(這裏有一個奇怪的現象:當請求頭中沒有帶有Token,但要去訪問有權限的接口時,後臺並沒有顯示報 UnauthenticatedException 異常,但確確實實地可以捕獲到這個異常)

JwtException
對異常進行統一地處理

@RestControllerAdvice
public class JwtException {

    @Autowired
    private JwtResultMap resultMap;

    // 捕獲與shiro相關的異常
    @ExceptionHandler(ShiroException.class)
    public JwtResultMap handle401() {
        return resultMap.fail().code(401).message("您沒有權限訪問!");
    }

    // 捕獲其他異常
    @ExceptionHandler(Exception.class)
    public JwtResultMap globalException(HttpServletRequest request, Throwable e) {
        return resultMap
                .fail()
                .code(getStatus(request).value())
                .message("訪問出錯,無法訪問:" + e.getMessage());
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("java.servlet.error.status_code");
        if (null == statusCode) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return  HttpStatus.valueOf(statusCode);
    }
}

JwtResultMap
對接口中返回的數據進行統一地封裝

@Component
public class JwtResultMap extends HashMap<String, Object> {

    public JwtResultMap success() {
        this.put("result", "success");
        return this;
    }
    public JwtResultMap fail() {
        this.put("result", "fail");
        return this;
    }
    public JwtResultMap code(int code) {
        this.put("code", code);
        return this;
    }
    public JwtResultMap message(Object message) {
        this.put("message", message);
        return this;
    }
}

下面就是一些接口:

GuestController 遊客接口
不做權限處理,所有人可以訪問

@RestController
@RequestMapping("/guest")
public class GuestController {
    
    @Autowired
    private JwtResultMap resultMap;

    @GetMapping("/enter")
    public JwtResultMap enter() {
        return resultMap.success().code(200).message("歡迎進入遊客頁面" );
    }
}

UserController 用戶接口

  • enter() 方法:需要 user 或 admin 角色,才能訪問
  • getMessage() 方法:需要 user 或 admin 角色並且有 vip 權限,才能訪問
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private JwtResultMap resultMap;

    @RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
    @GetMapping("/enter")
    public JwtResultMap enter() {
        return resultMap.success().code(200).message("歡迎進入用戶頁面" );
    }

    @RequiresPermissions("vip")
    @RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
    @GetMapping("/getMessage")
    public JwtResultMap getMessage() {
        return resultMap.success().code(200).message("成功獲得 vip 信息!");
    }
}

AdminController 管理員接口

  • enter() 方法:需要 admin 角色才能訪問
@RestController
@RequestMapping("/admin")
public class AdminController {

    @Autowired
    private JwtResultMap resultMap;

    @RequiresRoles("admin")
    @GetMapping("/enter")
    public JwtResultMap enter() {
        return resultMap.success().code(200).message("Admin Enter");
    }
}

LoginController 登錄接口

  • login() 方法:任何人可以訪問。認證成功後,會返回 Token。
  • unauthorized() 方法:權限不足時,訪問的接口。
@Controller
public class LoginController {
    @Autowired
    private JwtResultMap resultMap;
    @Autowired
    private UserService userService;

    @PostMapping("/login")
    @ResponseBody
    public JwtResultMap login(String username, String password) {
        User user = userService.getUserByName(username);
        if (null == user) {
            return resultMap.fail().code(401).message("賬號錯誤");
        } else if (!password.equals(user.getPassword())) {
            return resultMap.fail().code(401).message("密碼錯誤");
        }
        return resultMap.success().code(200).message(JwtUtil.createToken(username));
    }

    @ResponseBody
    @GetMapping("/unauthorized/{message}")
    public JwtResultMap unauthorized(@PathVariable String message) {
        return resultMap.success().code(401).message(message);
    }
}

上面的接口都會通過 JwtFilter 過濾器, 除了 unauthorized,它是經過 Shiro 的默認的過濾器----anon(ShiroConfig配置有)

3.3 測試

接下來就使用PostMan對上述接口進行測試。我這裏是8081端口。

  1. 不帶 Token,訪問遊客接口:
    在這裏插入圖片描述
  2. 不帶 Token,訪問帶有授權的用戶接口
    在這裏插入圖片描述
            訪問此接口,需要 user 或者 admin 角色,由於沒有攜帶 Token,會直接拋 UnauthenticatedException 異常(我上面已經指出了:這裏我後臺沒有顯示拋出),會被統一處理異常類 JwtException 捕獲到,執行 handle401() 方法。
  3. 訪問登錄接口,這裏我使用 zzc 用戶,他的角色爲 user
    在這裏插入圖片描述
    注意:是 POST 方法。這裏返回了一個 Token。也要注意 Token 的過期時間。我設置的是 5 min。進行測試時,可以設置長一點時間
  4. 攜帶這個 Token1,來訪問用戶接口
    在這裏插入圖片描述
  5. 攜帶 Token1,再次訪問用戶接口(getMessage())
    在這裏插入圖片描述
    這個接口需要 user 或者 admin 角色,並且也需要 vip 權限。zzc 用戶 normal 權限。當然,訪問 管理員接口也是不成功滴。
  6. 訪問登錄接口,這裏我使用 zzc 用戶,他的角色爲 user,權限爲 vip。再次生成一個 Token2,再次訪問用戶接口(getMessage())
    在這裏插入圖片描述
  7. 攜帶 Token2,訪問管理員接口
    在這裏插入圖片描述
  8. 訪問登錄接口,這裏我使用 yht 用戶,他的角色爲 admin,權限爲 vip。再次生成一個 Token3,訪問管理員接口
    在這裏插入圖片描述
    讀者還可以演示用戶接口(getMessage())
  9. 演示一個 Token 過期實例。重新設置一下 Token 過期時間(很短)。這裏就用 zzc 用戶,生成 Token4,然後,訪問一個需要授權的接口
    在這裏插入圖片描述
    查看後臺
    在這裏插入圖片描述
    通過異常信息可知,Token 已過期。讀者可以在統一異常類中對此異常進行處理。

總體上看,內容比較多,但過程自我感覺還比較清晰吧。嘻嘻~~~

【參考資料】
教你 Shiro + SpringBoot 整合 JWT

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章