SpringCloud整合Sa-Token登录认证+Gateway网关拦截

导读:本篇文章讲解 SpringCloud整合Sa-Token登录认证+Gateway网关拦截,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

Sa-Token介绍Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题

本文章框架使用:

SpringCloudAlibaba、SpringBoot2.1.13、sa-token1.30.0、redis

服务架构

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

 开始

一、首先配置网关服务

1、pom.xml

      <!-- Sa-Token 权限认证(Reactor响应式集成) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
            <version>1.30.0</version>
        </dependency>
        <!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>1.30.0</version>
        </dependency>
        <!--GateWay 网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

2、bootstrap.yml引入sa-token配置


# Sa-Token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: satoken
  # token有效期,单位秒,-1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期),单位秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false
  # 是否从cookie中读取token
  is-read-cookie: false
  # 是否从head中读取token
  is-read-head: true

3、新建类SaTokenConfigure,实现网关拦截

package com.frontop.meta.config;

import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
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.frontop.meta.util.ResultJsonUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.server.ServerWebExchange;

/**
 * @author YangBoss
 * @title: SaTokenConfigure
 * @projectName meta
 * @description: TODO
 * @date 2022/8/18 10:12
 */
@Configuration
public class SaTokenConfigure {
    // 注册 Sa-Token全局过滤器
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")
                // 开放地址
                .addExclude("/favicon.ico")
                // 鉴权方法:每次访问进入
                .setAuth(obj -> {
                    // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
                    SaRouter.match("/**", "/meta-auth/phoneLogin", r -> StpUtil.checkLogin());
                    // 角色认证 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
                    SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
                    // 权限认证 -- 不同模块, 校验不同权限
                    SaRouter.match("/meta-system/**", r -> StpUtil.checkPermission("system-no"));
                    SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
                    SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
                    SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    // 设置错误返回格式为JSON
                    ServerWebExchange exchange = SaReactorSyncHolder.getContext();
                    exchange.getResponse().getHeaders().set("Content-Type", "application/json; charset=utf-8");
//                    return new ResultJsonUtil().fail(e.getMessage());
                    return SaResult.error(e.getMessage());
                })
                .setBeforeAuth(obj -> {
                    // ---------- 设置跨域响应头 ----------
                    SaHolder.getResponse()
                            // 允许指定域访问跨域资源
                            .setHeader("Access-Control-Allow-Origin", "*")
                            // 允许所有请求方式
                            .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
                            // 有效时间
                            .setHeader("Access-Control-Max-Age", "3600")
                            // 允许的header参数
                            .setHeader("Access-Control-Allow-Headers", "*");

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


}

4、新建全局异常处理类GlobalException

package com.frontop.meta.config;

import cn.dev33.satoken.exception.DisableLoginException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import com.frontop.meta.constant.ResponseCodeConstant;
import com.frontop.meta.constant.ResponseMessageConstant;
import com.frontop.meta.util.ResultJsonUtil;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author YangBoss
 * @title: GlobalException
 * @projectName meta
 * @description: 拦截全局异常类
 * @date 2022/8/19 15:39
 */
public class GlobalException {
    // 全局异常拦截(拦截项目中的所有异常)
    @ResponseBody
    @ExceptionHandler
    public ResultJsonUtil<Object> handlerException(Exception e) {

        // 打印堆栈,以供调试
//        System.out.println("全局异常---------------");
        e.printStackTrace();

        // 不同异常返回不同状态码
        ResultJsonUtil<Object> re = null;
        if (e instanceof NotLoginException) {	// 如果是未登录异常
            NotLoginException ee = (NotLoginException) e;
            re =  new ResultJsonUtil().customized(
                    ResponseCodeConstant.OAUTH_TOKEN_FAILURE,
                    ResponseMessageConstant.OAUTH_TOKEN_MISSING,
                    null
            );
        }
        else if(e instanceof NotRoleException) {		// 如果是角色异常
            NotRoleException ee = (NotRoleException) e;
            re =  new ResultJsonUtil().customized(
                    ResponseCodeConstant.OAUTH_TOKEN_DENIED,
                    "无此角色:" + ee.getRole(),
                    null
            );
        }
        else if(e instanceof NotPermissionException) {	// 如果是权限异常
            NotPermissionException ee = (NotPermissionException) e;
            re =  new ResultJsonUtil().customized(
                    ResponseCodeConstant.OAUTH_TOKEN_DENIED,
                    "无此角色:" + ee.getCode(),
                    null
            );
        }
        else if(e instanceof DisableLoginException) {	// 如果是被封禁异常
            DisableLoginException ee = (DisableLoginException) e;
            re =  new ResultJsonUtil().customized(
                    ResponseCodeConstant.USER_LOCK,
                    "账号被封禁:" + ee.getDisableTime() + "秒后解封",
                    null
            );
        }
        else {	// 普通异常, 输出:500 + 异常信息
            re =  new ResultJsonUtil().fail(e.getMessage());
        }

        // 返回给前端
        return re;
    }
}

5、新建类StpInterfaceImpl,实现获取当前账号权限角色集合

package com.frontop.meta.config;

import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * @author YangBoss
 * @title: StpInterfaceImpl
 * @projectName meta
 * @description: TODO
 * @date 2022/8/18 10:26
 */
@Component
public class StpInterfaceImpl implements StpInterface {
    /**
    *当前全部是模拟数据,真实情况使用根据loginId动态查询对应角色和权限
    */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的权限列表
        List<String> strs = new ArrayList<>();
        strs.add("system");
        return strs;
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的角色列表
        List<String> strs = new ArrayList<>();
        strs.add("admin");
        return strs;
    }

}

二、配置授权服务

1、pom.xml

