文章
问答
冒泡
SpringCloud+saToken实现登录及权限认证

SpringCloud+saToken实现登录及权限认证

[TOC]

1.为什么要用sa-Token.

相比于我们常用的spring security框架,sa-Token更轻,更快,更优雅.使用过spring security的人肯定知道,spring security虽然灵活性很高,可以定制化各种复杂使用场景,但也因此导致框架很重,spring security使用的过滤器链每次请求认证也会花费大量性能,同时大部分功能都要自己手写完成.想比较之下,sa-Token更轻更快,开箱即用.比如登录认证时生成token的功能,spring security需要借助jwt写大量代码,而sa-Token只需要 StpUtil.login(Object id); 一个方法调用即可. 在 Sa-Token 中,大多数功能都可以一行代码解决

2. sa-Token功能

sa-Token可以满足我们大部分常规使用功能,可以解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题.


sa-Token权限认证实现方式和日常通用的设计思路,实现方式基本一致,只不过开箱即用,使用方便


1.png

3.springcloud集成sa-token

3.1 新建springcloud项目

我们需要至少有auth和gateway模块,auth作为用户登录认证模块,gateway里面需要实现网关统一鉴权功能,项目结构如下


2.png

3.2 auth模块功能实现

auth目录结构如下:


auth.png


AuthController代码:

@RestController
@AllArgsConstructor
@RequestMapping("/auth/")
//@Api(value = "用户授权认证", tags = "授权接口")
public class AuthController {
    @PostMapping("token")
    public R<AuthInfo> doLogin(@ApiParam(value = "授权类型", required = true) @RequestParam(defaultValue = "PASSWORD", required = false) GrantType grantType,
                               @ApiParam(value = "租户ID", required = true) @RequestParam(defaultValue = "000000", required = false) String tenantId,
                               @ApiParam(value = "账号") @RequestParam(required = false) String account,
                               @ApiParam(value = "密码") @RequestParam(required = false) String password) {

        UserParameter userParameter = UserParameter.builder()
                .tenantId(tenantId)
                .account(account)
                .password(password)
                .grantType(grantType)
                .build();

        //策略模式,兼容不同的登陆方式
        IUserInfoGranter userInfoGranter = UserInfoGranterBuilder.getUserInfoGranter(grantType);
        UserInfo userInfo = userInfoGranter.grant(userParameter);
        if (userInfo == null || userInfo.getUser() == null || userInfo.getUser().getId() == null) {
            return R.fail("用户名或密码错误");
        }

        return R.data(TokenGranter.createAuthInfo(userInfo));
    }

    // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin
    @RequestMapping("isLogin")
    public String isLogin() {
        return "当前会话是否登录:" + StpUtil.isLogin();
    }
}

IUserInfoGranter

public interface IUserInfoGranter {
    UserInfo grant(UserParameter userParameter);
}

PasswordUserInfoGranter(实际应用中密码需要加密处理)

@Component
@AllArgsConstructor
public class PasswordUserInfoGranter implements IUserInfoGranter {

    @Override
    public UserInfo grant(UserParameter userParameter) {
//        String tenantId = userParameter.getTenantId();
        String account = userParameter.getAccount();
        String password = userParameter.getPassword();

        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
        if ("admin".equals(account) && "admin".equals(password)) {
            StpUtil.login(66666);
            // 第2步,获取 Token  相关参数
            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
            return UserInfo.builder()
                    .user((User.builder().id(10001L).build()))
                    .roles(Lists.newArrayList("1", "2", "3"))
                    .build();
        }
        return null;
    }
}

PhoneNumUserInfoGranter

@Component
@AllArgsConstructor
public class PhoneNumUserInfoGranter implements IUserInfoGranter {

    @Override
    public UserInfo grant(UserParameter userParameter) {
        //TODO
        return null;
    }
}

TokenGranter

public class TokenGranter {

    /**
     * 创建认证token
     *
     * @param userInfo 用户信息
     * @return token
     */
    public static AuthInfo createAuthInfo(UserInfo userInfo) {
        User user = userInfo.getUser();
        
        StpUtil.login(user.getId());
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        
        AuthInfo authInfo = new AuthInfo();
        authInfo.setUserId(user.getId());
        authInfo.setTenantId(StringUtils.toStr(user.getTenantId()));
        authInfo.setOauthId(userInfo.getOauthId());
        authInfo.setAccount(user.getAccount());
        authInfo.setUserName(user.getRealName());
        authInfo.setAuthority(userInfo.getRoles().toString().substring(1, userInfo.getRoles().toString().length() - 1));
        authInfo.setAccessToken(tokenInfo.getTokenValue());
        authInfo.setTokenType("satoken");

        return authInfo;
    }

}

UserInfoGranterBuilder

@AllArgsConstructor
public class UserInfoGranterBuilder {
    private static final Map<GrantType, IUserInfoGranter> GRANTER_POOL = new HashMap<GrantType, IUserInfoGranter>();

    static {
        GRANTER_POOL.put(GrantType.PASSWORD, ApplicationContextHolder.getContext().getBean(PasswordUserInfoGranter.class));
        GRANTER_POOL.put(GrantType.PHONENUM, ApplicationContextHolder.getContext().getBean(PhoneNumUserInfoGranter.class));
    }

    public static IUserInfoGranter getUserInfoGranter(GrantType grantType) {
        IUserInfoGranter userInfoGranter = GRANTER_POOL.get(grantType);
        if (userInfoGranter == null) {
            throw new MSException("no grantType was found");
        } else {
            return userInfoGranter;
        }
    }
}

