Swoft 系列教程:(2)认证服务及组件

Swoft 提供了一整套认证服务组件,基本做到了配置后开箱即用。用户只需根据自身业务实现相应的登录认证逻辑,框架认证组件会调用你的登录业务进行token的签发,而后的请求中token解析、合法性验证也都由框架提供,同时框架开放了token权限认证接口给用户,我们需根据自身业务实现token对当前访问资源权限的认证。下面我们详细讲一下 jwt 的签发及验证、访问控制的流程。token 签发token 签发的基本流程为请求用户登录认证服务,认证通过则签发token。Swoft 的认证组件为我们完成了token签发工作,同时 Swoft 约定了一个Swoft\Auth\Mapping\AuthManagerInterface::login方法作为用户的认证业务的入口。使用到的组件及服务:#认证组件服务,使用此接口类名作为服务名注册到框架服务中Swoft\Auth\Mapping\AuthManagerInterface::class#框架认证组件的具体实现者 token 的签发、合法校验、解析Swoft\Auth\AuthManager#token的会话载体 存储着token的信息Swoft\Auth\Bean\AuthSession#约定用户的认证业务需实现返回Swoft\Auth\Bean\AuthResult的login方法和bool的authenticate的方法Swoft\Auth\Mapping\AccountTypeInterface#用于签发token的必要数据载体 iss/sub/iat/exp/data 传递给 Swoft\Auth\AuthManager 签发 tokenSwoft\Auth\Bean\AuthResult配置项:config/properties/app.php设定auth模式jwtreturn [ … ‘auth’ => [ ‘jwt’ => [ ‘algorithm’ => ‘HS256’, ‘secret’ => ‘big_cat’ ], ] …];config/beans/base.php为\Swoft\Auth\Mapping\AuthManagerInterface::class服务绑定具体的服务提供者return [ ‘serverDispatcher’ => [ ‘middlewares’ => [ … ], … ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ],];App\Models\Logic\AuthLogic实现用户业务的认证,以 Swoft\Auth\Mapping\AccountTypeInterface 接口的约定实现了 login/authenticate方法。login方法返回Swoft\Auth\Bean\AuthResult对象,存储用于jwt签发的凭证:setIdentity 对应 sub,即jwt的签发对象,一般使用uid即可setExtendedData 对应 payload, 即jwt的载荷,存储一些非敏感信息即可authenticate方法签发时用不到,主要在验证请求的token合法性时用到,即检测jwt的sub是否为本平台合法用户<?phpnamespace App\Models\Logic;use Swoft\Auth\Bean\AuthResult;use Swoft\Auth\Mapping\AccountTypeInterface;class AuthLogic implements AccountTypeInterface{ /** * 用户登录认证 需返回 AuthResult 对象 * 返回 Swoft\Auth\Bean\AuthResult 对象 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param array $data * @return AuthResult / public function login(array $data): AuthResult { $account = $data[‘account’]; $password = $data[‘password’]; $user = $this->userDao->getByConditions([‘account’ => $account]); $authResult = new AuthResult(); // 用户验证成功则签发token if ($user instanceof User && $this->userDao->verifyPassword($user, $password)) { // authResult 主标识 对应 jwt 中的 sub 字段 $authResult->setIdentity($user->getId()); // authResult 附加数据 jwt 的 payload $authResult->setExtendedData([self::ID => $user->getId()]); } return $authResult; } /* * 验证签发对象是否合法 这里我们简单验证签发对象是否为本平台用户 * $identity 即 jwt 的 sub 字段 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param string $identity token sub 字段 * @return bool / public function authenticate(string $identity): bool { return $this->userDao->exists($identity); }}Swoft\Auth\AuthManager::login 要求传入用户业务的认证类,及相应的认证字段,根据返回Swoft\Auth\Bean\AuthResult对象判断登录认证是否成功,成功则签发token,返回Swoft\Auth\Bean\AuthSession对象。App\Services\AuthManagerService用户认证管理服务,继承框架Swoft\Auth\AuthManager做定制扩展。比如我们这里实现一个auth方法供登录请求调用,auth 方法中则传递用户业务认证模块来验证和签发token,获取token会话数据。<?php/* * 用户认证服务 * User: big_cat * Date: 2018/12/17 0017 * Time: 16:36 /namespace App\Services;use App\Models\Logic\AuthLogic;use Swoft\Redis\Redis;use Swoft\Bean\Annotation\Bean;use Swoft\Bean\Annotation\Inject;use Swoft\Auth\AuthManager;use Swoft\Auth\Bean\AuthSession;use Swoft\Auth\Mapping\AuthManagerInterface;/* * @Bean() * @package App\Services /class AuthManagerService extends AuthManager implements AuthManagerInterface{ /* * 缓存类 * @var string / protected $cacheClass = Redis::class; /* * jwt 具有自包含的特性 能自己描述自身何时过期 但只能一次性签发 * 用户主动注销后 jwt 并不能立即失效 所以我们可以设定一个 jwt 键名的 ttl * 这里使用是否 cacheEnable 来决定是否做二次验证 * 当获取token并解析后,token 的算法层是正确的 但如果 redis 中的 jwt 键名已经过期 * 则可认为用户主动注销了 jwt,则依然认为 jwt 非法 * 所以我们需要在用户主动注销时,更新 redis 中的 jwt 键名为立即失效 * 同时对 token 刷新进行验证 保证当前用户只有一个合法 token 刷新后前 token 立即失效 * @var bool 开启缓存 / protected $cacheEnable = true; // token 有效期 7 天 protected $sessionDuration = 86400 * 7; /* * 定义登录认证方法 调用 Swoft的AuthManager@login 方法进行登录认证 签发token * @param string $account * @param string $password * @return AuthSession / public function auth(string $account, string $password): AuthSession { // AuthLogic 需实现 AccountTypeInterface 接口的 login/authenticate 方法 return $this->login(AuthLogic::class, [ ‘account’ => $account, ‘password’ => $password ]); }}App\Controllers\AuthController处理用户的登录请求<?php/* * Created by PhpStorm. * User: big_cat * Date: 2018/12/10 0010 * Time: 17:05 /namespace App\Controllers;use App\Services\AuthManagerService;use Swoft\Http\Message\Server\Request;use Swoft\Http\Server\Bean\Annotation\Controller;use Swoft\Http\Server\Bean\Annotation\RequestMapping;use Swoft\Http\Server\Bean\Annotation\RequestMethod;use Swoft\Bean\Annotation\Inject;use Swoft\Bean\Annotation\Strings;use Swoft\Bean\Annotation\ValidatorFrom;/* * 登录认证模块 * @Controller("/v1/auth") * @package App\Controllers /class AuthController{ /* * 用户登录 * @RequestMapping(route=“login”, method={RequestMethod::POST}) * @Strings(from=ValidatorFrom::POST, name=“account”, min=6, max=11, default="", template=“帐号需{min}{max}位,您提交的为{value}”) * @Strings(from=ValidatorFrom::POST, name=“password”, min=6, max=25, default="", template=“密码需{min}{max}位,您提交的为{value}”) * @param Request $request * @return array / public function login(Request $request): array { $account = $request->input(‘account’) ?? $request->json(‘account’); $password = $request->input(‘password’) ?? $request->json(‘password’); // 调用认证服务 - 登录&签发token $session = $this->authManagerService->auth($account, $password); // 获取需要的jwt信息 $data_token = [ ’token’ => $session->getToken(), ’expired_at’ => $session->getExpirationTime() ]; return [ “err” => 0, “msg” => ‘success’, “data” => $data_token ]; }}POST /v1/auth/login 的结果{ “err”: 0, “msg”: “success”, “data”: { “token”: “eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJBcHBcXE1vZGVsc1xcTG9naWNcXEF1dGhMb2dpYyIsInN1YiI6IjgwIiwiaWF0IjoxNTUxNjAyOTk4LCJleHAiOjE1NTIyMDc3OTgsImRhdGEiOnsidWlkIjo4MH19.u2g5yU9ir1-ETVehLFIIZZgtW7u9aOvH2cndMsIY98Y”, “expired_at”: 1552207798 }}这里提及一下为什么要提供在服务端缓存token的选项$cacheEnable。普通的token不像jwt具有自我描述的特性,我们为维护token的有效期只能在服务端缓存其有效期,防止过期失效的token被滥用。jwt可以自我描述过期时间,为什么也要缓存呢?因为jwt自身的描述是只读的,即我们无法让jwt提前过期失效,如果用户退出登录,则销毁token是个不错的安全开发习惯,所以只有在服务端也维护了一份jwt的过期时间,用户退出时过期此token,那么就可以自由控制jwt的过期时间。/* * @param string $token * @return bool /public function authenticateToken(string $token): bool{ … // 如果开启了服务端缓存选项 则验证token是否过期 可变向控制jwt的有效期 if ($this->cacheEnable === true) { try { $cache = $this->getCacheClient() ->get($this->getCacheKey($session->getIdentity(), $session->getExtendedData())); if (! $cache || $cache !== $token) { throw new AuthException(ErrorCode::AUTH_TOKEN_INVALID); } } catch (InvalidArgumentException $e) { $err = sprintf(‘Identity : %s ,err : %s’, $session->getIdentity(), $e->getMessage()); throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, $err); } } $this->setSession($session); return true;}token 解析、验证token的解析及合法性验证实现流程,注意只是验证token的合法性,即签名是否正确,签发者,签发对象是否合法,是否过期。并未对 token 的访问权限做认证。使用到的组件及服务:#调用token拦截服务尝试获取token,并调用token管理服务做解析及合法性验证Swoft\Auth\Middleware\AuthMiddleware#token拦截服务``Swoft\Auth\Mapping\AuthorizationParserInterface::class#token拦截服务提供者,根据token类型调用相应的token解析器``Swoft\Auth\Parser\AuthorizationHeaderParser#token管理服务,由token管理服务提供者提供基础服务,被token解析器调用Swoft\Auth\Mapping\AuthManagerInterface::class#token管理服务提供者,负责签发、解析、合法性验证Swoft\Auth\AuthManagerSwoft\Auth\Middleware\AuthMiddleware负责拦截请求并调用token解析及验证服务。会尝试获取请求头中的Authorization字段值,根据类型Basic/Bearer来选择相应的权限认证服务组件对token做合法性的校验并生成token会话。但并不涉及业务访问权限ACL的验证,即只保证某个token 是本平台合法签发的,不保证此token对当前资源有合法的访问权限。如果Authorization为空的话则视为普通请求。执行流程:Swoft\Auth\Middleware\AuthMiddleware调用 Swoft\Auth\Mapping\AuthorizationParserInterface::class 服务,服务具体由 Swoft\Auth\Parser\AuthorizationHeaderParser实现。服务AuthorizationHeaderParser尝试获取请求头中的Authorization字段值,如果获取到token,则根据token的类型:BasicorBearer来调用具体的解析器。Basic的解析器为`Swoft\Auth\Parser\Handler::BasicAuthHandler,Bearer的解析器为 Swoft\Auth\Parser\Handler::BearerTokenHandler,下面我们具体以Bearer模式的jwt为示例。在获取到类型为Bearer的token后,BearerTokenHandler将会调用Swoft\Auth\Mapping\AuthManagerInterface::class服务的authenticateToken方法来对token进行合法性的校验和解析,即判断此token的签名是否合法,签发者是否合法,签发对象是否合法(注意:调用了App\Models\Logic\AuthLogic::authenticate方法验证),是否过期等。token解析验证非法,则抛出异常中断请求处理。token解析验证合法,则将payload载入本次会话并继续执行。所以我们可以将此中间件注册到全局,请求携带token则解析验证,不携带token则视为普通请求。#config/beans/base.phpreturn [ ‘serverDispatcher’ => [ ‘middlewares’ => [ … \Swoft\Auth\Middleware\AuthMiddleware::class ], … ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ],];<?phpnamespace AppModelsLogic;use SwoftAuthBeanAuthResult;use SwoftAuthMappingAccountTypeInterface;class AuthLogic implements AccountTypeInterface{…/* * 验证签发对象是否合法 这里我们简单验证签发对象是否为本平台用户 * $identity 即 jwt 的 sub 字段 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param string $identity token sub 字段 * @return bool /public function authenticate(string $identity): bool{ return $this->userDao->exists($identity);}}acl鉴权token 虽然经过了合法性验证,只能说明token是本平台签发的,还无法判断此token是否有权访问当前业务资源,所以我们还要引入Acl认证。使用到的组件及服务:#Acl认证中间件Swoft\Auth\Middleware\AclMiddleware#用户业务权限auth服务Swoft\Auth\Mapping\AuthServiceInterface::class#token会话访问组件Swoft\Auth\AuthUserServiceSwoft\Auth\Middleware\AclMiddleware中间件会调用Swoft\Auth\Mapping\AuthServiceInterface::class服务,此服务主要用于Acl认证,即验证当前请求是否携带了合法token,及token是否对当前资源有访问权限。Swoft\Auth\Mapping\AuthServiceInterface::class服务由框架的Swoft\Auth\AuthUserService组件实现获取token会话的部分功能,auth方法则交由用户层重写,所以我们需继承Swoft\Auth\AuthUserService并根据自身业务需求实现auth方法。在继承了Swoft\Auth\AuthUserService的用户业务认证组件中,我们可以尝试获取token会话的签发对象及payload数据:getUserIdentity/getUserExtendData。然后在auth方法中判断当前请求是否有token会话及是否对当前资源有访问权限,来决定返回true or false给AclMiddleware中间件。AclMiddleware中间件获取到用户业务下的auth为false(请求没有携带合法token 401 或无权访问当前资源 403),则终端请求处理。AclMiddleware中间件获取到在用户业务下的auth为true,则说明请求携带合法token,且token对当前资源有权访问,继续请求处理。config/bean/base.phpreturn [ ‘serverDispatcher’ => [ ‘middlewares’ => [ …. //系统token解析中间件 \Swoft\Auth\Middleware\AuthMiddleware::class, … ] ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ], // Acl用户资源权限认证服务 \Swoft\Auth\Mapping\AuthServiceInterface::class => [ ‘class’ => \App\Services\AclAuthService::class, ‘userLogic’ => ‘${’ . \App\Models\Logic\UserLogic::class . ‘}’ // 注入UserLogicBean ],];App\Services\AclAuthService对token做Acl鉴权。<?phpnamespace App\Services;use Swoft\Auth\AuthUserService;use Swoft\Auth\Mapping\AuthServiceInterface;use Psr\Http\Message\ServerRequestInterface;/* * Bean 因在 config/beans/base.php 中已经以参数配置的方式注册,故此处不能再使用Bean注解声明 * Class AclAuthService * @package App\Services /class AclAuthService extends AuthUserService implements AuthServiceInterface{ /* * 用户逻辑模块 * 因本模块是以参数配置的方式注入到系统服务的 * 所以其相关依赖也需要使用参数配置方式注入 无法使用Inject注解声明 * @var App\Models\Logic\UserLogic / protected $userLogic; /* * 配合 AclMiddleware 中间件 验证用户请求是否合法 * true AclMiddleware 通过 *false AclMiddleware throw AuthException * @override AuthUserService * @param string $requestHandler * @param ServerRequestInterface $request * @return bool */ public function auth(string $requestHandler, ServerRequestInterface $request): bool { // 签发对象标识 $sub = $this->getUserIdentity(); // token载荷 $payload = $this->getUserExtendData(); // 验证当前token是否有权访问业务资源 aclAuth为自己的认证逻辑 if ($this->aclAuth($sub, $payload)) { return true; } return false; }} ...

