微服务架构整合Sa-Token实现网关鉴权及鉴权服务
这个项目是自己做的一个练手项目,使用SpringCloudAlibaba微服务架构,在做鉴权模块的时候想起来之前在网上看见的Sa-Token项目,被称为国产鉴权之光故查阅了他们的文档实现了一套鉴权服务 一整套使用下来发现SaToken还是比较迎合国内程序员的编码习惯,与SpringSecurity那一套繁琐的过滤链责任链模式不同,SaToken主要还是依靠调方法类中的方法来实现登入,登出,鉴权等一系列流程
项目架构
代码实现
引入依赖,由于是微服务项目,我们用户信息必须存储在Redis上,SaToken已经为我们提供了SaToken整合Redis的依赖,无需我们手动实现代码 ,注意由于GateWay网关是基于WebFlux实现的与我们平常的MVC项目引入的依赖不同,SaToken的官方文档中也已经提及
xml
<!--Sa-token-->
<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.37.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.37.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
网关中的鉴权拦截器SaToken也已经为我们留下拓展点,我们只需要实现我们自己的业务逻辑即可 代码如下
java
package com.titi.apigateway.config;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.titi.titicommon.enums.AppHttpCodeEnum;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import java.util.stream.Collectors;
@Configuration
public class SaTokenFilterConfiguration {
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**") /* 拦截全部path */
// 开放地址
.addExclude("/favicon.ico")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由,并排除/auth/** 用于开放登录
SaRouter.match("/**", "/auth/**", r -> StpUtil.checkLogin());
// 权限认证 -- 不同模块, 校验不同权限
SaRouter.match("/passenger/**", r -> StpUtil.checkPermission("passenger"));
SaRouter.match("/driver/**", r -> StpUtil.checkPermission("driver"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// 更多匹配 ... */
})
// 异常处理方法:每次setAuth函数出现异常时进入
.setError(e -> {
return SaResult.error(e.getMessage());
});
}
/**
* 由于网关没有引入springMVC依赖,所以使用feign的时候需要手动装配messageConverters
* @param converters
* @return
*/
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
}
SaToken为了兼容不同业务场景,将获取权限与获取当前用户角色的方法也开放给我们使用者,是需要实现对应的接口再注册到Spring中即可 这里的逻辑大家根据自己的系统实现即可,一般思路是从缓存或者数据库中的表获取对应userId下的权限或者角色信息 这里获取到的权限大家Debug源码的时候可以看到请求经过SaToken的鉴权过滤器的时候底层会调用这两个方法来获取用户权限来鉴权
java
package com.titi.apigateway.config;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import com.titi.feign.client.UserServiceClient;
import com.titi.titicommon.DTO.UserPermissionDto;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 自定义权限验证接口扩展
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private UserServiceClient userServiceClient;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 拥有的权限列表
//调用远程服务获取权限列表
ResponseResult<List<UserPermissionDto>> userPermission = userServiceClient.getUserPermission(Long.parseLong(loginId.toString()));
List<UserPermissionDto> permissList = userPermission.getData();
if (CollUtil.isEmpty(permissList)){
return null;
}
return permissList.stream().map(UserPermissionDto::getPermission).collect(Collectors.toList());
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 返回此 loginId 拥有的角色列表
return null;
}
}
授权登入部分
这里我抽象除了Auth服务用于多种身份下的角色登入,因为我的系统包括乘客端,司机端,管理员端,正常来说为了系统的可拓展性我应该再加一张角色表来做权限校验,但是由于是练手项目就怎么简单怎么来了,我只是用了单权限表来做 为了保证Auth服务尽量轻量级大部分的业务逻辑我都写在User服务中使用Feign进行远程调用 从代码中可以看出来在登入逻辑校验通过后我们只需要将用户的userId交给StpUtil工具类SaToken就会帮我们做例如Token生成,Token时效性设置,Token同步Redis,生成存放Token的Cookie返回给前端,还是非常方便的
kotlin
package com.titi.auth.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import com.titi.feign.client.SMSServiceClient;
import com.titi.feign.client.UserServiceClient;
import com.titi.titicommon.DO.TitiUser;
import com.titi.titicommon.DTO.SMSUserVerifyRequest;
import com.titi.titicommon.DTO.TitiUserDto;
import com.titi.titicommon.DTO.UserVerifyRequest;
import com.titi.titicommon.constants.AuthConstants;
import com.titi.titicommon.constants.RegexConstants;
import com.titi.titicommon.enums.AppHttpCodeEnum;
import com.titi.titicommon.exception.BussinessException;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
@RestController
public class PassengerUserController {
@Autowired
private UserServiceClient userServiceClient;
@Autowired
private SMSServiceClient smsServiceClient;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 登入注册二合一接口
* @param titiUserDto
* @param httpServletRequest
* @return
*/
@PostMapping("/doLogin")
public ResponseResult doLoginOrRegister(@RequestBody TitiUserDto titiUserDto, HttpServletRequest httpServletRequest){
//调用用户服务来进行登入或者注册登入
ResponseResult login = userServiceClient.login(titiUserDto);
if (!login.getCode().equals(200)){
return ResponseResult.errorResult(login.getCode(),login.getErrorMessage());
}
Long userId = (Long) login.getData();
StpUtil.login(userId);
return ResponseResult.okResult(StpUtil.getTokenInfo());
}
@PostMapping("/logout")
public ResponseResult doLogout(){
if (StpUtil.isLogin()) {
StpUtil.logout(StpUtil.getLoginId());
}
return ResponseResult.okResult(null);
}
@PostMapping("/sendVerifyCode")
public ResponseResult sendVerifyCode(@RequestBody @Validated UserVerifyRequest userVerifyRequest, HttpServletRequest httpServletRequest){
SMSUserVerifyRequest smsUserVerifyRequest = new SMSUserVerifyRequest();
BeanUtils.copyProperties(userVerifyRequest,smsUserVerifyRequest);
smsUserVerifyRequest.setIp(httpServletRequest.getRemoteAddr());
String verifyTypeKey = AuthConstants.verifyMap.get(userVerifyRequest.getVerifyType());
if (StrUtil.isBlank(verifyTypeKey)){
throw new BussinessException(AppHttpCodeEnum.PARAM_INVALID);
}
smsUserVerifyRequest.setVerifyType(verifyTypeKey);
//60s内只能接收一次验证码,使用Redis加锁来实现
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(verifyTypeKey + userVerifyRequest.getPhone()))) {
throw new BussinessException(AppHttpCodeEnum.SMS_TIME_LIMIT);
}
//调用短信服务来发送信息
return smsServiceClient.sendVerifyCode(smsUserVerifyRequest);
}
}
GateWay重写转发请求携带Token转发给服务
有些业务场景中,我们在GateWay网关鉴权完毕分发请求给服务的时候,服务中的某些场景还需要使用用户信息,但是此时由于GateWay转发请求前端传来的Token已经丢失,这个时候我们可以在GateWay中增加一个全局过滤器在请求转发之前重写一次转发请求,在转发请求中携带上用户信息,这边我直接携带的就是userId了因为是内部服务调用也不用担心安全问题 `
java
package com.titi.apigateway.filter;
import cn.dev33.satoken.stp.StpUtil;
import com.titi.titicommon.constants.AuthConstants;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest newRequest = exchange
.getRequest()
.mutate()
.header(AuthConstants.HTTP_LOGIN_HEADER, StpUtil.getLoginId(-1L).toString())
.build();
ServerWebExchange finalRequest = exchange.mutate().request(newRequest).build();
return chain.filter(finalRequest);
}
}
结语
SaToken的使用体验还是非常不错的,不愧是国产项目,简单易上手以及提供了很多拓展点供用户拓展,大家可以试一下呀