上一章講了微服務下的用戶身份認證《SpringCloud Gateway 身份認證》,這次主要講如何進行鑒權。
相對上一章的身份認證代碼略有改動
Java下常用的安全框架主要有Spring Security
和shiro
节芥,都可提供非常強大的功能剧包,但學習成本較高。在微服務下鑒權多多少少都會對服務有一定的入侵性娄琉。
為了降低依賴,減少入侵吓歇,讓鑒權功能相對應用服務透明孽水,我們采用網(wǎng)關攔截資源請求的方式進行鑒權。
一城看、整體架構
用戶鑒權
模塊位于API GateWay服務中女气,所有的API資源請求都需要從此通過。
- 做身份認證测柠,通過則緩存用戶權限數(shù)據(jù)炼鞠,不通過返回
401
- 做用戶鑒權,比對當前訪問資源(URI和Method)是否在已緩存的用戶權限數(shù)據(jù)中轰胁,在則轉(zhuǎn)發(fā)請求給對應應用服務谒主,不在則返回
403
二、實現(xiàn)步驟
1. 用戶登陸
public LoginUser login(String userName, String password){
// 檢查密碼
User user = userService.checkUser(userName, password);
LoginUser loginUser = LoginUser.builder()
.userName(userName)
.realName(user.getRealName())
.userToken(UUID.randomUUID().toString())
.loginTime(new Date())
.build();
// 保存session
session.saveSession(loginUser);
// 查詢權限
List<Permission> permissions = permissionRepository.findByUserName(userName);
// 保存用戶權限到緩存中
session.saveUserPermissions(userName, permissions);
return loginUser;
}
// ...
// 緩存用戶權限到Redis
public void saveUserPermissions(String userName, List<Permission> permissions) {
String key = String.format("login:permission:%s", userName);
HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
hashOperations.putAll(key, permissions.stream().collect(
Collectors.toMap(p -> p.getMethod().concat(":").concat(p.getUri()),
Permission::getName, (k1, k2) -> k2)));
if (expireTime != null) {
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
}
}
- 用戶驗證通過后赃阀,下發(fā)
userToken
霎肯,保存當前登陸信息,緩存用戶授權列表 - 緩存授權列表時榛斯,為了方便讀取使用hash方式保存為list观游,切勿直接將數(shù)組對象保存為一個object
2. 攔截請求
@Slf4j
@Component
public class AuthorizationFilter extends AbstractGatewayFilterFactory {
@Autowired
private Session session;
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String uri = request.getURI().getPath();
String method = request.getMethodValue();
// 1.從AuthenticationFilter中獲取userName
String key = "X-User-Name";
if (!request.getHeaders().containsKey(key)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
String userName = Objects.requireNonNull(request.getHeaders().get(key)).get(0);
// 2.驗證權限
if (!session.checkPermissions(userName, uri, method)) {
log.info("用戶:{}, 沒有權限", userName);
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
return chain.filter(exchange);
};
}
}
- 第一步從取出
身份認證
模塊傳遞的X-User-Name - 第二步去緩存中檢查是否有相應的權限
public boolean checkPermissions(String userName, String uri, String method) {
String key = String.format("login:permission:%s", userName);
String hashKey = String.format("%s:%s", method, uri);
if (redisTemplate.opsForHash().hasKey(key, hashKey)){
return true;
}
String allKey = "login:permission:all";
// 權限列表中沒有則通過
return !redisTemplate.opsForHash().hasKey(allKey, hashKey);
}
-
權限列表中沒有則通過
主要是放過一些沒有必要配置的公共資源,默認都可以訪問的資源 -
login:permission:all
所有配置過的權限列表需要在程序啟動時放入緩存驮俗,并需要保持數(shù)據(jù)的更新
3. 鑒權Filter配置
spring:
cloud:
gateway:
routes:
- id: cloud-user
uri: lb://cloud-user # 后端服務名
predicates:
- Path=/user/** # 路由地址
filters:
- name: AuthenticationFilter # 身份認證
- name: AuthorizationFilter # 用戶鑒權
- StripPrefix=1 # 去掉前綴
- 特別注意filter的順序懂缕,必須先做身份認證后再進行鑒權
- 如果有較多的路由都需要配置,可使用
default-filters
默認Filter配置
三王凑、其它問題
在做單元測試時搪柑,如遇到如下錯誤
nested exception is java.lang.NoClassDefFoundError: javax/validation/ValidationException
請升級依賴包版本:
<!--升級validation-api的版本-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.5.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
四吮蛹、完整代碼
https://gitee.com/hypier/barry-cloud