AuthInfo

@Data
public class AuthInfo {
    @ApiModelProperty("令牌")
    private String accessToken;
    @ApiModelProperty("令牌类型")
    private String tokenType;
    @ApiModelProperty("用户ID")
    @JsonSerialize(
            using = ToStringSerializer.class
    )
    private Long userId;
    @ApiModelProperty("租户ID")
    private String tenantId;
    @ApiModelProperty("第三方系统ID")
    private String oauthId;
    @ApiModelProperty("头像")
    private String avatar = "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png";
    @ApiModelProperty("角色名")
    private String authority;
    @ApiModelProperty("用户名")
    private String userName;
    @ApiModelProperty("账号名")
    private String account;
}

User

@Data
@Builder
public class User {

    private static final long serialVersionUID = 1L;

    /**
     * 主键id
     */
    @ApiModelProperty(value = "主键")
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    private Long tenantId;
    /**
     * 编号
     */
    private String code;
    /**
     * 账号
     */
    private String account;
    /**
     * 密码
     */
    private String password;
    /**
     * 昵称
     */
    private String name;
    /**
     * 真名
     */
    private String realName;
    /**
     * 头像
     */
    private String avatar;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 手机
     */
    private String phone;
    /**
     * 生日
     */
    private Date birthday;
    /**
     * 性别
     */
    private Integer sex;
    /**
     * 角色id
     */
    private String roleId;
    /**
     * 部门id
     */
    private String deptId;
    /**
     * 部门id
     */
    private String postId;

}

UserInfo

@Data
@Builder
public class UserInfo implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 用户基础信息
     */
    @ApiModelProperty(value = "用户")
    private User user;

    /**
     * 权限标识集合
     */
    @ApiModelProperty(value = "权限集合")
    private List<String> permissions;

    /**
     * 角色集合
     */
    @ApiModelProperty(value = "角色集合")
    private List<String> roles;

    /**
     * 第三方授权id
     */
    @ApiModelProperty(value = "第三方授权id")
    private String oauthId;
}

UserParameter

@Data
@Builder
public class UserParameter {
    private String account;
    private String password;
    /**
     * 租户ID
     */
    private String tenantId;
    /**
     * 授权类型
     */
    private GrantType grantType;
    /**
     * 刷新令牌
     */
    private String refreshToken;
}

application.yml

server:
  # 端口
  port: 8100

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: satoken
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true

#数据源配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://60.204.187.101:3306/blade?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8
    username: hetu2023
    password: R9YGxWEl1m
  data:
    redis:
      ##redis 单机环境配置
      host: 60.204.187.101
      port: 3379
      password:
      database: 0
      ssl:
        enabled: false
      ##redis 集群环境配置
      #cluster:
      #  nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
      #  commandTimeout: 5000
      connect-timeout: 10s
      lettuce:
        pool:
          # 连接池最大连接数
          max-active: 200
          # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 连接池中的最大空闲连接
          max-idle: 10
          # 连接池中的最小空闲连接
          min-idle: 0

pom添加以下依赖,这里注意,我是用的时3版本的spring,如果使用的3以下的,把***boot3改成boot就好

        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot3-starter</artifactId>
            <version>1.37.0</version>
        </dependency>
        <!-- 需要引入Redis集成包,因为我们的网关和子服务主要通过Redis来同步数据  -->
        <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-redis-jackson</artifactId>
            <version>1.37.0</version>
        </dependency>
        <!-- 提供Redis连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

3.3 gateway模块功能实现

gateway目录结构如下:


gateway.png


SaTokenConfigure

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

                    // 权限认证 -- 不同模块, 校验不同权限
                    SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
                    SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
                    SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
                    SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));

                    // 更多匹配 ...  */
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                });
    }
}

StpInterfaceImpl

@Component  // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {

    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
        List<String> list = new ArrayList<String>();
        list.add("101");
        list.add("user.add");
        list.add("user.update");
        list.add("user.get");
        // list.add("user.delete");
        list.add("art.*");
        return list;
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
        List<String> list = new ArrayList<String>();
        list.add("admin");
        list.add("super-admin");
        return list;
    }
}

application.yml

server:
  port: 8080

spring:
  application:
    name: cloud-gateway-service
  cloud:
    gateway:
      routes:
        #访问http://localhost:8080/guonei就可以转发到http://news.baidu.com/guonei
        - id: payment_routes2
          uri: http://news.baidu.com/
          predicates:
            - Path=/guonei/**

        - id: auth
          uri: http://localhost:8100
          predicates:
            - Path=/auth/**
      #开启自动定位功能 结合nacos注册中心使用
#      discovery:
#        locator:
#          enabled: true
  data:
    redis:
      ##redis 单机环境配置
      host: 60.204.187.101
      port: 3379
      password:
      database: 0
      ssl:
        enabled: false
      ##redis 集群环境配置
      #cluster:
      #  nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
      #  commandTimeout: 5000
      connect-timeout: 10s
      lettuce:
        pool:
          # 连接池最大连接数
          max-active: 200
          # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 连接池中的最大空闲连接
          max-idle: 10
          # 连接池中的最小空闲连接
          min-idle: 0

pom中加入以下依赖,注意springcloud gateway使用的是Reactor 模型框架 ,需要引得依赖和mvc的不一样

        <!-- Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-reactor-spring-boot3-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>

3.4 测试

输入正确账号密码后:


login-success.png


请求无权限的路由后:


no-permission.png

springcloud
sa-token

关于作者

BenbobaBigKing
获得点赞
文章被阅读