探索Sa-Token:强大而易用的Java权限认证框架,告别Shiro与Spring Security的繁琐

在进行SpringBoot项目开发时,认证与授权是必不可少的功能。通常我们会选择像Shiro或Spring Security这样的权限认证框架来满足需求,但它们的使用过程往往较为复杂,且功能也有一定限制。最近,我发现了一款功能齐全、易于使用的权限认证框架——Sa-Token。它具有简洁的API设计和优雅的功能实现,非常推荐给大家使用!

Sa-Token概述

Sa-Token是一个轻量级的Java权限认证框架,旨在解决登录认证、权限认证、Session会话、单点登录、OAuth2.0以及微服务网关鉴权等多个与权限相关的问题。

该框架的集成十分简单,开箱即用,API设计优雅。借助Sa-Token,您可以以极其简便的方式完成系统的权限认证,很多功能甚至只需一行代码即可实现。

Sa-Token功能丰富,具体信息可参考下图。

图片

使用

在SpringBoot中,使用Sa-Token实现认证与授权非常简单。接下来,我们将展示如何实现最常用的认证功能,包括登录认证、角色认证和权限认证。

集成与配置

Sa-Token的集成和配置非常简便,确实是开箱即用的选择。

  • 首先,需要在项目的pom.xml中添加Sa-Token的相关依赖:
<!-- Sa-Token 权限认证 -->  
<dependency>  
    <groupId>cn.dev33</groupId>  
    <artifactId>sa-token-spring-boot-starter</artifactId>  
    <version>1.24.0</version>  
</dependency>  
  • 接着,在application.yml中添加Sa-Token的相关配置。为支持前后端分离,我们将从请求头中读取token,并关闭从cookie中读取的设置:
# Sa-Token配置  
sa-token:  
  # token名称 (同样是cookie名称)  
  token-name: Authorization  
  # 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  
  # 是否从头部读取token  
  is-read-head: true  

登录认证

在管理系统中,除了登录接口外,基本所有接口都需要进行登录认证。使用Sa-Token进行路由拦截鉴权非常方便。接下来我们来实现一个示例。

  • 实现登录认证非常简单,首先在UmsAdminController中添加登录接口:
/**  
 * 后台用户管理  
 */  
@Controller  
@Api(tags = "UmsAdminController", description = "后台用户管理")  
@RequestMapping("/admin")  
public class UmsAdminController {  
    @Autowired  
    private UmsAdminService adminService;  
  
    @ApiOperation(value = "登录以后返回token")  
    @RequestMapping(value = "/login", method = RequestMethod.POST)  
    @ResponseBody  
    public CommonResult login(@RequestParam String username, @RequestParam String password) {  
        SaTokenInfo saTokenInfo = adminService.login(username, password);  
        if (saTokenInfo == null) {  
            return CommonResult.validateFailed("用户名或密码错误");  
        }  
        Map<String, String> tokenMap = new HashMap<>();  
        tokenMap.put("token", saTokenInfo.getTokenValue());  
        tokenMap.put("tokenHead", saTokenInfo.getTokenName());  
        return CommonResult.success(tokenMap);  
    }  
}  
  • 然后在UmsAdminServiceImpl中添加登录的具体逻辑。首先验证密码,然后调用StpUtil.login(adminUser.getId())即可实现登录,一行代码搞定:
/**  
 *  
 */  
@Slf4j  
@Service  
public class UmsAdminServiceImpl implements UmsAdminService {  
  
    @Override  
    public SaTokenInfo login(String username, String password) {  
        SaTokenInfo saTokenInfo = null;  
        AdminUser adminUser = getAdminByUsername(username);  
        if (adminUser == null) {  
            return null;  
        }  
        if (!SaSecureUtil.md5(password).equals(adminUser.getPassword())) {  
            return null;  
        }  
        // 密码验证成功后进行登录  
        StpUtil.login(adminUser.getId());  
        // 获取当前登录用户Token信息  
        saTokenInfo = StpUtil.getTokenInfo();  
        return saTokenInfo;  
    }  
}  
  • 之后,我们再添加一个测试接口用于查询当前登录状态。返回true表示已经登录:
/**  
 *  
 */  
@Slf4j  
@Service  
public class UmsAdminServiceImpl implements UmsAdminService {  
    @ApiOperation(value = "查询当前登录状态")  
    @RequestMapping(value = "/isLogin", method = RequestMethod.GET)  
    @ResponseBody  
    public CommonResult isLogin() {  
        return CommonResult.success(StpUtil.isLogin());  
    }  
}  

