SpringCloud+Vue+Python人工智能(fastAPI,机器学习,深度学习)前后端架构各功能实现思路——SpringCloud后端——登录,网关统一鉴权

SpringCloud+Vue+Python人工智能(fastAPI,机器学习,深度学习)前后端架构各功能实现思路——主目录(持续更新):https://blog.csdn.net/grd_java/article/details/144986730
SpringCloud+Vue+Python人工智能(fastAPI,机器学习,深度学习)前后端架构各功能实现思路——SpringCloud后端——Sa-Token实现Security权限管理https://blog.csdn.net/grd_java/article/details/145013189

一、登录功能实现

1. 登录逻辑

根据我们的架构,必须确保redis和nacos的正常启动


在这里插入图片描述

只需额外添加Controller即可,登录由sa-token实现,其余相关业务前面讲rbac实现时,已经实现了


在这里插入图片描述

package com.yd_oa_java_cloud.security.controller;

import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.yd_oa_java_cloud.base.entity.enums.ResponseCodeEnum;
import com.yd_oa_java_cloud.base.entity.vo.YdOaResult;
import com.yd_oa_java_cloud.security.entity.SysRole;
import com.yd_oa_java_cloud.security.entity.SysUser;
import com.yd_oa_java_cloud.security.service.ISysUserRoleService;
import com.yd_oa_java_cloud.security.service.ISysUserService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :
 */
@RestController
@Log4j2
public class LoginController {
    @Autowired
    private ISysUserService sysUserService;
    @Autowired
    private ISysUserRoleService sysUserRoleService;
    @RequestMapping("login")
    public YdOaResult doLogin(String username, String password) {
        // 此处仅做模拟登录,真实环境应该查询数据进行登录
        if (username==null || username.isEmpty()) return YdOaResult.error().setMsg("请输入用户名");
//            SaHolder.getRequest().getParam("code");//获取验证码
        try {
            SysUser user = sysUserService.getByUsernameOne(username);
            if (user == null) return YdOaResult.ok().setMsg("用户名不存在!");
            if(user.getUsername().equals(username) && BCrypt.checkpw(password,user.getPassword())) {
                StpUtil.login(user.getId());//登录
                SaTokenInfo tokenInfo = StpUtil.getTokenInfo();//获取token
                StpUtil.getSession().set("user",user);//实体类对象存入session
                return YdOaResult.ok("登录成功").set("tokenInfo",tokenInfo);
            }
            return YdOaResult.error("登录失败,请检查用户名和密码!");
        } catch (Exception e) {
            log.error("登录时出错");
            return YdOaResult.error().setCode(ResponseCodeEnum.CODE_503.getCode()).setMsg("登录出错!");
        }
    }
    @RequestMapping("LoginUserInfo")
    public YdOaResult loginUserInfo(){
        String uid = StpUtil.getLoginId().toString();//获取登录用户id
        SysUser user = (SysUser)StpUtil.getSession().get("user");//获取当前登录用户信息
        if (user.getRoles()==null) {//如果不包含登录用户的角色信息
            List<SysRole> rolesByUserId = sysUserRoleService.getRolesByUserId(uid);//查询角色
            user.setRoles(rolesByUserId);//添加角色
        }
        return YdOaResult.ok().set("id",uid)
                .set("userInfo",user);
    }
    @RequestMapping("isLogin")
    public YdOaResult isLogin() {
        return YdOaResult.ok("是否登录:").set("isLogin",StpUtil.isLogin());
    }
    // 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo
    @RequestMapping("tokenInfo")
    public YdOaResult tokenInfo() {
        return YdOaResult.ok().set("tokenInfo",StpUtil.getTokenInfo());
    }
    // 测试注销  ---- http://localhost:8081/acc/logout
    @RequestMapping("logout")
    public YdOaResult logout() {
        StpUtil.logout();
        return YdOaResult.ok("退出登录成功");
    }
}

2. 监听器和全局异常

