再見(jiàn),Shiro!這個(gè)開(kāi)源的權(quán)限認(rèn)證框架功能真強(qiáng)大!很香
在我們做SpringBoot項(xiàng)目的時(shí)候,認(rèn)證授權(quán)是必不可少的功能!我們經(jīng)常會(huì)選擇Shiro、Spring Security這類(lèi)權(quán)限認(rèn)證框架來(lái)實(shí)現(xiàn),但這些框架使用起來(lái)有點(diǎn)繁瑣,而且功能也不夠強(qiáng)大。最近發(fā)現(xiàn)一款功能強(qiáng)大的權(quán)限認(rèn)證框架Sa-Token,它使用簡(jiǎn)單、API設(shè)計(jì)優(yōu)雅,推薦給大家!
Sa-Token簡(jiǎn)介
Sa-Token是一款輕量級(jí)的Java權(quán)限認(rèn)證框架,可以用來(lái)解決登錄認(rèn)證、權(quán)限認(rèn)證、Session會(huì)話、單點(diǎn)登錄、OAuth2.0、微服務(wù)網(wǎng)關(guān)鑒權(quán)等一系列權(quán)限相關(guān)問(wèn)題。
框架集成簡(jiǎn)單、開(kāi)箱即用、API設(shè)計(jì)優(yōu)雅,通過(guò)Sa-Token,你將以一種極其簡(jiǎn)單的方式實(shí)現(xiàn)系統(tǒng)的權(quán)限認(rèn)證部分,有時(shí)候往往只需一行代碼就能實(shí)現(xiàn)功能。
Sa-Token功能很全,具體可以參考下圖。

使用
在SpringBoot中使用Sa-Token是非常簡(jiǎn)單的,接下來(lái)我們使用它來(lái)實(shí)現(xiàn)最常用的認(rèn)證授權(quán)功能,包括登錄認(rèn)證、角色認(rèn)證和權(quán)限認(rèn)證。
集成及配置
Sa-Token的集成和配置都非常簡(jiǎn)單,不愧為開(kāi)箱即用。
首先我們需要在項(xiàng)目的 pom.xml中添加Sa-Token的相關(guān)依賴;
<!-- Sa-Token 權(quán)限認(rèn)證 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.24.0</version>
</dependency>
然后在 application.yml中添加Sa-Token的相關(guān)配置,考慮到要支持前后端分離項(xiàng)目,我們關(guān)閉從cookie中讀取token,改為從head中讀取token。
# Sa-Token配置
sa-token:
# token名稱 (同時(shí)也是cookie名稱)
token-name: Authorization
# token有效期,單位秒,-1代表永不過(guò)期
timeout: 2592000
# token臨時(shí)有效期 (指定時(shí)間內(nèi)無(wú)操作就視為token過(guò)期),單位秒
activity-timeout: -1
# 是否允許同一賬號(hào)并發(fā)登錄 (為false時(shí)新登錄擠掉舊登錄)
is-concurrent: true
# 在多人登錄同一賬號(hào)時(shí),是否共用一個(gè)token (為false時(shí)每次登錄新建一個(gè)token)
is-share: false
# token風(fēng)格
token-style: uuid
# 是否輸出操作日志
is-log: false
# 是否從cookie中讀取token
is-read-cookie: false
# 是否從head中讀取token
is-read-head: true
登錄認(rèn)證
在管理系統(tǒng)中,除了登錄接口,基本都需要登錄認(rèn)證,在Sa-Token中使用路由攔截鑒權(quán)是最方便的,下面我們來(lái)實(shí)現(xiàn)下。
實(shí)現(xiàn)登錄認(rèn)證非常簡(jiǎn)單,首先在 UmsAdminController中添加一個(gè)登錄接口;
/**
* 后臺(tái)用戶管理
* Created by macro on 2018/4/26.
*/
@Controller
@Api(tags = "UmsAdminController", description = "后臺(tái)用戶管理")
@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("用戶名或密碼錯(cuò)誤");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", saTokenInfo.getTokenValue());
tokenMap.put("tokenHead", saTokenInfo.getTokenName());
return CommonResult.success(tokenMap);
}
}
然后在 UmsAdminServiceImpl添加登錄的具體邏輯,先驗(yàn)證密碼,然后調(diào)用StpUtil.login(adminUser.getId())即可實(shí)現(xiàn)登錄,調(diào)用API一行搞定;
/**
* Created by macro on 2020/10/15.
*/
@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;
}
// 密碼校驗(yàn)成功后登錄,一行代碼實(shí)現(xiàn)登錄
StpUtil.login(adminUser.getId());
// 獲取當(dāng)前登錄用戶Token信息
saTokenInfo = StpUtil.getTokenInfo();
return saTokenInfo;
}
}
我們?cè)偬砑右粋€(gè)測(cè)試接口用于查詢當(dāng)前登錄狀態(tài),返回 true表示已經(jīng)登錄;
/**
* Created by macro on 2020/10/15.
*/
@Slf4j
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
@ApiOperation(value = "查詢當(dāng)前登錄狀態(tài)")
@RequestMapping(value = "/isLogin", method = RequestMethod.GET)
@ResponseBody
public CommonResult isLogin() {
return CommonResult.success(StpUtil.isLogin());
}
}
之后可以通過(guò)Swagger訪問(wèn)登錄接口來(lái)獲取Token了,使用賬號(hào)為 admin:123456,訪問(wèn)地址:http://localhost:8088/swagger-ui/