March 5, 2019 · 4 min · jiezi

研发中:联邦SPIFFE信任域

作者:Daniel Feldman介绍联邦信任域是SPIFFE和SPIRE最高需求和活跃开发的功能之一。在这篇博文中,我将概述我们当前的计划以及实施它的挑战。什么是联邦?SPIFFE信任域中的证书共享一个信任根。 这是一个根信任捆绑包,由使用非标准化格式和协议在控制平面之间和内部共享的多个证书组成。然而,这还不够好。许多组织都有多个信任根源:可能是因为他们与不同的管理员有不同的组织划分,或者因为他们有偶尔需要沟通的独立的临时和生产环境。类似的用例是组织之间的SPIFFE互操作性,例如云供应商与其客户之间的互操作性。这两种用例都需要一个定义明确、可互操作的方法,以便一个信任域中的工作负载对不同信任域中的工作负载进行身份验证。这是联邦。联邦设计要实现联邦,我们必须在不同的SPIFFE服务器之间共享公钥。这不是一次性操作;由于密钥轮换,每个信任域的公钥会定期更改。每个联邦域必须定期下载其他域的公钥,其频率至少与密钥轮换一样快。定期下载证书的数据格式尚未最终确定。我们目前的想法是让SPIFFE的实现去使用JWKS格式,在一个众所周知的URL上公开发布证书。然后,要启动联邦关系,实现可以下载JWKS数据,并从中导入证书。我们喜欢JWKS,因为它是一种通用的、可扩展的格式,用于共享可以容纳JWT和X.509证书的密钥信息。(出于安全原因,SPIFFE需要不同的JWT和X.509标识的密钥材料 - 它们不能只是以不同格式编码的相同公钥。)JWKS的灵活性允许单个联邦API支持JWT和X.509 。工作负载APISPIFFE工作负载API提供用于读取联邦公钥的端点。此API与用于读取当前信任域的证书的API不同,所以应用程序可以区分本地和联邦域的客户端。SPIRE的实验支持虽然它尚未正式标准化为SPIFFE的一部分,但是SPIRE已经可以提供JWKS的实验性实施。挑战外部SPIFFE服务器的初始身份验证联邦API存在引导问题:如果双方都没有共享信任根,则无法建立初始安全连接。其一种解决方案,是使用两个SPIFFE服务器信任的证书颁发机构的Web PKI。另一种解决方案,是使用手动身份验证机制来消除对公共证书颁发机构(CA)的需求。SPIRE使用与节点和工作负载注册类似的方式实现联邦。随着我们扩展注册API,可以通过该API操作联邦,就像节点和工作负载注册一样。网络中断容错每次SPIFFE实现,从同等的SPIFFE实现,导入新证书时,它都会使用上一个已知捆绑包对连接进行身份验证。如果网络中断很长,并且两个SPIFFE实现无法通信,超过完整的密钥轮换周期,那么它们将无法继续进行通信,从而破坏了联邦关系。其一种解决方案,是将密钥轮换间隔,设置为长于可能的最长网络中断长度(或者如果发生长中断,则重新初始化联邦)。这是设计权衡:如果密钥轮换间隔较长,则受损密钥也将在较长时间内保持有效。或者,如果Web PKI可用于SPIFFE服务器,则可用于保护联邦连接。我们相信联邦SPIFFE服务器之间的Web PKI,将是一种常见的设计模式,因为它避免了长网络中断导致密钥轮换的问题。传递与双向联邦Kerberos和Active Directory具有与联邦相同的,称为“跨领域信任”。在大多数情况下,跨领域信任是双向的(双方互相信任)和传递(如果A信任B,B信任C,然后A信托C)。SPIFFE中的双向联邦通常(但并非总是如此)是可取的。对于公共API,API提供程序可能希望使用Web PKI来保护连接的服务器端,并使用SPIFFE来保护客户端。因此,我们不会自动配置双向联邦。对于具有许多信任域的大型组织,传递联邦可以简化实现复杂性。但是,传递联邦可能难以推断SPIFFE实现的安全属性。出于这个原因,我们现在没有在SPIFFE中实现传递联邦。目前,用户必须通过添加更多联邦关系,来手动配置传递和双向联邦。联邦信任域SVID的范围在Web PKI中,每个人都信任相同的根证书颁发机构。在SPIFFE中,彼此不完全信任的组织可能仍希望联邦其信任域。应用程序必须验证每个SVID是否由拥有该信任域的SPIFFE服务器颁发。想象一个奇怪的世界,可口可乐和百事可乐必须交换数据。为此,他们联邦各自的信任域。可口可乐的SPIFFE证书根,添加到百事可乐的信托商店,反之亦然。在证书验证的简单实现中,可口可乐服务器可以欺骗性地冒充百事可乐网络上的百事可乐服务器,因为百事可乐信任可口可乐的根证书!这是问题所在:根证书没有“范围”。任何CA都可以为任何名称签署证书。如果所有CA都受信任,例如在单个公司内,则可以。在具有多个CA的环境中,每个CA都应该只允许签署具有特定名称的证书,不然这会导致安全漏洞。防止这种情况的一种方法是使用X.509名称约束扩展。名称约束扩展允许将CA证书限制为为特定域名颁发证书。但是,在TLS库中对名称约束扩展的支持是有限的,并且它不能解决未来SPIFFE身份格式(如JWT)的问题。由于这些原因,SPIFFE不包括名称约束扩展。这意味着所有使用SVID的应用程序都必须检查SVID中的SPIFFE ID,是否与签署证书的实际CA的信任域匹配。这意味着检查百事可乐SVID不是被可口可乐的CA签名。当前广泛使用的应用程序(例如Web服务器和代理)不执行此检查。结论联邦对于SPIFFE的成功实施至关重要。但是,以安全和可用的方式实施它仍然存在挑战。我们正在努力与社区一起努力应对这些挑战,如果您有远见,我们很乐意听取您的意见!要了解有关SPIFFE联邦的更多信息:查看新的Java SPIFFE Federation Demo,它演示了在Tomcat服务器环境中使用SPIRE在两个域之间进行联邦。加入SPIFFE Slack与专家讨论SPIFFE。加入SPIFFE SIG-SPEC双月会议,设计SPIFFE联邦会的未来。 (不久将有一个单独的联邦工作组。)