全局异常就是一个mvc拦截器,具体代码不写了,大家可以根据自身业务情况,自由发挥


在这里插入图片描述

package com.yd_oa_java_cloud.security.handler;

import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    // 全局异常拦截
    @ExceptionHandler
    public SaResult handlerException(Exception e) {
        e.printStackTrace();
        return SaResult.error(e.getMessage());
    }
}

SaTokenListener就是一个AOP切面,监听器会在登录相关操作进行动态代理


在这里插入图片描述

package com.yd_oa_java_cloud.security.handler;

import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import org.springframework.stereotype.Component;
/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description : 自定义侦听器的实现
 */
@Component
public class MySaTokenListener implements SaTokenListener {

    /** 每次登录时触发 */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        System.out.println("---------- 自定义侦听器实现 doLogin");
    }

    /** 每次注销时触发 */
    @Override
    public void doLogout(String loginType, Object loginId, String tokenValue) {
        System.out.println("---------- 自定义侦听器实现 doLogout");
    }

    /** 每次被踢下线时触发 */
    @Override
    public void doKickout(String loginType, Object loginId, String tokenValue) {
        System.out.println("---------- 自定义侦听器实现 doKickout");
    }

    /** 每次被顶下线时触发 */
    @Override
    public void doReplaced(String loginType, Object loginId, String tokenValue) {
        System.out.println("---------- 自定义侦听器实现 doReplaced");
    }

    /** 每次被封禁时触发 */
    @Override
    public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
        System.out.println("---------- 自定义侦听器实现 doDisable");
    }

    /** 每次被解封时触发 */
    @Override
    public void doUntieDisable(String loginType, Object loginId, String service) {
        System.out.println("---------- 自定义侦听器实现 doUntieDisable");
    }

    /** 每次二级认证时触发 */
    @Override
    public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
        System.out.println("---------- 自定义侦听器实现 doOpenSafe");
    }

    /** 每次退出二级认证时触发 */
    @Override
    public void doCloseSafe(String loginType, String tokenValue, String service) {
        System.out.println("---------- 自定义侦听器实现 doCloseSafe");
    }

    /** 每次创建Session时触发 */
    @Override
    public void doCreateSession(String id) {
        System.out.println("---------- 自定义侦听器实现 doCreateSession");
    }

    /** 每次注销Session时触发 */
    @Override
    public void doLogoutSession(String id) {
        System.out.println("---------- 自定义侦听器实现 doLogoutSession");
    }

    /** 每次Token续期时触发 */
    @Override
    public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
        System.out.println("---------- 自定义侦听器实现 doRenewTimeout");
    }
}

二、网关鉴权

1. sa-token过滤器

网关和普通微服务代码几乎一样,只不过网关这里new的是SaReactorFilter这个类


在这里插入图片描述

package com.yd_oa_java_cloud.gateway.filter;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.yd_oa_java_cloud.base.entity.enums.ResponseCodeEnum;
import com.yd_oa_java_cloud.base.entity.vo.YdOaResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :
 * [Sa-Token 权限认证] 配置类
 */
@Configuration
@Slf4j
public class SaTokenConfigure {

