# Java 权限认证框架功能 测试 / 对比 / 迁移。 对比以下框架的常见功能,为项目技术栈迁移提供代码示例 - Sa-Token - Apache Shiro - Spring Security - JWT > [!TIP| label:注意事项] > - 因个人精力&能力有限,本篇只展示部分常见功能的对比,也欢迎大家一起贡献案例,提交pr。 > - 代码案例仓库:[https://gitee.com/sa-tokens/auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test) > - 注:本篇主要展示一些常见功能不同框架的实现差异,而非每个框架的所含功能点对比。 --- ### 登录 & 注销 & 查询会话状态 测试 Controller ``` java @RestController @RequestMapping("/acc/") public class LoginController { @Autowired SysUserDao sysUserDao; // 测试登录 ---- http://localhost:8081/acc/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); if(user == null) { return AjaxJson.getError("用户不存在"); } if(!user.getPassword().equals(password)) { return AjaxJson.getError("密码错误"); } // 登录 StpUtil.login(user.getId()); StpUtil.getSession().set("user", user); return AjaxJson.getSuccess("登录成功"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public AjaxJson isLogin() { if(StpUtil.isLogin()) { return AjaxJson.getSuccess("已登录,账号id:" + StpUtil.getLoginId()); } return AjaxJson.getError("未登录"); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public AjaxJson logout() { StpUtil.logout(); return AjaxJson.getSuccess("注销成功"); } } ``` 自定义 Realm ``` java public class MyRealm extends AuthorizingRealm { @Autowired private SysUserDao sysUserDao; // 加载用户信息 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { String username = (String)token.getPrincipal(); SysUser sysUser = sysUserDao.findByUsername(username); if(sysUser == null){ return null; } return new SimpleAuthenticationInfo( sysUser, sysUser.getPassword(), getName() ); } // 加载权限信息 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } } ``` Shiro 配置类 ``` java @Configuration public class ShiroConfigure { @Bean public MyRealm myRealm() { return new MyRealm(); } @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); return manager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); return bean; } } ``` 测试 Controller ``` java @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8082/acc/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password)); return AjaxJson.getSuccess("登录成功!"); } catch (AuthenticationException e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } // 查询登录状态 ---- http://localhost:8082/acc/isLogin @RequestMapping("isLogin") public AjaxJson isLogin() { Subject subject = SecurityUtils.getSubject(); if(subject.isAuthenticated()) { SysUser sysUser = (SysUser)subject.getPrincipal(); return AjaxJson.getSuccess("已登录,账号id:" + sysUser.getId()); } return AjaxJson.getError("未登录"); } // 测试注销 ---- http://localhost:8082/acc/logout @RequestMapping("logout") public AjaxJson logout() { SecurityUtils.getSubject().logout(); return AjaxJson.getSuccess("注销成功"); } } ``` ### 账号密码登录(加盐 MD5) 测试 Controller ``` java @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); if(user == null) { return AjaxJson.getError("用户不存在"); } String salt = "abc"; if(!user.getPassword().equals(SaSecureUtil.md5(salt + password))) { return AjaxJson.getError("密码错误"); } // 登录 StpUtil.login(user.getId()); StpUtil.getSession().set("user", user); return AjaxJson.getSuccess("登录成功"); } ``` 自定义 Realm Bean 设定密码凭证器 ``` java @Bean public MyRealm myRealm() { MyRealm realm = new MyRealm(); // 设定凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); realm.setCredentialsMatcher(credentialsMatcher); // 返回 return realm; } ``` 自定义 Realm 实现类 doGetAuthenticationInfo 方法返回 slat 信息 ``` java // 加载用户信息 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { String username = (String)token.getPrincipal(); SysUser sysUser = sysUserDao.findByUsername(username); if(sysUser == null){ return null; } return new SimpleAuthenticationInfo( sysUser, sysUser.getPassword(), ByteSource.Util.bytes("abc"), // 指定 slat 信息 getName() ); } ``` 登录代码照旧 ### 从上下文获取当前登录 User 信息 ``` java // 从上下文获取当前登录 User 信息 @RequestMapping("getCurrUser") public AjaxJson getCurrUser() { return AjaxJson.getSuccess() .set("id", StpUtil.getLoginId()) .set("user", StpUtil.getSession().get("user")); } ``` ``` java // 从上下文获取当前登录 User 信息 @RequestMapping("getCurrUser") public AjaxJson getCurrUser() { Subject subject = SecurityUtils.getSubject(); SysUser sysUser = (SysUser)subject.getPrincipal(); return AjaxJson.getSuccess() .set("id", sysUser.getId()) .set("user", sysUser); } ``` ### 从 session 上存取值 ``` java // 测试从 session 上存取值 @RequestMapping("testSession") public AjaxJson test() { SaSession session = StpUtil.getSession(); System.out.println("从 session 上取值:" + session.get("name")); session.set("name", "zhang"); System.out.println("从 session 上取值:" + session.get("name")); return AjaxJson.getSuccess(); } ``` ``` java // 测试从 session 上存取值 @RequestMapping("testSession") public AjaxJson test() { Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); System.out.println("从 session 上取值:" + session.getAttribute("name")); session.setAttribute("name", "zhang"); System.out.println("从 session 上取值:" + session.getAttribute("name")); return AjaxJson.getSuccess(); } ``` ### 角色认证 & 权限认证 自定义 StpInterface 实现类 ``` java @Component public class StpInterfaceImpl implements StpInterface { // 加载角色信息 @Override public List getPermissionList(Object loginId, String loginType) { return Arrays.asList("admin", "super-admin", "ceo"); } // 加载权限信息 @Override public List getRoleList(Object loginId, String loginType) { return Arrays.asList("user:add", "user:delete", "user:update"); } } ``` 测试 Controller ``` java @RestController @RequestMapping("/jur/") public class JurController { // 角色判断 ---- http://localhost:8082/jur/assertRole @RequestMapping("assertRole") public AjaxJson assertRole() { // is 模式,返回 true 或 false System.out.println("单个权限判断:" + StpUtil.hasRole("admin")); System.out.println("多个权限判断(and):" + StpUtil.hasRoleAnd("admin", "dev-admin")); System.out.println("多个权限判断(or):" + StpUtil.hasRoleOr("admin", "dev-admin")); // check 模式,无角色时抛出异常 StpUtil.checkRole("admin"); // 单个 check StpUtil.checkRoleAnd("admin", "dev-admin"); // 多个 check (and) StpUtil.checkRoleOr("admin", "dev-admin"); // 多个 check (or) return AjaxJson.getSuccess(); } // 权限判断 ---- http://localhost:8082/jur/assertPermission @RequestMapping("assertPermission") public AjaxJson assertPermission() { // is 模式,返回 true 或 false System.out.println("单个权限判断:" + StpUtil.hasPermission("user:add")); System.out.println("多个权限判断(and):" + StpUtil.hasPermissionAnd("user:add", "user:delete22")); System.out.println("多个权限判断(or):" + StpUtil.hasPermissionOr("user:add", "user:delete22")); // check 模式,无权限时抛出异常 StpUtil.checkPermission("user:add"); // 单个 check StpUtil.checkPermissionAnd("user:add", "user:delete22"); // 多个 check (and) StpUtil.checkPermissionOr("user:add", "user:delete22"); // 多个 check (or) return AjaxJson.getSuccess(); } } ``` 自定义 Realm 里重写方法 doGetAuthorizationInfo ``` java @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 加载角色信息 authorizationInfo.addRoles(Arrays.asList("admin", "super-admin", "ceo")); // 加载权限信息 authorizationInfo.addStringPermissions(Arrays.asList("user:add", "user:delete", "user:update")); return authorizationInfo; } ``` 测试 Controller ``` java @RestController @RequestMapping("/jur/") public class JurController { // 角色判断 @RequestMapping("assertRole") public AjaxJson assertRole() { Subject subject = SecurityUtils.getSubject(); // is 模式,返回 true 或 false System.out.println("单个权限判断:" + subject.hasRole("admin")); System.out.println("多个权限判断(and):" + subject.hasAllRoles(Arrays.asList("admin", "dev-admin"))); System.out.println("多个权限判断(or):" + (subject.hasRole("admin") || subject.hasRole("dev-admin"))); // check 模式,无角色时抛出异常 subject.checkRole("admin"); // 单个 check subject.checkRoles("admin", "dev-admin"); // 多个 check (and) return AjaxJson.getSuccess(); } // 权限判断 @RequestMapping("assertPermission") public AjaxJson assertPermission() { Subject subject = SecurityUtils.getSubject(); // is 模式,返回 true 或 false System.out.println("单个权限判断:" + subject.isPermitted("user:add")); System.out.println("多个权限判断(and):" + subject.isPermittedAll("user:add", "user:delete22")); System.out.println("多个权限判断(or):" + (subject.isPermitted("user:add") || subject.isPermitted("user:delete22"))); // check 模式,无权限时抛出异常 subject.checkPermission("user:add"); // 单个 check subject.checkPermissions("user:add", "user:delete22"); // 多个 check (and) return AjaxJson.getSuccess(); } } ``` ### 注解鉴权 SaTokenConfigure 配置注解拦截器 ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } } ``` 测试 Controller ``` java @RestController @RequestMapping("/at-check/") public class AtCheckController { // 登录校验 @SaCheckLogin @RequestMapping("checkLogin") public AjaxJson checkLogin() { return AjaxJson.getSuccess(); } // 角色校验 @SaCheckRole("admin") @RequestMapping("checkRole") public AjaxJson checkRole() { return AjaxJson.getSuccess(); } // 权限校验 @SaCheckPermission("user:add") @RequestMapping("checkPermission") public AjaxJson checkPermission() { return AjaxJson.getSuccess(); } // 忽略认证校验 @SaIgnore @SaCheckLogin @RequestMapping("ignoreCheck") public AjaxJson ignoreCheck() { return AjaxJson.getSuccess(); } } ``` 测试 Controller ``` java @RestController @RequestMapping("/at-check/") public class AtCheckController { // 登录校验 @RequiresAuthentication @RequestMapping("checkLogin") public AjaxJson checkLogin() { return AjaxJson.getSuccess(); } // 角色校验 @RequiresRoles("admin") @RequestMapping("checkRole") public AjaxJson checkRole() { return AjaxJson.getSuccess(); } // 权限校验 @RequiresPermissions("user:add") @RequestMapping("checkPermission") public AjaxJson checkPermission() { return AjaxJson.getSuccess(); } } ``` ### 路由拦截鉴权 SaTokenConfigure 配置 ``` java @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor(handle -> { SaRouter.match("/route-check/getInfo1").stop(); // 不拦截 SaRouter.match("/route-check/getInfo2").check(r -> StpUtil.checkLogin()); // 需要登录 SaRouter.match("/route-check/getInfo3").check(r -> StpUtil.checkRole("admin2")); // 需要角色 SaRouter.match("/route-check/getInfo4").check(r -> StpUtil.checkPermission("user:add3")); // 需要权限 })).addPathPatterns("/**"); } ``` 鉴权未通过时处理方案 ``` java @RestControllerAdvice public class GlobalException { @ExceptionHandler(NotLoginException.class) public AjaxJson handlerException(NotLoginException e) { return AjaxJson.get(401, "未登录"); } @ExceptionHandler(NotRoleException.class) public AjaxJson handlerException(NotRoleException e) { return AjaxJson.get(403, "缺少角色:" + e.getRole()); } @ExceptionHandler(NotPermissionException.class) public AjaxJson handlerException(NotPermissionException e) { return AjaxJson.get(403, "缺少权限:" + e.getPermission()); } } ``` 过滤器配置 ``` java @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); // 路由拦截鉴权 Map filterMap = new LinkedHashMap<>(); filterMap.put("/route-check/getInfo", "anon"); // 不拦截 filterMap.put("/route-check/getInfo2", "authc"); // 需要登录 filterMap.put("/route-check/getInfo3", "perms[admin2]"); // 需要角色 filterMap.put("/route-check/getInfo4", "perms[user:add3]"); // 需要权限 bean.setFilterChainDefinitionMap(filterMap); bean.setLoginUrl("/401"); // 未登录时跳转的 url bean.setUnauthorizedUrl("/403"); // 未授权时跳转的 url return bean; } ``` 鉴权未通过时处理方案 ``` java @RestController public class ShiroErrorController { @RequestMapping("/401") public Object error401(HttpServletRequest request, HttpServletResponse response) { response.setStatus(200); return AjaxJson.get(401, "not login"); } @RequestMapping("/403") public Object error403(HttpServletRequest request, HttpServletResponse response) { response.setStatus(200); return AjaxJson.get(403, "鉴权未通过"); } } ``` ### 和 Thymeleaf 集成 pom.xml 依赖 ``` xml cn.dev33 sa-token-dialect-thymeleaf ${sa-token.version} ``` SaTokenConfigure 增加配置 Sa-Token 标签方言对象 ``` java // Sa-Token 标签方言 (Thymeleaf版) @Bean public SaTokenDialect getSaTokenDialect() { return new SaTokenDialect(); } ``` 新建 ThymeleafConfigure 注入全局变量 ``` java @Configuration public class ThymeleafConfigure { // 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法 @Autowired public void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { viewResolver.addStaticVariable("stp", StpUtil.stpLogic); } } ``` 新建 Controller ``` java @Controller public class HomeController { @RequestMapping("/") public Object index(HttpServletRequest request) { request.setAttribute("isLogin", StpUtil.isLogin()); return new ModelAndView("index.html"); } } ``` 新建 templates/index.html ``` html Sa-Token 集成 Thymeleaf 标签方言

Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面