December 18, 2018 · 1 min · jiezi

spring boot 结合Redis 实现工具类

自己整理了 spring boot 结合 Redis 的工具类引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>加入配置# Redis数据库索引(默认为0)spring.redis.database=0# Redis服务器地址spring.redis.host=localhost# Redis服务器连接端口spring.redis.port=6379实现代码这里用到了 静态类工具类中 如何使用 @Autowiredpackage com.lmxdawn.api.common.utils;import java.util.Collection;import java.util.Set;import java.util.concurrent.TimeUnit;import javax.annotation.PostConstruct;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;/** * 缓存操作类 /@Componentpublic class CacheUtils { @Autowired private RedisTemplate<String, String> redisTemplate; // 维护一个本类的静态变量 private static CacheUtils cacheUtils; @PostConstruct public void init() { cacheUtils = this; cacheUtils.redisTemplate = this.redisTemplate; } /* * 将参数中的字符串值设置为键的值,不设置过期时间 * @param key * @param value 必须要实现 Serializable 接口 / public static void set(String key, String value) { cacheUtils.redisTemplate.opsForValue().set(key, value); } /* * 将参数中的字符串值设置为键的值,设置过期时间 * @param key * @param value 必须要实现 Serializable 接口 * @param timeout / public static void set(String key, String value, Long timeout) { cacheUtils.redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } /* * 获取与指定键相关的值 * @param key * @return / public static Object get(String key) { return cacheUtils.redisTemplate.opsForValue().get(key); } /* * 设置某个键的过期时间 * @param key 键值 * @param ttl 过期秒数 / public static boolean expire(String key, Long ttl) { return cacheUtils.redisTemplate.expire(key, ttl, TimeUnit.SECONDS); } /* * 判断某个键是否存在 * @param key 键值 / public static boolean hasKey(String key) { return cacheUtils.redisTemplate.hasKey(key); } /* * 向集合添加元素 * @param key * @param value * @return 返回值为设置成功的value数 / public static Long sAdd(String key, String… value) { return cacheUtils.redisTemplate.opsForSet().add(key, value); } /* * 获取集合中的某个元素 * @param key * @return 返回值为redis中键值为key的value的Set集合 / public static Set<String> sGetMembers(String key) { return cacheUtils.redisTemplate.opsForSet().members(key); } /* * 将给定分数的指定成员添加到键中存储的排序集合中 * @param key * @param value * @param score * @return / public static Boolean zAdd(String key, String value, double score) { return cacheUtils.redisTemplate.opsForZSet().add(key, value, score); } /* * 返回指定排序集中给定成员的分数 * @param key * @param value * @return / public static Double zScore(String key, String value) { return cacheUtils.redisTemplate.opsForZSet().score(key, value); } /* * 删除指定的键 * @param key * @return / public static Boolean delete(String key) { return cacheUtils.redisTemplate.delete(key); } /* * 删除多个键 * @param keys * @return */ public static Long delete(Collection<String> keys) { return cacheUtils.redisTemplate.delete(keys); }}相关地址GitHub 地址: https://github.com/lmxdawn/vu… ...