    // 注册 Sa-Token全局过滤器
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        log.info("SaTokenConfigure:权限路径鉴权");
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")    /* 拦截全部path */
                // 开放地址
                .addExclude("/security/login")
                //
//                .addExclude("/security/**")
                // 鉴权方法:每次访问进入a
                .setAuth(obj -> {
                    // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
//                    SaRouter.match("/**", "/security/login", r -> StpUtil.checkLogin());

                    // 权限认证 -- 不同模块, 校验不同权限
                    SaRouter.match("/security/sysUser/**", r -> StpUtil.checkLogin());
                    SaRouter.match("/security/sysUserRole/**", r -> StpUtil.checkPermission("security"));
                    SaRouter.match("/security/sysMenu/**", r -> StpUtil.checkLogin());
                    // 更多匹配 ...  */
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    // 打印堆栈,以供调试
                    e.printStackTrace();
                    if (e instanceof NotLoginException){
                        NotLoginException nle = (NotLoginException) e;
                        // 判断场景值,定制化异常信息
                        Integer code = 0;
                        String message = "";
                        if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
                            code = ResponseCodeEnum.CODE_500.getCode();
                            message = "未能读取到有效 token";
                        }
                        else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
                            code = ResponseCodeEnum.CODE_50008.getCode();
                            message = "token 无效";
                        }
                        else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
                            code = ResponseCodeEnum.CODE_50014.getCode();
                            message = "token 已过期";
                        }
                        else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
                            code = ResponseCodeEnum.CODE_50012.getCode();
                            message = "token 已被顶下线";
                        }
                        else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
                            code = ResponseCodeEnum.CODE_50012.getCode();
                            message = "token 已被踢下线";
                        }
                        else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {
                            code = ResponseCodeEnum.CODE_500.getCode();
                            message = "token 已被冻结";
                        }
                        else if(nle.getType().equals(NotLoginException.NO_PREFIX)) {
                            code = ResponseCodeEnum.CODE_50008.getCode();
                            message = "未按照指定前缀提交 token";
                        }
                        else {
                            code = ResponseCodeEnum.CODE_401.getCode();
                            message = "当前会话未登录";
                        }
                        // 返回给前端
                        return YdOaResult.code(code).setMsg(message);
                    }else{
                        return SaResult.error().setMsg(e.getMessage());
                    }

                })
                // 前置函数:在每次认证函数之前执行
                .setBeforeAuth(obj -> {

                    // 获得客户端domain
                    SaRequest request = SaHolder.getRequest();
                    String origin = request.getHeader("Origin");
                    if (origin == null) {
                        origin = request.getHeader("Referer");
                    }

                    // ---------- 设置跨域响应头 ----------
                    SaHolder.getResponse()
                            // 允许第三方 Cookie
                            .setHeader("Access-Control-Allow-Credentials", "true")
                            // 允许指定域访问跨域资源
                            .setHeader("Access-Control-Allow-Origin", origin)
                            // 允许所有请求方式
//                            .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, DELETE")
                            .setHeader("Access-Control-Allow-Methods", "*")
                            // 允许的header参数
//                            .setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken,authorization")
                            .setHeader("Access-Control-Allow-Headers", "*")
                            // 有效时间
                            .setHeader("Access-Control-Max-Age", "3600")
                    ;

                    // 如果是预检请求,则立即返回到前端
                    SaRouter.match(SaHttpMethod.OPTIONS)
                            .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
                            .back();
                })
                ;
    }
}

2. 内部服务外网隔离

我们有了网关,要通过网关统一鉴权,同时,不可以绕过网关直接访问子级服务。


Sa-Token中提供了Same-Token机制,只需要配置全局拦截器即可。每次网关转发请求都添加same-token


还记得我们配置gateway网关时,配置了全局过滤器,但是没有写逻辑吗,这里就是填坑的时刻


在这里插入图片描述

package com.yd_oa_java_cloud.gateway.filter;

import cn.dev33.satoken.same.SaSameUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :  openFeign 调用security微服务相关接口
 * gateway 全局过滤器,可以调用spring Security微服务进行鉴权
 * implements GlobalFilter 实现GlobalFilter接口,实现全局过滤器。可以有多个
 * implements Ordered 实现过滤器的先后顺序,如果有多个过滤器,通过此接口规定每个过滤器的先后顺序
 */
@Component//组件组成,交由SpringIOC容器管理
@Slf4j //日志
public class GateWayGlobalRequestFilter implements GlobalFilter , Ordered {
    //全局过滤器,为请求添加 Same-Token
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String rawPath = exchange.getRequest().getURI().getRawPath();//获取请求路径
        log.info("GateWayGlobalRequestFilter:"+rawPath);
        ServerHttpRequest newRequest = exchange
                .getRequest()
                .mutate()
                // 为请求追加 Same-Token 参数
                .header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
                .build();
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
        return chain.filter(newExchange);
        //过滤完成,可以向下传递
//        return chain.filter(exchange);
    }
    //定义此过滤器为所有过滤器的第0层过滤器
    @Override
    public int getOrder() {
        return 0;
    }
}