然后在 Authorization請(qǐng)求頭中添加獲取到的token;

訪問(wèn) /admin/isLogin接口,data屬性就會(huì)返回true了,表示你已經(jīng)是登錄狀態(tài)了;

接下來(lái)我們需要把除登錄接口以外的接口都添加登錄認(rèn)證,添加Sa-Token的Java配置類(lèi) SaTokenConfig,注冊(cè)一個(gè)路由攔截器SaRouteInterceptor,這里我們的IgnoreUrlsConfig配置會(huì)從配置文件中讀取白名單配置;
/**
* Sa-Token相關(guān)配置
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
/**
* 注冊(cè)sa-token攔截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaRouteInterceptor((req, resp, handler) -> {
// 獲取配置文件中的白名單路徑
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
// 登錄認(rèn)證:除白名單路徑外均需要登錄認(rèn)證
SaRouter.match(Collections.singletonList("/**"), ignoreUrls, StpUtil::checkLogin);
})).addPathPatterns("/**");
}
}
application.yml文件中的白名單配置如下,注意開(kāi)放Swagger的訪問(wèn)路徑和靜態(tài)資源路徑;
# 訪問(wèn)白名單路徑
secure:
ignored:
urls:
- /
- /swagger-ui/
- /*.html
- /favicon.ico
- /**/*.html
- /**/*.css
- /**/*.js
- /swagger-resources/**
- /v2/api-docs/**
- /actuator/**
- /admin/login
- /admin/isLogin
由于未登錄狀態(tài)下訪問(wèn)接口,Sa-Token會(huì)拋出 NotLoginException異常,所以我們需要全局處理下;
/**
* 全局異常處理
* Created by macro on 2020/2/27.
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 處理未登錄的異常
*/
@ResponseBody
@ExceptionHandler(value = NotLoginException.class)
public CommonResult handleNotLoginException(NotLoginException e) {
return CommonResult.unauthorized(e.getMessage());
}
}
之后當(dāng)我們?cè)诘卿洜顟B(tài)下訪問(wèn)接口時(shí),可以獲取到數(shù)據(jù);

當(dāng)我們未登錄狀態(tài)(不帶token)時(shí)無(wú)法正常訪問(wèn)接口,返回 code為401。

角色認(rèn)證
角色認(rèn)證也就是我們定義好一套規(guī)則,比如
ROLE-ADMIN角色可以訪問(wèn)/brand下的所有資源,而ROLE_USER角色只能訪問(wèn)/brand/listAll,接下來(lái)我們來(lái)實(shí)現(xiàn)下角色認(rèn)證。
首先我們需要擴(kuò)展Sa-Token的 StpInterface接口,通過(guò)實(shí)現(xiàn)方法來(lái)返回用戶的角色碼和權(quán)限碼;
/**
* 自定義權(quán)限驗(yàn)證接口擴(kuò)展
*/
@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的攔截器中配置路由規(guī)則, ROLE_ADMIN角色可以訪問(wèn)所有路徑,而ROLE_USER只能訪問(wèn)/brand/listAll路徑;
/**
* Sa-Token相關(guān)配置
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
/**
* 注冊(cè)sa-token攔截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaRouteInterceptor((req, resp, handler) -> {
// 獲取配置文件中的白名單路徑
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
// 登錄認(rèn)證:除白名單路徑外均需要登錄認(rèn)證
SaRouter.match(Collections.singletonList("/**"), ignoreUrls, StpUtil::checkLogin);
// 角色認(rèn)證:ROLE_ADMIN可以訪問(wèn)所有接口,ROLE_USER只能訪問(wèn)查詢?nèi)拷涌?/span>
SaRouter.match("/brand/listAll", () -> {
StpUtil.checkRoleOr("ROLE_ADMIN","ROLE_USER");
//強(qiáng)制退出匹配鏈
SaRouter.stop();
});
SaRouter.match("/brand/**", () -> StpUtil.checkRole("ROLE_ADMIN"));
})).addPathPatterns("/**");
}
}
當(dāng)用戶不是被允許的角色訪問(wèn)時(shí),Sa-Token會(huì)拋出 NotRoleException異常,我們可以全局處理下;
/**
* 全局異常處理
* Created by macro on 2020/2/27.
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 處理沒(méi)有角色的異常
*/
@ResponseBody
@ExceptionHandler(value = NotRoleException.class)
public CommonResult handleNotRoleException(NotRoleException e) {
return CommonResult.forbidden(e.getMessage());
}
}
我們現(xiàn)在有兩個(gè)用戶, admin用戶具有ROLE_ADMIN角色,macro用戶具有ROLE_USER角色;

