赞
踩
这个项目是自己做的一个练手项目,使用SpringCloudAlibaba微服务架构,在做鉴权模块的时候想起来之前在网上看见的Sa-Token项目,被称为国产鉴权之光故查阅了他们的文档实现了一套鉴权服务
一整套使用下来发现SaToken还是比较迎合国内程序员的编码习惯,与SpringSecurity那一套繁琐的过滤链责任链模式不同,SaToken主要还是依靠调方法类中的方法来实现登入,登出,鉴权等一系列流程
引入依赖,由于是微服务项目,我们用户信息必须存储在Redis上,SaToken已经为我们提供了SaToken整合Redis的依赖,无需我们手动实现代码 ,注意由于GateWay网关是基于WebFlux实现的与我们平常的MVC项目引入的依赖不同,SaToken的官方文档中也已经提及
<!--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也已经为我们留下拓展点,我们只需要实现我们自己的业务逻辑即可 代码如下
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的鉴权过滤器的时候底层会调用这两个方法来获取用户权限来鉴权
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返回给前端,还是非常方便的
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网关鉴权完毕分发请求给服务的时候,服务中的某些场景还需要使用用户信息,但是此时由于GateWay转发请求前端传来的Token已经丢失,这个时候我们可以在GateWay中增加一个全局过滤器在请求转发之前重写一次转发请求,在转发请求中携带上用户信息,这边我直接携带的就是userId了因为是内部服务调用也不用担心安全问题
`
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的使用体验还是非常不错的,不愧是国产项目,简单易上手以及提供了很多拓展点供用户拓展,大家可以试一下呀
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。