当前是否登录:

登录 注销

登录之后才能显示:value

不登录才能显示:value

具有角色 admin 才能显示:value

同时具备多个角色才能显示:value

只要具有其中一个角色就能显示:value

不具有角色 admin 才能显示:value

具有权限 user-add 才能显示:value

同时具备多个权限才能显示:value

只要具有其中一个权限就能显示:value

不具有权限 user-add 才能显示:value

从SaSession中取值:

``` pom.xml 依赖 ``` xml com.github.theborakompanioni thymeleaf-extras-shiro 2.1.0 ``` ShiroConfigure 增加配置 Shiro 方言对象 ``` java @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } ``` 新建 Controller ``` java @Controller public class HomeController { @RequestMapping("/") public Object index(HttpServletRequest request) { Subject subject = SecurityUtils.getSubject(); request.setAttribute("isLogin", subject.isAuthenticated()); return new ModelAndView("index.html"); } } ``` 新建 templates/index.html ``` html Shiro 集成 Thymeleaf 标签方言

Shiro 集成 Thymeleaf 标签方言 —— 测试页面

当前是否登录:

登录 注销

登录之后才能显示:value

不登录才能显示:value

具有角色 admin 才能显示:value

同时具备多个角色才能显示:value

只要具有其中一个角色就能显示:value

