SpringBoot integrates Sa-Token framework (1)

1. Document reference: Framework introduction (sa-token.cc)

Framework Ecology - Open Source Project (sa-token.cc)

2. Integration with SpingBoot

1. Create a project

        Create a new SpringBoot project in the IDE, for example: sa-token-demo-springboot(Students who don’t know how to do it, please go to Baidu or refer to: SpringBoot-Pure )

2. Add dependencies

        This is a dependency used by the springboot web project:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
</dependency>

         Introduce sa-token dependency:

<!-- 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>

Note: If you are using SpringBoot 3.x, you only need to  sa-token-spring-boot-starter modify to  sa-token-spring-boot3-starter .

3. Set up the configuration file

        You can start the project with zero configuration  , but at the same time you can also  application.yml add the following configuration to use the framework for customization: framework configuration (sa-token.cc)

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

4. Create startup class

import cn.dev33.satoken.SaManager;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

5. Create the Controller class

import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

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

6. Run the test 

3. Basic operations based on SpingBoot

​Sa-Token currently has five main functional modules: login authentication, authority authentication, single sign-on, OAuth2.0, and microservice authentication.

1. Login authentication 

(Single-end login, multi-end login, same-end mutually exclusive login, no login required within seven days)

@SpringBootApplication
public class AppLogin {
    public static void main(String[] args) {
        SpringApplication.run(AppLogin.class,args);
    }
}

 Startup class code

package com.satoken.controller;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

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

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

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

    // 退出 http://localhost:8081/user/logout
    @GetMapping("/logout")
    public boolean logout(){
        StpUtil.logout();
        return true;
    }

     检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
    // http://localhost:8081/user/checkLogin
    @GetMapping("/checkLogin")
    public SaResult checkLogin(){

        try{
            StpUtil.checkLogin();
            return SaResult.ok("已经登录");
        }catch (NotLoginException e){
            e.printStackTrace();
            return SaResult.error("未登录");
        }
    }

    // 获取登录ID http://localhost:8081/user/getLoginId
    @GetMapping("/getLoginId")
    public Map<String,Object> getLoginId(){
        Map<String,Object> map = new HashMap<>();
        // 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
        map.put("getLoginId",StpUtil.getLoginId());
        map.put("getLoginIdAsInt",StpUtil.getLoginIdAsInt());// 获取当前会话账号id, 并转化为`String`类型
        return map;
    }

    // token信息 http://localhost:8081/user/getToken
    @GetMapping("/getToken")
    public SaTokenInfo getToken(){
        return StpUtil.getTokenInfo();
    }

}

test:

2. Permission authentication 

        The so-called permission authentication, the core logic is to judge whether an account has the specified permission: if yes, let you pass. No? Then access is prohibited! Going deep into the underlying data, each account will have a set of permission codes, and the framework will check whether the set contains the specified permission codes.

        For example: The current account has a set of permission codes  ["user-add", "user-delete", "user-get"]. If I verify the permissions at this time  "user-update", the result will be: verification failed and access prohibited .

        So the core of the problem now is two: How to obtain the set of permission codes owned by an account? Which permission code needs to be verified for this operation?

        Because the requirements of each project are different, its permission design is also ever-changing, so the operation of [get the current account permission code set] cannot be built into the framework, so Sa-Token exposes this operation to you in the form of an interface, which is convenient for you Rewrite it according to your own business logic. All you need to do is create a new class and implement  StpInterfacethe interface, such as the following code:

package com.satoken.service;

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

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

/**
 * 自定义权限加载接口实现类
 */
@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<>();
        list.add("admin");
        list.add("super-admin");
        return list;
    }
}

Then in the controller layer

import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequestMapping("/auth")
public class AuthenticationController {

    // 获取所有权限 http://localhost:8081/auth/getPermissionList
    @GetMapping("/getPermissionList")
    public List<String> getPermissionList(){
        return StpUtil.getPermissionList();
    }

    // 权限判断 http://localhost:8081/auth/hasPermission
    @GetMapping("/hasPermission")
    public boolean hasPermission(){
        // 判断:当前账号是否含有指定权限, 返回 true 或 false
        return StpUtil.hasPermission("user.add");
    }

    // 权限检查 http://localhost:8081/auth/checkPermission
    @GetMapping("/checkPermission")
    public boolean checkPermission(){
        // 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
        StpUtil.checkPermission("user.xadd");
        return true;
    }

    // 权限检查 http://localhost:8081/auth/checkPermissionAnd
    @GetMapping("/checkPermissionAnd")
    public boolean checkPermissionAnd(){
        // 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
        StpUtil.checkPermissionAnd("user.add", "user.add", "art.get");
        return true;
    }

    // http://localhost:8081/auth/getRoleList
    @GetMapping("/getRoleList")
    public List<String> getRoleList(){
        // 获取:当前账号所拥有的角色集合
        return StpUtil.getRoleList();
    }

