您好,登錄后才能下訂單哦!
[TOC]
在微服務(wù)的用戶認(rèn)證與授權(quán)雜談(上)一文中簡單介紹了微服務(wù)下常見的幾種認(rèn)證授權(quán)方案,并且使用JWT編寫了一個極簡demo來模擬Token的頒發(fā)及校驗(yàn)。而本文的目的主要是延續(xù)上文來補(bǔ)充幾個要點(diǎn),例如Token如何在多個微服務(wù)間進(jìn)行傳遞,以及如何利用AOP實(shí)現(xiàn)登錄態(tài)和權(quán)限的統(tǒng)一校驗(yàn)。
為了讓登錄態(tài)的檢查邏輯能夠通用,我們一般會選擇使用過濾器、攔截器以及AOP等手段來實(shí)現(xiàn)這個功能。而本小節(jié)主要是介紹使用AOP實(shí)現(xiàn)登錄狀態(tài)檢查,因?yàn)槔肁OP同樣可以攔截受保護(hù)的資源訪問請求,在對資源訪問前先做一些必要的檢查。
首先需要在項(xiàng)目中添加AOP的依賴:
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定義一個注解,用于標(biāo)識哪些方法在被訪問之前需要進(jìn)行登錄態(tài)的檢查。具體代碼如下:
package com.zj.node.usercenter.auth;
/**
* 被該注解標(biāo)記的方法都需要檢查登錄狀態(tài)
*
* @author 01
* @date 2019-09-08
**/
public @interface CheckLogin {
}
編寫一個切面,實(shí)現(xiàn)登錄態(tài)檢查的具體邏輯,代碼如下:
package com.zj.node.usercenter.auth;
import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 登錄態(tài)檢查切面類
*
* @author 01
* @date 2019-09-08
**/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CheckLoginAspect {
private static final String TOKEN_NAME = "X-Token";
private final JwtOperator jwtOperator;
/**
* 在執(zhí)行@CheckLogin注解標(biāo)識的方法之前都會先執(zhí)行此方法
*/
@Around("@annotation(com.zj.node.usercenter.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取request對象
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 從header中獲取Token
String token = request.getHeader(TOKEN_NAME);
// 校驗(yàn)Token是否合法
Boolean isValid = jwtOperator.validateToken(token);
if (BooleanUtils.isFalse(isValid)) {
log.warn("登錄態(tài)校驗(yàn)不通過,無效的Token:{}", token);
// 拋出自定義異常
throw new SecurityException("Token不合法!");
}
// 校驗(yàn)通過,可以設(shè)置用戶信息到request里
Claims claims = jwtOperator.getClaimsFromToken(token);
log.info("登錄態(tài)校驗(yàn)通過,用戶名:{}", claims.get("userName"));
request.setAttribute("id", claims.get("id"));
request.setAttribute("userName", claims.get("userName"));
request.setAttribute("role", claims.get("role"));
return joinPoint.proceed();
}
}
然后編寫兩個接口用于模擬受保護(hù)的資源和獲取token。代碼如下:
@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final JwtOperator jwtOperator;
/**
* 需要校驗(yàn)登錄態(tài)后才能訪問的資源
*/
@CheckLogin
@GetMapping("/{id}")
public User findById(@PathVariable Integer id) {
log.info("get request. id is {}", id);
return userService.findById(id);
}
/**
* 模擬生成token
*
* @return token
*/
@GetMapping("gen-token")
public String genToken() {
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", 1);
userInfo.put("userName", "小眀");
userInfo.put("role", "user");
return jwtOperator.generateToken(userInfo);
}
}
最后我們來進(jìn)行一個簡單的測試,看看訪問受保護(hù)的資源時是否會先執(zhí)行切面方法來檢查登錄態(tài)。首先啟動項(xiàng)目獲取token:
在訪問受保護(hù)的資源時在header中帶上token:
訪問成功,此時控制臺輸出如下:
Tips:
這里之所以沒有使用過濾器或攔截器來實(shí)現(xiàn)登錄態(tài)的校驗(yàn),而是采用了AOP,這是因?yàn)槭褂肁OP寫出來的代碼比較干凈并且可以利用自定義注解實(shí)現(xiàn)可插拔的效果,例如訪問某個資源不用進(jìn)行登錄態(tài)檢查了,那么只需要把
@CheckLogin
注解給去掉即可。另外就是AOP屬于比較重要的基礎(chǔ)知識,也是在面試中經(jīng)常被問到的知識點(diǎn),通過這個實(shí)際的應(yīng)用例子,可以讓我們對AOP的使用技巧有一定的了解。當(dāng)然也可以選擇過濾器或攔截器來實(shí)現(xiàn),沒有說哪種方式就是最好的,畢竟這三種方式都有各自的特性和優(yōu)缺點(diǎn),需要根據(jù)具體的業(yè)務(wù)場景來選擇。
在微服務(wù)架構(gòu)中通常會使用Feign來調(diào)用其他微服務(wù)所提供的接口,若該接口需要對登錄態(tài)進(jìn)行檢查的話,那么就得傳遞當(dāng)前客戶端請求所攜帶的Token。而默認(rèn)情況下Feign在請求其他服務(wù)的接口時,是不會攜帶任何額外信息的,所以此時我們就得考慮如何在微服務(wù)之間傳遞Token。
讓Feign實(shí)現(xiàn)Token的傳遞還是比較簡單的,主要有兩種方式,第一種是使用Spring MVC的@RequestHeader
注解。如下示例:
@FeignClient(name = "order-center")
public interface OrderCenterService {
@GetMapping("/orders/{id}")
OrderDTO findById(@PathVariable Integer id,
@RequestHeader("X-Token") String token);
}
Controller里的方法也需要使用這個注解來從header中獲取Token,然后傳遞給Feign。如下:
@RestController
@RequiredArgsConstructor
public class TestController {
private final OrderCenterService orderCenterService;
@GetMapping("/{id}")
public OrderDTO findById(@PathVariable("id") Integer id,
@RequestHeader("X-Token") String token) {
return orderCenterService.findById(id, token);
}
}
從上面這個例子可以看出,使用@RequestHeader
注解的優(yōu)點(diǎn)就是簡單直觀,而缺點(diǎn)也很明顯。當(dāng)只有一兩個接口需要傳遞Token時,這種方式還是可行的,但如果有很多個遠(yuǎn)程接口需要傳遞Token的話,那么每個方法都得加上這個注解,顯然會增加很多重復(fù)的工作。
所以第二種傳遞Token的方式更為通用,這種方式是通過實(shí)現(xiàn)一個Feign的請求攔截器,然后在攔截器中獲取當(dāng)前客戶端請求所攜帶的Token并添加到Feign的請求header中,以此實(shí)現(xiàn)Token的傳遞。如下示例:
package com.zj.node.contentcenter.feignclient.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 請求攔截器,實(shí)現(xiàn)在服務(wù)間傳遞Token
*
* @author 01
* @date 2019-09-08
**/
public class TokenRelayRequestInterceptor implements RequestInterceptor {
private static final String TOKEN_NAME = "X-Token";
@Override
public void apply(RequestTemplate requestTemplate) {
// 獲取當(dāng)前的request對象
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 從header中獲取Token
String token = request.getHeader(TOKEN_NAME);
// 傳遞token
requestTemplate.header(TOKEN_NAME,token);
}
}
然后需要在配置文件中,配置該請求攔截器的包名路徑,不然不會生效。如下:
# 定義feign相關(guān)配置
feign:
client:
config:
# default即表示為全局配置
default:
requestInterceptor:
- com.zj.node.contentcenter.feignclient.interceptor.TokenRelayRequestInterceptor
除了Feign以外,部分情況下有可能會使用RestTemplate來請求其他服務(wù)的接口,所以本小節(jié)也介紹一下,在使用RestTemplate的情況下如何實(shí)現(xiàn)Token的傳遞。
RestTemplate也有兩種方式可以實(shí)現(xiàn)Token的傳遞,第一種方式是請求時使用exchange()
方法,因?yàn)樵摲椒梢越邮説eader。如下示例:
@RestController
@RequiredArgsConstructor
public class TestController {
private final RestTemplate restTemplate;
@GetMapping("/{id}")
public OrderDTO findById(@PathVariable("id") Integer id,
@RequestHeader("X-Token") String token) {
// 傳遞token
HttpHeaders headers = new HttpHeaders();
headers.add("X-Token", token);
return restTemplate.exchange(
"http://order-center/orders/{id}",
HttpMethod.GET,
new HttpEntity<>(headers),
OrderDTO.class,
id).getBody();
}
}
另一種則是實(shí)現(xiàn)ClientHttpRequestInterceptor
接口,該接口是RestTemplate的攔截器接口,與Feign的攔截器類似,都是用來實(shí)現(xiàn)通用邏輯的。具體代碼如下:
public class TokenRelayRequestInterceptor implements ClientHttpRequestInterceptor {
private static final String TOKEN_NAME = "X-Token";
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 獲取當(dāng)前的request對象
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest servletRequest = attributes.getRequest();
// 從header中獲取Token
String token = servletRequest.getHeader(TOKEN_NAME);
// 傳遞Token
request.getHeaders().add(TOKEN_NAME,token);
return execution.execute(request, body);
}
}
最后需要將實(shí)現(xiàn)的攔截器注冊到RestTemplate中讓其生效,代碼如下:
@Configuration
public class BeanConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(
new TokenRelayRequestInterceptor()
));
return restTemplate;
}
}
在第一小節(jié)中我們介紹了如何使用AOP實(shí)現(xiàn)登錄態(tài)檢查,除此之外某些受保護(hù)的資源可能需要用戶擁有特定的權(quán)限才能夠訪問,那么我們就得在該資源被訪問之前做權(quán)限校驗(yàn)。權(quán)限校驗(yàn)功能同樣也可以使用過濾器、攔截器或AOP來實(shí)現(xiàn),和之前一樣本小節(jié)采用AOP作為示例。
這里也不做太復(fù)雜的校驗(yàn)邏輯,主要是判斷用戶是否是某個角色即可。所以首先定義一個注解,該注解有一個value,用于標(biāo)識受保護(hù)的資源需要用戶為哪個角色才允許訪問。代碼如下:
package com.zj.node.usercenter.auth;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 被該注解標(biāo)記的方法都需要檢查用戶權(quán)限
*
* @author 01
* @date 2019-09-08
**/
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {
/**
* 允許訪問的角色名稱
*/
String value();
}
然后定義一個切面,用于實(shí)現(xiàn)具體的權(quán)限校驗(yàn)邏輯。代碼如下:
package com.zj.node.usercenter.auth;
import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 權(quán)限驗(yàn)證切面類
*
* @author 01
* @date 2019-09-08
**/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AuthAspect {
private static final String TOKEN_NAME = "X-Token";
private final JwtOperator jwtOperator;
/**
* 在執(zhí)行@CheckAuthorization注解標(biāo)識的方法之前都會先執(zhí)行此方法
*/
@Around("@annotation(com.zj.node.usercenter.auth.CheckAuthorization)")
public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取request對象
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 從header中獲取Token
String token = request.getHeader(TOKEN_NAME);
// 校驗(yàn)Token是否合法
Boolean isValid = jwtOperator.validateToken(token);
if (BooleanUtils.isFalse(isValid)) {
log.warn("登錄態(tài)校驗(yàn)不通過,無效的Token:{}", token);
// 拋出自定義異常
throw new SecurityException("Token不合法!");
}
Claims claims = jwtOperator.getClaimsFromToken(token);
String role = (String) claims.get("role");
log.info("登錄態(tài)校驗(yàn)通過,用戶名:{}", claims.get("userName"));
// 驗(yàn)證用戶角色名稱是否與受保護(hù)資源所定義的角色名稱匹配
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
CheckAuthorization annotation = signature.getMethod()
.getAnnotation(CheckAuthorization.class);
if (!annotation.value().equals(role)) {
log.warn("權(quán)限校驗(yàn)不通過!當(dāng)前用戶角色:{} 允許訪問的用戶角色:{}",
role, annotation.value());
// 拋出自定義異常
throw new SecurityException("權(quán)限校驗(yàn)不通過,無權(quán)訪問該資源!");
}
log.info("權(quán)限驗(yàn)證通過");
// 設(shè)置用戶信息到request里
request.setAttribute("id", claims.get("id"));
request.setAttribute("userName", claims.get("userName"));
request.setAttribute("role", claims.get("role"));
return joinPoint.proceed();
}
}
使用的時候只需要加上該注解并且設(shè)置角色名稱即可,如下示例:
/**
* 需要校驗(yàn)登錄態(tài)及權(quán)限后才能訪問的資源
*/
@GetMapping("/{id}")
@CheckAuthorization("admin")
public User findById(@PathVariable Integer id) {
log.info("get request. id is {}", id);
return userService.findById(id);
}
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。