不具有角色 admin 才能显示:value

具有权限 user-add 才能显示:value

同时具备多个权限才能显示:value

只要具有其中一个权限就能显示:value

不具有权限 user-add 才能显示:value

当前登录账号:

``` ### 前后端分离 1、在登录时,将 token 信息返回到前端 ``` java // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); // user 信息校验代码不再赘述 ... // 登录 StpUtil.login(user.getId()); StpUtil.getSession().set("user", user); return AjaxJson.getSuccess("登录成功").set("satoken", StpUtil.getTokenValue()); // 关键代码 } ``` 2、前端改造 - 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('satoken', res.satoken)`。 - 2、在后续每次请求中,读取本地保存的 satoken 塞到请求 header 中 ``` js const header = {}; if(localStorage.satoken) { header.satoken = localStorage.satoken; } // 后续提交请求... ``` 1、自定义 SessionManager,从请求 header 里读取前端提交的 token ``` java public class MySessionManager extends DefaultWebSessionManager { private static final String TOKEN = "token"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public MySessionManager() { super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String id = WebUtils.toHttp(request).getHeader(TOKEN); // 如果请求头中有 token 则其值为sessionId if (!StringUtils.isEmpty(id)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return id; } else { //否则按默认规则从cookie取sessionId return super.getSessionId(request, response); } } } ``` 2、注入到 SecurityManager 中 ``` java @Configuration public class ShiroConfigure { // 省略其它次要代码 ... @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); manager.setSessionManager(sessionManager()); return manager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); return bean; } // 自定义sessionManager @Bean public SessionManager sessionManager() { MySessionManager mySessionManager = new MySessionManager(); return mySessionManager; } } ``` 3、测试 Controller,登录时将 token 信息返回到前端 ``` java // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password)); String token = subject.getSession().getId().toString(); // 关键代码 return AjaxJson.getSuccess("登录成功!").set("token", token); // 关键代码 } catch (AuthenticationException e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } ``` 4、前端改造 - 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。 - 2、在后续每次请求中,读取本地保存的 token 塞到请求 header 中 ``` js const header = {}; if(localStorage.token) { header.token = localStorage.token; } // 后续提交请求... ``` ### 集成 Redis pom.xml 引入依赖 ``` xml cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 ``` application.yml 新增连接配置 ``` yaml spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ``` 其它代码照旧 pom.xml 引入依赖 ``` xml org.crazycake shiro-redis 3.3.1 ``` application.yml 新增连接配置 ``` yaml spring: redis: shiro: # Redis服务器地址 host: 127.0.0.1:6379 # Redis服务器连接密码(默认为空) password: # Redis数据库索引(默认为0) database: 2 # 连接超时时间 timeout: 1800 ``` ShiroConfigure 注入相关 Bean ``` java @Configuration public class ShiroConfigure { // 自定义 securityManager @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); // manager.setRealm(myRealm()); // 自定义session管理 使用redis manager.setSessionManager(sessionManager()); // 自定义缓存实现 使用redis manager.setCacheManager(cacheManager()); return manager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); return bean; } // -------- 以下为 shiro redis 相关 -------- // Shiro redis 连接信息 @Value("${spring.redis.shiro.host}") private String host; @Value("${spring.redis.shiro.database}") private int database; @Value("${spring.redis.shiro.timeout}") private int timeout; @Value("${spring.redis.shiro.password}") private String password; /** * 配置shiro redisManager */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); if(StringUtils.hasText(password)){ redisManager.setPassword(password); } redisManager.setDatabase(database); redisManager.setTimeout(timeout); return redisManager; } /** * cacheManager 缓存 redis 实现 */ @Bean public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * RedisSessionDAO redis 实现 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } // 自定义sessionManager @Bean public SessionManager sessionManager() { MySessionManager mySessionManager = new MySessionManager(); mySessionManager.setSessionDAO(redisSessionDAO()); return mySessionManager; } } ``` SysUser 实体类要实现 Serializable 接口 ``` java @Data @NoArgsConstructor @AllArgsConstructor public class SysUser implements Serializable { // ... } ``` 其它代码照旧