使用 admin賬號(hào)訪問(wèn)/brand/list接口可以正常訪問(wèn);

使用 macro賬號(hào)訪問(wèn)/brand/list接口無(wú)法正常訪問(wèn),返回code為403。

權(quán)限認(rèn)證
當(dāng)我們給角色分配好權(quán)限,然后給用戶分配好角色后,用戶就擁有了這些權(quán)限。我們可以為每個(gè)接口分配不同的權(quán)限,擁有該權(quán)限的用戶就可以訪問(wèn)該接口。這就是權(quán)限認(rèn)證,接下來(lái)我們來(lái)實(shí)現(xiàn)下它。
我們可以在Sa-Token的攔截器中配置路由規(guī)則, admin用戶可以訪問(wèn)所有路徑,而macro用戶只有讀取的權(quán)限,沒(méi)有寫(xiě)、改、刪的權(quán)限;
/**
* Sa-Token相關(guān)配置
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
/**
* 注冊(cè)sa-token攔截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaRouteInterceptor((req, resp, handler) -> {
// 獲取配置文件中的白名單路徑
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
// 登錄認(rèn)證:除白名單路徑外均需要登錄認(rèn)證
SaRouter.match(Collections.singletonList("/**"), ignoreUrls, StpUtil::checkLogin);
// 權(quán)限認(rèn)證:不同接口, 校驗(yàn)不同權(quán)限
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("/**");
}
}
當(dāng)用戶無(wú)權(quán)限訪問(wèn)時(shí),Sa-Token會(huì)拋出 NotPermissionException異常,我們可以全局處理下;
/**
* 全局異常處理
* Created by macro on 2020/2/27.
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 處理沒(méi)有權(quán)限的異常
*/
@ResponseBody
@ExceptionHandler(value = NotPermissionException.class)
public CommonResult handleNotPermissionException(NotPermissionException e) {
return CommonResult.forbidden(e.getMessage());
}
}
使用 admin賬號(hào)訪問(wèn)/brand/delete接口可以正常訪問(wèn);

使用 macro賬號(hào)訪問(wèn)/brand/delete無(wú)法正常訪問(wèn),返回code為403。

總結(jié)
通過(guò)對(duì)Sa-Token的一波實(shí)踐,我們可以發(fā)現(xiàn)它的API設(shè)計(jì)非常優(yōu)雅,比起Shiro和Spring Security來(lái)說(shuō)確實(shí)順手多了。Sa-Token不僅提供了一系列強(qiáng)大的權(quán)限相關(guān)功能,還提供了很多標(biāo)準(zhǔn)的解決方案,比如Oauth2、分布式Session會(huì)話等,大家感興趣的話可以研究下。
參考資料
Sa-Token的官方文檔很全,也很良心,不僅提供了解決方式,還提供了解決思路,強(qiáng)烈建議大家去看下。

官方文檔:http://sa-token.dev33.cn/
項(xiàng)目源碼地址
https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-sa-token
我是 Guide哥,一個(gè)工作2年有余,接觸編程已經(jīng)6年有余的程序員。大三開(kāi)源 JavaGuide,目前已經(jīng) 100k+ Star。未來(lái)幾年,希望持續(xù)完善 JavaGuide,爭(zhēng)取能夠幫助更多學(xué)習(xí) Java 的小伙伴!共勉!凎!點(diǎn)擊即可了解我的個(gè)人經(jīng)歷。
歡迎點(diǎn)贊分享。咱們下期再會(huì)!