图片

  • 然后在Authorization请求头中添加获取到的token:

图片

  • 访问/admin/isLogin接口,data属性会返回true,表示您已处于登录状态:

图片

  • 接下来,除了登录接口外,其余接口都需要添加登录认证。我们需要添加Sa-Token的Java配置类SaTokenConfig,并注册一个路由拦截器SaRouteInterceptor。在这里,我们的IgnoreUrlsConfig配置会从配置文件中读取白名单配置:
/**  
 * Sa-Token相关配置  
 */  
@Configuration  
public class SaTokenConfig implements WebMvcConfigurer {  
  
    @Autowired  
    private IgnoreUrlsConfig ignoreUrlsConfig;  
  
    /**  
     * 注册sa-token拦截器  
     */  
    @Override  
    public void addInterceptors(InterceptorRegistry registry) {  
        registry.addInterceptor(new SaRouteInterceptor((req, resp, handler) -> {  
            // 获取配置文件中的白名单路径  
            List<String> ignoreUrls = ignoreUrlsConfig.getUrls();  
            // 登录认证:除白名单路径外均需要登录  
            SaRouter.match(Collections.singletonList("/**"), ignoreUrls, StpUtil::checkLogin);  
        })).addPathPatterns("/**");  
    }  
}  
  • application.yml文件中的白名单配置如下,注意开放Swagger的访问路径和静态资源路径:
# 访问白名单路径  
secure:  
  ignored:  
    urls:  
      - /  
      - /swagger-ui/  
      - /*.html  
      - /favicon.ico  
      - /**/*.html  
      - /**/*.css  
      - /**/*.js  
      - /swagger-resources/**  
      - /v2/api-docs/**  
      - /actuator/**  
      - /admin/login  
      - /admin/isLogin  
  • 由于未登录状态下访问接口,Sa-Token将抛出NotLoginException异常,因此我们需要进行全局处理:
/**  
 * 全局异常处理  
 *  
 */  
@ControllerAdvice  
public class GlobalExceptionHandler {  
  
    /**  
     * 处理未登录的异常  
     */  
    @ResponseBody  
    @ExceptionHandler(value = NotLoginException.class)  
    public CommonResult handleNotLoginException(NotLoginException e) {  
        return CommonResult.unauthorized(e.getMessage());  
    }  
}  
  • 之后,当我们在登录状态下访问接口时,可以正常获取到数据:

图片

  • 当我们处于未登录状态(不带token)时,无法正常访问接口,返回code401

图片

角色认证

角色认证是指定义一套规则,例如ROLE-ADMIN角色可以访问/brand下的所有资源,而ROLE_USER角色只能访问/brand/listAll。接下来,我们将实现角色认证功能。

  • 首先,我们需要扩展Sa-Token的StpInterface接口,通过实现方法来返回用户的角色码和权限码:
/**  
 * 自定义权限验证接口扩展  
 */  
@Component  
public class StpInterfaceImpl implements StpInterface {  
    @Autowired  
    private UmsAdminService adminService;  
    @Override  
    public List<String> getPermissionList(Object loginId, String loginType) {  
        AdminUser adminUser = adminService.getAdminById(Convert.toLong(loginId));  
        return adminUser.getRole().getPermissionList();  
    }  
  
    @Override  
    public List<String> getRoleList(Object loginId, String loginType) {  
        AdminUser adminUser = adminService.getAdminById(Convert.toLong(loginId));  
        return Collections.singletonList(adminUser.getRole().getName());  
    }  
}  
  • 然后在Sa-Token的拦截器中配置路由规则,ROLE_ADMIN角色可以访问所有路径,而ROLE_USER只能访问/brand/listAll路径:
/**  
 * Sa-Token相关配置  
 */  
@Configuration  
public class SaTokenConfig implements WebMvcConfigurer {  
  
    @Autowired  
    private IgnoreUrlsConfig ignoreUrlsConfig;  
  