同时子级服务(哪个需要实现不允许外部直接访问的功能就配置哪个)也要添加校验same-token的机制,其实就是一个webMVC过滤器


在这里插入图片描述

package com.yd_oa_java_cloud.security.config;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.same.SaSameUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :  Sa-Token 权限认证 配置类
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    //SaTokenConfigure 配置注解拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }
    // 注册 Sa-Token 全局过滤器
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                .addInclude("/**")
//                .addExclude("/favicon.ico")
                .setAuth(obj -> {
                    // 校验 Same-Token 身份凭证     —— 以下两句代码可简化为:SaSameUtil.checkCurrentRequestToken();
                    String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);
                    SaSameUtil.checkToken(token);
                })
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                })
                ;
    }
}

3. 权限与角色

我们过滤请求时,需要获取当前用户的角色和权限,此时需要调用子级服务security中内容,就需要feign组件的登场了

3.1 feign组件依赖和配置

1. 引入依赖


在这里插入图片描述

<!--        远程调用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
<!--        远程调用连接池-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>

yml文件中需要配置启用连接池,否则很慢的(推荐配置到nacos配置中心)


feign:
    okhttp:
        enabled: true #feign组件

3.2 配置服务间内部调用鉴权

前面搞了内部服务外网隔离,此时子服务每次有人请求都要校验same-token


而远程调用可不走全局过滤器,也就是我们上面对于网关的操作是没办法在远程调用的时候为我们加上same-token的

很简单,构建一个Feign的拦截器就可以了


在这里插入图片描述

package com.yd_oa_java_cloud.gateway.filter;

import cn.dev33.satoken.same.SaSameUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

/**
 * _*_ coding : utf-8 _*_
 *
 * @Time : 2025/1/16 0016 14:38
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : FeignInterceptor
 * @Project : yd_oa_java_cloud
 * @Description : feign拦截器, 在feign请求发出之前,加入一些操作
 */
@Component
public class FeignInterceptor implements RequestInterceptor {
    // 为 Feign 的 RCP调用 添加请求头Same-Token
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());

        // 如果希望被调用方有会话状态,此处就还需要将 satoken 添加到请求头中
        // requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue());
    }
}

3.3 启动类配置feign的启用

在这里插入图片描述


添加两个注解即可


@EnableFeignClients
@EnableDiscoveryClient

3.4 配置feign远程调用client

注意配置上拦截器哦

在这里插入图片描述

package com.yd_oa_java_cloud.gateway.feign.security;

import com.yd_oa_java_cloud.base.entity.vo.YdOaResult;
import com.yd_oa_java_cloud.gateway.filter.FeignInterceptor;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :  openFeign 调用security微服务相关接口
 */
@FeignClient(
        name = "yd-oa-java-security",// 服务名称
        configuration = FeignInterceptor.class        // 请求拦截器 (关键代码)
        //fallbackFactory = SpCfgInterfaceFallback.class    // 服务降级处理
)
public interface SecuritySysUserRoleClient {
    @GetMapping("sysUserRole/getById/{uid}")
    public YdOaResult getById(@PathVariable String uid);
    //根据用户id获取菜单
    @GetMapping("sysMenu/getMenusByUserId/{id}")
    public YdOaResult getMenusByUserId(@PathVariable String id);
}

3.5 手动注入HttpMessageConverters

Spring Cloud Gateway是基于WebFlux的,是ReactiveWeb,所以异步和并发时,HttpMessageConverters不会自动注入

记住在gateway这样的响应式网关,是无法直接使用同步请求的,要求用异步进行。强行用同步会直接报错,虽然可以配置为强行使用同步,但是为什么我们不用异步遵循响应式编程呢?