 <!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.30.0</version>
        </dependency>


        <!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>1.30.0</version>
        </dependency>

2、bootstarp.yml引入sa-token配置


# Sa-Token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: satoken
  # token有效期,单位秒,-1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期),单位秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false
  # 是否从cookie中读取token
  is-read-cookie: false
  # 是否从head中读取token
  is-read-head: true

3、编写一个简单的登录

package com.frontop.meta.controller;

import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.frontop.meta.util.ResultJsonUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author YangBoss
 * @title: UserLoginController
 * @projectName meta
 * @description: TODO
 * @date 2022/8/19 14:44
 */
@RestController
@Api(tags = "用户授权登录")
public class UserLoginController {

    @ApiOperation(value = "手机+密码登录")
    @PostMapping("/phoneLogin")
    public ResultJsonUtil<Object> getAwardCount(String phone,String password) {
        
        if(phone.equals("18874288923") && password.equals("123")){
            StpUtil.login(1001,"PC");
            return new ResultJsonUtil().success(StpUtil.getTokenInfo());
        }
        return new ResultJsonUtil().fail("手机号或密码错误");
    }


}

三、测试效果

1、启动网关服务和授权服务后调用登录接口

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

 redis中

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

 到这里简单的登录就完成了

2、在system业务服务中简单配置一个测试接口

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

 system业务服务中也需要引入sa-token,bootsrap.yml配置都是一样的

<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:http://sa-token.dev33.cn/ -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
            <version>1.30.0</version>
        </dependency>
        <!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>1.30.0</version>
        </dependency>

3、不携带token值访问接口

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

 4、携带token访问 

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

这里报无权限的原因就是网关实现了拦截,在上面配置中网关配置了meta-system路由的权限必须使用system-no

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

而我们在添加权限集合时候没有该权限所以被拦截 

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

角色拦截配置也是类似

四、使用注解拦截

1、如果想使用注解拦截,只能写在业务服务的接口层

2、首先要在业务服务中开启注解拦截配置

package com.frontop.meta;

import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author YangBoss
 * @title: SaTokenConfigure
 * @projectName meta
 * @description: TODO
 * @date 2022/9/7 10:53
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册Sa-Token的注解拦截器,打开注解式鉴权功能
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
        registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
    }
}

3、也需要配置获取角色和权限的集合类

package com.frontop.meta;

import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * @author YangBoss
 * @title: StpInterfaceImpl
 * @projectName meta
 * @description: TODO
 * @date 2022/8/18 10:26
 */
@Component
public class StpInterfaceImpl implements StpInterface {

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的权限列表
        List<String> strs = new ArrayList<>();
        strs.add("system-no");
        return strs;
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的角色列表
        List<String> strs = new ArrayList<>();
        strs.add("admin");
        return strs;
    }

}

4、接口配置注解拦截

@RestController
@RequestMapping("/test")
@Api(tags = "测试")
public class TestContorller {

    @ApiOperation(value = "请求汇总",consumes = "application/json;charset=UTF-8")
    @RequestMapping(value = "/apiGather", method = RequestMethod.POST)
    @SaCheckRole("super-admin2")//必须拥有该角色可访问
    @SaCheckPermission("system-no")//必须拥有该权限可访问
    public ResultJsonUtil<Object> apiGather(){
        return new ResultJsonUtil().success("111");
    }

}

5、测试效果

SpringCloud整合Sa-Token登录认证+Gateway网关拦截

网关跨域的问题

解决:

在网关服务中创建类Cors

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.support.DefaultServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;


@Configuration
public class Cors  {

    private static final String MAX_AGE = "18000L";

    @Bean
    public WebFilter corsFilter() {
        return (ServerWebExchange ctx, WebFilterChain chain) -> {
            ServerHttpRequest request = ctx.getRequest();
            if (CorsUtils.isCorsRequest(request)) {
                ServerHttpResponse response = ctx.getResponse();
                HttpHeaders headers = response.getHeaders();
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "PUT,POST,GET,DELETE,OPTIONS");

                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,"Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,authorization,Authorization");
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
                headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
                if (request.getMethod() == HttpMethod.OPTIONS) {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }

            return chain.filter(ctx);
        };
    }
}

git地址:https://gitee.com/yangjial/meta.git

代码在frontop的分支,记得切换

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/67225.html

(1)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!