基于 Sa-Token 实现微服务登录鉴权实战

发布时间 2023-07-08 22:19:24作者: PinGoo

简介

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

SpringBoot 微服务实战

1、创建项目

在 IDEA 中新建一个 SpringBoot 项目,命名:sa-token-demo-springboot

2、添加依赖

在项目中添加依赖:

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

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

3、设置配置文件

在 application.yml 文件中增加如下配置,定制性使用框架:

server:
    # 端口
    port: 8081
    
############## 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

# 端口
server.port=8081
    
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############

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

4、创建启动类

在项目中新建包 com.xinyi ,在此包内新建主类 SaTokenDemoApplication.java,复制以下代码:

@SpringBootApplication
public class SaTokenDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SaTokenDemoApplication.class, args);
        System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig());
    }
}

5、创建测试 controller

@RestController
@RequestMapping("user")
public class UserController {

    /**
     * 测试登录 
     * 浏览器访问: http://localhost:8081/user/doLogin?username=admin&password=123456
     *
     * @param username 用户名
     * @param password 密码
     * @return 登录结果
     */
    @RequestMapping("doLogin")
    public String doLogin(String username, String password) {
        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
        if ("admin".equals(username) && "123456".equals(password)) {
            StpUtil.login(10001);
            return "登录成功";
        }
        return "登录失败";
    }

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

6、测试运行

项目启动日志如下:
image.png

浏览器访问: http://localhost:8081/user/doLogin?username=admin&password=123456

登录成功,返回这个用户的 Token 会话凭证

image.png

浏览器访问:http://localhost:8081/user/isLogin

登录成功,返回这个用户的 Token 会话凭证,用户后续的每次请求,都携带上这个 Token,服务器根据 Token 判断此会话是否登录成功。

image.png

登录认证

登录访问流程

用户携带用户名和密码调用登录接口,登录成功后,返回该用户Token会话凭证,用户后续的每次请求,都携带上这个Token,服务器根据 Token 判断此会话是否登录成功

API 列表

登录与注销

// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);

// 当前会话注销登录
StpUtil.logout();

// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();

// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();

会话查询

// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();

// 类似查询API还有:
StpUtil.getLoginIdAsString();    // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt();       // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong();      // 获取当前会话账号id, 并转化为`long`类型

// ---------- 指定未登录情形下返回的默认值 ----------

// 获取当前会话账号id, 如果未登录,则返回 null 
StpUtil.getLoginIdDefaultNull();

// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);

token 查询

// 获取当前会话的 token 值
StpUtil.getTokenValue();

// 获取当前`StpLogic`的 token 名称
StpUtil.getTokenName();

// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);

// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();

// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();

测试代码

@RestController
@RequestMapping("login")
public class LoginController {

    /**
     * 测试登录  
     * 浏览器访问:http://localhost:8081/login/doLogin?name=admin&pwd=123456
     *
     * @param name 用户名
     * @param pwd  密码
     * @return 登录结果
     */
    @GetMapping("doLogin")
    public SaResult doLogin(@RequestParam("name") String name, @RequestParam("pwd") String pwd) {
        if ("admin".equals(name) && "12345".equals(pwd)) {
            StpUtil.login("10001");
            return SaResult.ok(name + " 登录成功");
        }
        return SaResult.error("登录失败");
    }

    /**
     * 查询登录状态 
     * 浏览器访问:http://localhost:8081/login/isLogin
     *
     * @return 结果
     */
    @GetMapping("isLogin")
    public SaResult isLogin() {
        return SaResult.ok("是否登录:" + StpUtil.isLogin());
    }

    /**
     * 查询 Token 
     * 浏览器访问:http://localhost:8081/login/tokenInfo
     *
     * @return
     */
    @GetMapping("tokenInfo")
    public SaResult tokenInfo() {
        return SaResult.data(StpUtil.getTokenInfo());
    }

    /**
     * 测试注销  
     * 浏览器访问:http://localhost:8081/login/logout
     *
     * @return
     */
    @GetMapping("logout")
    public SaResult logout() {
        StpUtil.logout();
        return SaResult.ok();
    }
}

权限认证

1、设计思路

权限认证, 核心逻辑就是验证一个账户是否拥有指定权限
深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码

2、获取当前账户权限码合集

实现Sa-Token暴露的 StpInterface 接口

@Component
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;
    }
}

3、API 列表

权限校验 API

// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();

// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");        

// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException 
StpUtil.checkPermission("user.add");        

// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");        

// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");

扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

角色校验 API

// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();

// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");        

// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");        

// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");        

// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] 
StpUtil.checkRoleOr("super-admin", "shop-admin");

扩展:NotRoleException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

注销和下线

设计思路

踢人下线,核心操作就是找到指定 loginId 对应的 Token,并设置其失效

强制注销 和 踢人下线 的区别在于:

  • 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
  • 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。

API 列表

强制注销 API

StpUtil.logout(10001);                    // 强制指定账号注销下线 
StpUtil.logout(10001, "PC");              // 强制指定账号指定端注销下线 
StpUtil.logoutByTokenValue("token");      // 强制指定 Token 注销下线

踢人下线 API

StpUtil.kickout(10001);                    // 将指定账号踢下线 
StpUtil.kickout(10001, "PC");              // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token");      // 将指定 Token 踢下线

测试代码

@RestController
@RequestMapping("out")
public class KickoutController {

    /**
     * 测试用户强制注销 浏览器访问:http://localhost:8081/out/doLogout/10002
     *
     * @param loginId
     * @return
     */
    @GetMapping("doLogout/{loginId}")
    public SaResult doLogout(@PathVariable String loginId) {
        StpUtil.logout(loginId);
        return SaResult.ok();
    }

    /**
     * 测试用户踢下线 浏览器访问:http://localhost:8081/out/doKickout/10002
     *
     * @param loginId
     * @return
     */
    @GetMapping("doKickout/{loginId}")
    public SaResult doKickout(@PathVariable String loginId) {
        StpUtil.kickout(loginId);
        return SaResult.ok();
    }
}

注解方式实现鉴权

注解鉴权

  • @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
  • @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
  • @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
  • @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
  • @SaCheckBasic: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
  • @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
  • @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。

Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中

1、注册拦截器

新建配置类SaTokenConfigure.java

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");    
    }
}

2、使用注解鉴权

@SaCheckLogin // 作用在类上,表示该类下的任意方法均需登录后访问
@RestController
@RequestMapping("atc")
public class AtCheckController {

    /**
     * 登录校验:只有登录之后才能进入该方法
     *
     * @return
     */
    @SaCheckLogin
    @RequestMapping("info")
    public String info() {
        return "查询用户信息";
    }

    /**
     * 角色校验:必须具有指定角色才能进入该方法
     *
     * @return
     */
    @SaCheckRole("super-admin")
    @RequestMapping("add")
    public String add() {
        return "用户增加";
    }

    /**
     * 权限校验:必须具有指定权限才能进入该方法
     *
     * @return
     */
    @SaCheckPermission("user-update")
    @RequestMapping("add/permission")
    public String update() {
        return "用户修改";
    }


    /**
     * 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法
     *
     * @return
     */
    @SaCheckDisable("comment")
    @RequestMapping("send")
    public String send() {
        return "查询用户信息";
    }

    /**
     * 注解式鉴权:只要具有其中一个权限即可通过校验
     *
     * @return
     */
    @RequestMapping("atJurOr")
    @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)
    public SaResult atJurOr() {
        return SaResult.data("用户信息");
    }

    /**
     * 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
     *
     * 写法一:orRole = "admin",代表需要拥有角色 admin 。
     * 写法二:orRole = {"admin", "manager", "staff"},代表具有三个角色其一即可。
     * 写法三:orRole = {"admin, manager, staff"},代表必须同时具有三个角色。
     * @return
     */
    @RequestMapping("userAdd")
    @SaCheckPermission(value = "user.add", orRole = "admin")
    public SaResult userAdd() {
        return SaResult.data("用户信息");
    }

    /**
     * 此接口加上了 @SaIgnore 可以游客访问
     * @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。
     * @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。
     * @return
     */
    @SaIgnore
    @RequestMapping("getList")
    public SaResult getList() {
        // ...
        return SaResult.ok();
    }

    /**
     * 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。
     * @return
     */
    @SaCheckOr(
        login = @SaCheckLogin,
        role = @SaCheckRole("admin"),
        permission = @SaCheckPermission("user.add"),
        safe = @SaCheckSafe("update-password"),
        basic = @SaCheckBasic(account = "sa:123456"),
        disable = @SaCheckDisable("submit-orders")
    )
    @RequestMapping("test")
    public SaResult test() {
        // ...
        return SaResult.ok();
    }

}

本文由mdnice多平台发布