在这里插入图片描述

package com.yd_oa_java_cloud.gateway.config;

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;

/**
 * _*_ coding : utf-8 _*_
 *
 * @Time : 2025/1/15 0015 10:13
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : FeignConfig
 * @Project : yd_oa_java_cloud
 * @Description : gateway配置类,Spring Cloud Gateway是基于WebFlux的,是ReactiveWeb,所以异步和并发时,HttpMessageConverters不会自动注入。
 */
@Configuration
public class FeignConfig {
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

3.6 实现鉴权接口

这里就正式到了,feign远程调用,注意上面说到的,不可以同步远程调用,要用异步


可以使用CompletableFuture类进行异步任务编排,然后通过completableFuture.get()方法执行编排好的任务,并等待它响应成功


在这里插入图片描述

package com.yd_oa_java_cloud.gateway.service;

import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import com.yd_oa_java_cloud.base.entity.enums.ResponseCodeEnum;
import com.yd_oa_java_cloud.base.entity.vo.YdOaResult;
import com.yd_oa_java_cloud.base.exception.BusinessException;
import com.yd_oa_java_cloud.gateway.feign.security.SecuritySysUserRoleClient;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description : 配置当前登录用户的角色和权限列表
 */
@Component
@Log4j2
public class StpInterfaceImpl implements StpInterface {
    @Autowired
    @Lazy
    private SecuritySysUserRoleClient securitySysUserRoleClient;
    //获取权限列表
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        String id = loginId.toString();
        //并发异步任务编排
        CompletableFuture<YdOaResult> completableFuture = CompletableFuture.supplyAsync(() -> {
            return securitySysUserRoleClient.getMenusByUserId(id);
        });
        //执行编排
        try {
            YdOaResult ydOaResult = completableFuture.get();

            if (!Objects.equals(ydOaResult.getCode(), ResponseCodeEnum.CODE_20000.getCode()))
                throw new BusinessException(ydOaResult.getCode(),ydOaResult.getMsg());

            List data = (List) ydOaResult.get("menus");
            ArrayList<String> list = new ArrayList<>();
            for (Object object:data) {
                HashMap<String, String> map = (HashMap<String, String>) object;
                String name = map.get("permission");
                list.add(name);
            }
            // 返回此 loginId 拥有的权限列表
            return list;
        } catch (InterruptedException e) {
            log.error("getPermissionList并发出错!!————",e);
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            log.error("getPermissionList并发出错!!————",e);
            throw new RuntimeException(e);
        } catch (BusinessException e){
            log.error("getPermissionList并发出错!!————",e.getCode(),"————",e.getMessage());
            throw e;
        }

    }
    //获取角色列表
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        String uid = StpUtil.getLoginId().toString();
        //并发异步任务编排
        CompletableFuture<YdOaResult> completableFuture = CompletableFuture.supplyAsync(() -> {
            return securitySysUserRoleClient.getById(uid);
        });
        //执行编排
        try {
            YdOaResult ydOaResult = completableFuture.get();

            if (!Objects.equals(ydOaResult.getCode(), ResponseCodeEnum.CODE_20000.getCode()))
                throw new BusinessException(ydOaResult.getCode(),ydOaResult.getMsg());

            List data = (List) ydOaResult.getData();
            ArrayList<String> list = new ArrayList<>();
            for (Object object:data) {
                HashMap<String, String> map = (HashMap<String, String>) object;
                String name = map.get("name");
                list.add(name);
            }
            // 返回此 loginId 拥有的角色列表
            return list;
        } catch (InterruptedException e) {
            log.error("getRoleList并发出错!!————",e);
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            log.error("getRoleList并发出错!!————",e);
            throw new RuntimeException(e);
        } catch (BusinessException e){
            log.error("getRoleList并发出错!!————",e.getCode(),"————",e.getMessage());
            throw e;
        }
    }

}