    // http://localhost:8081/auth/hasRole?role=
    @GetMapping("/hasRole")
    public boolean hasRole(String role){
        return StpUtil.hasRole(role);
    }

    // http://localhost:8081/auth/checkRole?role=
    @GetMapping("/checkRole")
    public boolean checkRole(String role){
        StpUtil.checkRole(role);
        return true;
    }
}

Obtain all permissions (the premise is that you must log in first, and you must first establish a session session) 

 Permission judgment (the same applies to subsequent interfaces)

Intercept global exceptions:

        Authentication failed and an exception was thrown. Should the exception be displayed to the user? Of course not! You can create a global exception interceptor to unify the format returned to the front end, refer to:

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 全局异常拦截 
    @ExceptionHandler
    public SaResult handlerException(Exception e) {
        e.printStackTrace(); 
        return SaResult.error(e.getMessage());
    }
}

 

 

How to precise permissions down to the button level ( the scope of permissions can control whether each button on the page is displayed )

Idea: Such precise range control is difficult to achieve by relying only on the backend. At this time, the frontend needs to make certain logical judgments.

If it is an integrated front-end and back-end project, you can refer to: Thymeleaf label dialect . If it is a front-end and back-end separated project, then:

  1. When logging in, all permission codes owned by the current account are returned to the front end at once.
  2. The front end saves the permission code collection in localStorageor other global state management objects.
  3. On buttons that require permission control, use js to make logical judgments. For example, in Vuethe framework we can use the following writing:
<button v-if="arr.indexOf('user.delete') > -1">删除按钮</button>

        Note: The above writing method is only to provide a reference example. Different frameworks have different writing methods. You can flexibly encapsulate and call according to the project technology stack.

3. Kick someone offline

The so-called kicking someone offline, the core operation is to find the specified  loginId corresponding one  Tokenand set it to invalid.

Force logout:

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

Kick someone offline:

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

 After logging in, kick people offline based on their ID.

 

4. Annotation authentication

        Although it is very convenient to use code authentication, I still want to separate the authentication logic from the business logic. Can I use annotation authentication? sure! Annotation authentication - elegantly separate authentication from business code! However, annotation authentication is relatively inconvenient to change, and there is no dynamic authentication like code authentication.

  • @SaCheckLogin: Login verification - This method can only be entered after logging in.
  • @SaCheckRole("admin"): Role verification - You must have the specified role ID to enter this method.
  • @SaCheckPermission("user:add"): Permission verification - You must have the specified permission to enter this method.
  • @SaCheckSafe: Second-level authentication verification——This method can only be entered after second-level authentication.
  • @SaCheckBasic: HttpBasic verification——This method can only be entered after passing Basic authentication.
  • @SaIgnore: Ignore verification - indicates that the modified method or class does not require annotation authentication and routing interceptor authentication.
  • @SaCheckDisable("comment"): Account service ban verification - Verify whether the specified service of the current account is banned.

        Sa-Token uses a global interceptor to complete the annotation authentication function. In order not to bring unnecessary performance burden to the project, the interceptor is turned off by default. Therefore, in order to use annotation authentication, you must manually register Sa-Token's global interceptor into your project : (take SpringBoot2.0as an example, create a new configuration class SaTokenConfigure.java)

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

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

controller layer:

import cn.dev33.satoken.annotation.*;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

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

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

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

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

    // 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
    @RequestMapping("userAdd")
    @SaCheckPermission(value = "user.add", orRole = "admin")
    public SaResult userAdd() {
        return SaResult.data("用户信息");
    }

    // 此接口加上了 @SaIgnore 可以游客访问
    @SaIgnore
    @RequestMapping("getList")
    public SaResult getList() {
        // ...
        return SaResult.ok();
    }

    // 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。
    @SaCheckOr(
            login = @SaCheckLogin,
            role = @SaCheckRole("admin"),
            permission = @SaCheckPermission("user.add")
    )
    @RequestMapping("test")
    public SaResult test() {
        // ...
        return SaResult.ok();
    }

}

Annotation authentication is implemented         in   annotation authentication, but the default interceptor mode has a disadvantage, that is, it cannot be Controller层used for verification in other codes. Therefore, Sa-Token provides an AOP plug-in. You only need to pom.xmladd the following dependencies in it to use annotation authentication at any level.

<!-- Sa-Token 整合 SpringAOP 实现注解鉴权 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-aop</artifactId>
    <version>1.35.0.RC</version>
</dependency>

  • In interceptor mode, annotations can only be written at Controller层any level. In AOP mode, annotations can be written at any level.
  • The interceptor and AOP mode cannot be integrated at the same time , otherwise Controller层a bug will occur where the annotation is verified twice.

Guess you like

Origin blog.csdn.net/weixin_49171365/article/details/130634089