November 24, 2018 · 2 min · jiezi

spring boot 利用注解实现权限验证

这里使用 aop 来实现权限验证引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>定义注解package com.lmxdawn.api.admin.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * 后台登录授权/权限验证的注解 ///此注解只能修饰方法@Target(ElementType.METHOD)//当前注解如何去保持@Retention(RetentionPolicy.RUNTIME)public @interface AuthRuleAnnotation { String value();}拦截实现登录和权限验证package com.lmxdawn.api.admin.aspect;import com.lmxdawn.api.admin.annotation.AuthRuleAnnotation;import com.lmxdawn.api.admin.enums.ResultEnum;import com.lmxdawn.api.admin.exception.JsonException;import com.lmxdawn.api.admin.service.auth.AuthLoginService;import com.lmxdawn.api.common.utils.JwtUtils;import io.jsonwebtoken.Claims;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;import java.util.List;/* * 登录验证 AOP /@Aspect@Component@Slf4jpublic class AuthorizeAspect { @Resource private AuthLoginService authLoginService; @Pointcut("@annotation(com.lmxdawn.api.admin.annotation.AuthRuleAnnotation)") public void adminLoginVerify() { } /* * 登录验证 * * @param joinPoint / @Before(“adminLoginVerify()”) public void doAdminAuthVerify(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { throw new JsonException(ResultEnum.NOT_NETWORK); } HttpServletRequest request = attributes.getRequest(); String id = request.getHeader(“X-Adminid”); Long adminId = Long.valueOf(id); String token = request.getHeader(“X-Token”); if (token == null) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } // 验证 token Claims claims = JwtUtils.parse(token); if (claims == null) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } Long jwtAdminId = Long.valueOf(claims.get(“admin_id”).toString()); if (adminId.compareTo(jwtAdminId) != 0) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } // 判断是否进行权限验证 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //从切面中获取当前方法 Method method = signature.getMethod(); //得到了方,提取出他的注解 AuthRuleAnnotation action = method.getAnnotation(AuthRuleAnnotation.class); // 进行权限验证 authRuleVerify(action.value(), adminId); } /* * 权限验证 * * @param authRule / private void authRuleVerify(String authRule, Long adminId) { if (authRule != null && authRule.length() > 0) { List<String> authRules = authLoginService.listRuleByAdminId(adminId); // admin 为最高权限 for (String item : authRules) { if (item.equals(“admin”) || item.equals(authRule)) { return; } } throw new JsonException(ResultEnum.AUTH_FAILED); } }}Controller 中使用使用 AuthRuleAnnotation 注解, value 值就是在数据库里面定义的 权限规则名称/* * 获取管理员列表 */@AuthRuleAnnotation(“admin/auth/admin/index”)@GetMapping("/admin/auth/admin/index")public ResultVO index(@Valid AuthAdminQueryForm authAdminQueryForm, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return ResultVOUtils.error(ResultEnum.PARAM_VERIFY_FALL, bindingResult.getFieldError().getDefaultMessage()); } if (authAdminQueryForm.getRoleId() != null) { List<AuthRoleAdmin> authRoleAdmins = authRoleAdminService.listByRoleId(authAdminQueryForm.getRoleId()); List<Long> ids = new ArrayList<>(); if (authRoleAdmins != null && !authRoleAdmins.isEmpty()) { ids = authRoleAdmins.stream().map(AuthRoleAdmin::getAdminId).collect(Collectors.toList()); } authAdminQueryForm.setIds(ids); } List<AuthAdmin> authAdminList = authAdminService.listAdminPage(authAdminQueryForm); // 查询所有的权限 List<Long> adminIds = authAdminList.stream().map(AuthAdmin::getId).collect(Collectors.toList()); List<AuthRoleAdmin> authRoleAdminList = authRoleAdminService.listByAdminIdIn(adminIds); // 视图列表 List<AuthAdminVo> authAdminVoList = authAdminList.stream().map(item -> { AuthAdminVo authAdminVo = new AuthAdminVo(); BeanUtils.copyProperties(item, authAdminVo); List<Long> roles = authRoleAdminList.stream() .filter(authRoleAdmin -> authAdminVo.getId().equals(authRoleAdmin.getAdminId())) .map(AuthRoleAdmin::getRoleId) .collect(Collectors.toList()); authAdminVo.setRoles(roles); return authAdminVo; }).collect(Collectors.toList()); PageInfo<AuthAdmin> authAdminPageInfo = new PageInfo<>(authAdminList); PageSimpleVO<AuthAdminVo> authAdminPageSimpleVO = new PageSimpleVO<>(); authAdminPageSimpleVO.setTotal(authAdminPageInfo.getTotal()); authAdminPageSimpleVO.setList(authAdminVoList); return ResultVOUtils.success(authAdminPageSimpleVO);}相关地址GitHub 地址: https://github.com/lmxdawn/vu… ...

November 24, 2018 · 2 min · jiezi