4. SaToken全局异常处理

常用的就没必要try-catch了


在这里插入图片描述

package com.yd_oa_java_cloud.gateway.exception;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.exception.NotPermissionException;
import com.yd_oa_java_cloud.base.entity.enums.ResponseCodeEnum;
import com.yd_oa_java_cloud.base.entity.vo.YdOaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :  SaToken全局异常处理
 */
@RestControllerAdvice
public class SaTokenGlobalException {
    @ExceptionHandler(NotLoginException.class)
    public YdOaResult handlerException(NotLoginException e) {//未登录全局异常
        ResponseCodeEnum res = ResponseCodeEnum.CODE_401;
        return YdOaResult
                .code(res.getCode())
                .setMsg(res.getMsg());
    }

    @ExceptionHandler(NotRoleException.class)
    public YdOaResult handlerException(NotRoleException e) {//缺少角色
        ResponseCodeEnum role = ResponseCodeEnum.CODE_403Role;
        return YdOaResult
                .code(role.getCode())
                .setMsg(role.getMsg()+e.getRole());
    }

    @ExceptionHandler(NotPermissionException.class)
    public YdOaResult handlerException(NotPermissionException e) {//缺少权限
        ResponseCodeEnum permission = ResponseCodeEnum.CODE_403Permission;
        return YdOaResult
                .code(permission.getCode())
                .setMsg(permission.getMsg()+e.getPermission());
    }
}

5. 子级服务内部注解鉴权

多此一举!

除了网关统一鉴权外,如果你还想要子级服务内进行鉴权,也可以使用注解,但是需要再次实现鉴权接口


1. 保证配置了注解拦截器


在这里插入图片描述

前面的内部服务外网隔离中已经配置过了,这里不多赘述

2. 调用相关接口,获取当前登录用户的权限和角色列表,也可以在网关转发之前,例如添加same-token的时候,顺便将角色和权限列表加进去


在这里插入图片描述

package com.yd_oa_java_cloud.security.handler;

import cn.dev33.satoken.stp.StpInterface;
import com.yd_oa_java_cloud.base.exception.BusinessException;
import com.yd_oa_java_cloud.security.entity.SysMenu;
import com.yd_oa_java_cloud.security.entity.SysRole;
import com.yd_oa_java_cloud.security.service.ISysMenuService;
import com.yd_oa_java_cloud.security.service.ISysUserRoleService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;

/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description : 配置当前登录用户的角色和权限列表
 */
@Component
@Log4j2
public class StpInterfaceImpl implements StpInterface {
    @Autowired
    private ISysMenuService sysMenuService;
    @Autowired
    private ISysUserRoleService sysUserRoleService;
    //获取权限列表
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {

        try {
            ArrayList<String> list = new ArrayList<>();
            List<SysMenu> menusByLoginId = sysMenuService.getMenusByLoginId();
            for (SysMenu sysMenu:menusByLoginId) {
                list.add(sysMenu.getPermission());
            }
            // 返回此 loginId 拥有的权限列表
            return list;
        } catch (BusinessException e){
            log.error("security/getPermissionList!!————",e.getCode(),"————",e.getMessage());
            throw e;
        }

    }
    //获取角色列表
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {

        try {
            List<SysRole> rolesByUserId = sysUserRoleService.getRolesByUserId((String) loginId);

            ArrayList<String> list = new ArrayList<>();
            for (SysRole object:rolesByUserId) {
                list.add(object.getName());
            }
            log.info(list);
            // 返回此 loginId 拥有的角色列表
            return list;
        }  catch (BusinessException e){
            log.error("security/getRoleList!!————",e.getCode(),"————",e.getMessage());
            throw e;
        }
    }

}

3. 需要的接口上,配置注解鉴权即可


在这里插入图片描述

6. 跨越解决

网关统一转发,因此网关satoken鉴权前,先添加跨越响应头


在这里插入图片描述

