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 缺點
- sessio需要存在服務器內存中,這樣就不能跨實例共享。當下一次請求被分發到另一個實例的時候,就會造成重新登錄。
- session依賴於瀏覽器的cookie機制,對於移動客戶端就很難支持。移動端使用token。
- 在高併發情況下,session放在內存中受限於內存的大小
1.2 Token認證
Token認證一般用於前後端分離的項目
1.2.1 Token認證流程
Token認證 在服務端不需要存儲用戶的登錄記錄。其大概的流程是:
- 客戶端使用用戶名跟密碼請求登錄接口
- 登錄接口收到請求並驗證用戶名和密碼
- 登錄接口驗證成功後,會生成一個uuid作爲token,將用戶信息作爲值,然後保存到redis緩存中jedis.set(token, user);
- 登錄接口返回用戶信息和token
- 瀏覽器將token保存到本地
- 當請求其它接口時就攜帶token值
- 接口根據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的主要用途:
- 認證:一旦用戶登錄,後面每個請求都會包含 JWT,從而允許用戶訪問該令牌所允許的路由、服務和資源。單點登錄是當今廣泛使用 JWT 的一項功能,因爲它的開銷很小。
- 信息交換:JWT 是能夠安全傳輸信息的一種方式。通過使用公鑰/私鑰對 JWT 進行簽名認證。此外,由於簽名是使用 head 和 payload 計算的,因此你還可以驗證內容是否遭到篡改。
2.2 JWT格式
JWT 主要由三部分組成,每個部分用 . 進行分割,各個部分分別是:
- Header
- Payload
- 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的使用場景和優劣
應用場景:
- 一次性驗證:如:用戶註冊後需要發一封郵件讓其激活賬戶
- 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());
}
}
}
該過濾器主要有三步:
- 檢驗請求頭是否帶有 Token: ((HttpServletRequest) request).getHeader(“Token”)
- 如果帶有 Token ,則執行 Shiro 中的 login() 方法,該方法將導致:將 Token 提交到 Realm 中進行驗證(執行自定義的Reaml中的方法);如果沒有 Token,則說明當前狀態爲遊客狀態或者其他一些不需要進行認證的接口
- 如果在 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());
}
}
- 認證:拿到從 executeLogin() 方法中傳過來的 Token,並對 Token 檢驗是否有效、用戶是否存在以及是否封號
- 授權:從 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端口。
- 不帶 Token,訪問遊客接口:
- 不帶 Token,訪問帶有授權的用戶接口
訪問此接口,需要 user 或者 admin 角色,由於沒有攜帶 Token,會直接拋 UnauthenticatedException 異常(我上面已經指出了:這裏我後臺沒有顯示拋出),會被統一處理異常類 JwtException 捕獲到,執行 handle401() 方法。 - 訪問登錄接口,這裏我使用 zzc 用戶,他的角色爲 user
注意:是 POST 方法。這裏返回了一個 Token。也要注意 Token 的過期時間。我設置的是 5 min。進行測試時,可以設置長一點時間 - 攜帶這個 Token1,來訪問用戶接口
- 攜帶 Token1,再次訪問用戶接口(getMessage())
這個接口需要 user 或者 admin 角色,並且也需要 vip 權限。zzc 用戶 normal 權限。當然,訪問 管理員接口也是不成功滴。 - 訪問登錄接口,這裏我使用 zzc 用戶,他的角色爲 user,權限爲 vip。再次生成一個 Token2,再次訪問用戶接口(getMessage())
- 攜帶 Token2,訪問管理員接口
- 訪問登錄接口,這裏我使用 yht 用戶,他的角色爲 admin,權限爲 vip。再次生成一個 Token3,訪問管理員接口
讀者還可以演示用戶接口(getMessage()) - 演示一個 Token 過期實例。重新設置一下 Token 過期時間(很短)。這裏就用 zzc 用戶,生成 Token4,然後,訪問一個需要授權的接口
查看後臺
通過異常信息可知,Token 已過期。讀者可以在統一異常類中對此異常進行處理。
總體上看,內容比較多,但過程自我感覺還比較清晰吧。嘻嘻~~~
【參考資料】
教你 Shiro + SpringBoot 整合 JWT