    /**  
     * 注册sa-token拦截器  
     */  
    @Override  
    public void addInterceptors(InterceptorRegistry registry) {  
        registry.addInterceptor(new SaRouteInterceptor((req, resp, handler) -> {  
            // 获取配置文件中的白名单路径  
            List<String> ignoreUrls = ignoreUrlsConfig.getUrls();  
            // 登录认证:除白名单路径外均需要登录认证  
            SaRouter.match(Collections.singletonList("/**"), ignoreUrls, StpUtil::checkLogin);  
            // 角色认证:ROLE_ADMIN可以访问所有接口,ROLE_USER只能访问查询全部接口  
            SaRouter.match("/brand/listAll", () -> {  
                StpUtil.checkRoleOr("ROLE_ADMIN","ROLE_USER");  
                // 强制退出匹配链  
                SaRouter.stop();  
            });  
            SaRouter.match("/brand/**", () -> StpUtil.checkRole("ROLE_ADMIN"));  
        })).addPathPatterns("/**");  
    }  
}  
  • 当用户尝试以未被允许的角色访问时,Sa-Token将抛出NotRoleException异常,我们可以进行全局处理:
/**  
 * 全局异常处理  
 *  
 */  
@ControllerAdvice  
public class GlobalExceptionHandler {  
  
    /**  
     * 处理没有角色的异常  
     */  
    @ResponseBody  
    @ExceptionHandler(value = NotRoleException.class)  
    public CommonResult handleNotRoleException(NotRoleException e) {  
        return CommonResult.forbidden(e.getMessage());  
    }  
}  
  • 目前我们有两个用户:admin用户具有ROLE_ADMIN角色,macro用户则具有ROLE_USER角色。

图片

  • 使用admin账号访问/brand/list接口时可以正常获取数据:

图片

  • 使用macro账号访问/brand/list接口时则无法正常访问,返回code403

图片

权限认证

当我们给角色分配权限后,再将角色分配给用户,用户便拥有了对应的权限。我们可以为每个接口分配不同的权限,拥有该权限的用户方可访问该接口。下面我们将实现权限认证。

  • 在Sa-Token的拦截器中配置路由规则,admin用户可以访问所有路径,而macro用户只有读取权限,没有写、改、删的权限:
/**  
 * Sa-Token相关配置  
 */  
@Configuration  
public class SaTokenConfig implements WebMvcConfigurer {  
  
    @Autowired  
    private IgnoreUrlsConfig ignoreUrlsConfig;  
  
    /**  
     * 注册sa-token拦截器  
     */  
    @Override  
    public void addInterceptors(InterceptorRegistry registry) {  
        registry.addInterceptor(new SaRouteInterceptor((req, resp, handler) -> {  
            // 获取配置文件中的白名单路径  
            List<String> ignoreUrls = ignoreUrlsConfig.getUrls();  
            // 登录认证:除白名单路径外均需要登录  
            SaRouter.match(Collections.singletonList("/**"), ignoreUrls, StpUtil::checkLogin);  
            // 权限认证:不同接口需校验不同权限  
            SaRouter.match("/brand/listAll", () -> StpUtil.checkPermission("brand:read"));  
            SaRouter.match("/brand/create", () -> StpUtil.checkPermission("brand:create"));  
            SaRouter.match("/brand/update/{id}", () -> StpUtil.checkPermission("brand:update"));  
            SaRouter.match("/brand/delete/{id}", () -> StpUtil.checkPermission("brand:delete"));  
            SaRouter.match("/brand/list", () -> StpUtil.checkPermission("brand:read"));  
            SaRouter.match("/brand/{id}", () -> StpUtil.checkPermission("brand:read"));  
        })).addPathPatterns("/**");  
    }  
}  
  • 当用户尝试访问无权限接口时,Sa-Token会抛出NotPermissionException异常。我们可以全局处理该异常:
/**  
 * 全局异常处理  
 *  
 */  
@ControllerAdvice  
public class GlobalExceptionHandler {  
  
    /**  
     * 处理没有权限的异常  
     */  
    @ResponseBody  
    @ExceptionHandler(value = NotPermissionException.class)  
    public CommonResult handleNotPermissionException(NotPermissionException e) {  
        return CommonResult.forbidden(e.getMessage());  
    }  
}  
  • 使用admin账号访问/brand/delete接口时可以正常获取数据:

图片

  • 使用macro账号访问/brand/delete接口时则无法正常访问,返回code403

图片

总结

通过对Sa-Token的实践,我们发现它的API设计极为优雅,使用体验较Shiro和Spring Security更为顺畅。Sa-Token不仅提供了一系列强大的权限相关功能,还包含了诸如Oauth2、分布式Session会话等标准解决方案,感兴趣的朋友可以深入研究。

参考资料

Sa-Token的官方文档非常全面且友好,不仅提供了解决方案,还引导用户理解解决思路,强烈建议大家查看。

图片
官方文档:http://sa-token.dev33.cn/

项目源码地址

https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-sa-token