package com.yd_oa_java_cloud.gateway.filter;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.yd_oa_java_cloud.base.entity.enums.ResponseCodeEnum;
import com.yd_oa_java_cloud.base.entity.vo.YdOaResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * _*_ coding : utf-8 _*_
 * @Time : 2025/1/14 星期二 10:30
 * @Author : Taylor Sinclair(殷志鹏)
 * @File : ${NAME}
 * @Project : ${PROJECT_NAME}
 * @Description :
 * [Sa-Token 权限认证] 配置类
 */
@Configuration
@Slf4j
public class SaTokenConfigure {

    // 注册 Sa-Token全局过滤器
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        log.info("SaTokenConfigure:权限路径鉴权");
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")    /* 拦截全部path */
                // 开放地址
                .addExclude("/security/login")
                //
//                .addExclude("/security/**")
                // 鉴权方法:每次访问进入a
                .setAuth(obj -> {
                    // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
//                    SaRouter.match("/**", "/security/login", r -> StpUtil.checkLogin());

                    // 权限认证 -- 不同模块, 校验不同权限

                    SaRouter.match("/security/**", r -> StpUtil.checkPermission("security"));

                    // 更多匹配 ...  */
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    // 打印堆栈,以供调试
                    e.printStackTrace();
                    if (e instanceof NotLoginException){
                        NotLoginException nle = (NotLoginException) e;
                        // 判断场景值,定制化异常信息
                        Integer code = 0;
                        String message = "";
                        if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
                            code = ResponseCodeEnum.CODE_500.getCode();
                            message = "未能读取到有效 token";
                        }
                        else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
                            code = ResponseCodeEnum.CODE_50008.getCode();
                            message = "token 无效";
                        }
                        else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
                            code = ResponseCodeEnum.CODE_50014.getCode();
                            message = "token 已过期";
                        }
                        else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
                            code = ResponseCodeEnum.CODE_50012.getCode();
                            message = "token 已被顶下线";
                        }
                        else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
                            code = ResponseCodeEnum.CODE_50012.getCode();
                            message = "token 已被踢下线";
                        }
                        else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {
                            code = ResponseCodeEnum.CODE_500.getCode();
                            message = "token 已被冻结";
                        }
                        else if(nle.getType().equals(NotLoginException.NO_PREFIX)) {
                            code = ResponseCodeEnum.CODE_50008.getCode();
                            message = "未按照指定前缀提交 token";
                        }
                        else {
                            code = ResponseCodeEnum.CODE_401.getCode();
                            message = "当前会话未登录";
                        }
                        // 返回给前端
                        return YdOaResult.code(code).setMsg(message);
                    }else{
                        return SaResult.error().setMsg(e.getMessage());
                    }

                })
                // 前置函数:在每次认证函数之前执行
                .setBeforeAuth(obj -> {

                    // 获得客户端domain
                    SaRequest request = SaHolder.getRequest();
                    String origin = request.getHeader("Origin");
                    if (origin == null) {
                        origin = request.getHeader("Referer");
                    }

                    // ---------- 设置跨域响应头 ----------
                    SaHolder.getResponse()
                            // 允许第三方 Cookie
                            .setHeader("Access-Control-Allow-Credentials", "true")
                            // 允许指定域访问跨域资源
                            .setHeader("Access-Control-Allow-Origin", origin)
                            // 允许所有请求方式
                            .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
                            // 允许的header参数
                            .setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken,authorization")
                            // 有效时间
                            .setHeader("Access-Control-Max-Age", "3600")
                    ;

                    // 如果是预检请求,则立即返回到前端
                    SaRouter.match(SaHttpMethod.OPTIONS)
                            .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
                            .back();
                })
                ;
    }
}

三、测试

1. 使用网关访问正常


在这里插入图片描述


2. 直接请求子级服务,会报没有same-token异常


在这里插入图片描述

3. 随便登录一个没有security模块权限的用户


在这里插入图片描述


此时访问security服务接口会报错没有权限


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ydenergy_殷志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值