7_springboot_shiro_jwt_多端认证鉴权_自定义AuthenticationToken

1. 目标

​ 本小节会先对Shiro的核心流程进行一次回顾,并进行梳理。然后会介绍如果应用是以API接口的方式提供给它方进行调用,那么在这种情况下如何使用Shiro框架来完成接口调用的认证和授权。

2. 核心架构

引用官方的架构图:
在这里插入图片描述

2.1 Subject(主体)

org.apache.shiro.subject.Subject 接口,翻译为主体,主体代表当前与软件系统交互的用户、程序或任何其他实体。Subject可以是实际用户(例如登录的用户),也可以是程序(例如后台任务或定时任务)。Shiro将Subject视为与安全相关操作的主要入口点,它封装了与安全相关的操作和状态。

与 Subject 相关的概念:

  • Principal(身份): Principal代表了Subject的身份信息,通常是唯一标识Subject的信息,比如用户名、用户ID等。Principal通常用于认证过程中,用来标识Subject的身份。在Shiro中,Principal可以是任何对象(Object类型),但通常是字符串或者其他可以唯一标识Subject身份的对象。
  • PrincipalCollection(身份集合): PrincipalCollection是一个集合,用于保存Subject的所有身份信息。在某些情况下,Subject可能具有多个身份信息,例如同时具有用户名、用户ID等多个身份。PrincipalCollection用于保存这些身份信息,并提供了一些便捷的方法来访问和操作这些身份信息。在Shiro中,Subject可以具有一个或多个Principal,它们都被保存在PrincipalCollection中。

在应用开发中,一般我们这样使用Subject:

  1. 获取Subject对象:通过SecurityUtils.getSubject()方法获取当前执行代码的Subject对象。
  2. 认证:如果用户尚未认证(即未登录),可以使用Subject对象进行认证操作。通常是创建一个AuthenticationToken对象,封装用户提交的身份信息和凭证信息,然后调用Subject的login(AuthenticationToken token)方法进行认证。
  3. 授权:认证成功后,可以使用Subject对象来进行权限控制。通过调用Subject的hasRole(String role)isPermitted(String permission)等方法来检查当前用户是否具有某个角色或权限。
  4. 会话管理:Subject对象还可以用于管理用户的会话信息。可以通过Subject对象获取当前用户的会话,或者手动创建会话,设置会话属性等。
  5. 注销:用户操作完成后,可以使用Subject对象进行注销操作,清除用户的认证状态和会话信息。

通过前面章节的分析,Web环境下,请求会先经过SpringShiroFilter过滤器,在过滤器的执行链中,创建Subject对象交给了securityManager来创建,而真正到底层的时候,SubJect 对象最终是由 org.apache.shiro.web.mgt.DefaultWebSubjectFactory 这个工厂来创建的。 在过滤器中调用SecurityManager来创建Subject实例对象之前会创建一个SubjectContext。

Subject 上下文:它的作用是为Subject的创建提供了一个统一的上下文环境,可以在其中设置和获取Subject的相关配置信息,还可以用于传递Subject的上下文信息,例如认证状态、会话状态等。它其实就是一个 java.util.Map , 这个Map中存放了以下的key:

  • SECURITY_MANAGER (securityManager对象)
  • SESSION_ID (sessionId)
  • SUBJECT(subject)
  • PRINCIPALS 身份信息
  • SESSION 会话
  • AUTHENTICATED 是否认证
  • AUTHENTICATION_INFO (reaml 中的 AuthenticationInfo,即认证信息)
  • AUTHENTICATION_TOKEN (提交的认证token信息)

这个对象刚被创建出来的时候,里面的数据是空的。但是随着调用链的深入,这些信息将会被逐步填充进去

在应用中我们一般用SecurityUtils.getSubject() 方法来获取当前的subject对象。我们发现它是一个静态方法,而且不管在什么时候调用,得到的都是同一个subject对象。

前面分析过,过滤器中得到subject 对象之后,subject将会被绑定到当前线程上。实际就是使用了 ThreadLocal 的子类 java.lang.InheritableThreadLocal(它绑定了一个Map结构,map中有两个key,一个是securityManager的key,一个是subject的key) 。Shiro框架用 ThreadContext 这个类对 ThreadLocal 进行了封装,分别提供了绑定和解绑 securityManagersubject 对象的方法。

因为底层使用的是java.lang.InheritableThreadLocal 所以在主线程以及这个主线程创建的子线程中获取到的Subject信息是一致的

在Web应用中,每个HTTP请求都会对应一个Subject对象,而DefaultWebSubjectFactory负责在每个请求到达时创建对应的Subject对象。

2.2 SecurityManager(安全管理器)

定义:

public interface SecurityManager extends Authenticator, Authorizer, SessionManager{...
}

从定义可以看出,SecurityManager虽然叫做 安全管理器,它从Authenticator, Authorizer, SessionManager 几个接口继承而来,也就是说它具备认证和鉴权还有会话管理器的功能。默认情况下使用的实现类是:org.apache.shiro.web.mgt.DefaultWebSecurityManager

安全管理器是Shiro框架的核心组件,负责管理所有的Subject对象,并协调它们之间的安全操作。SecurityManager是一个入口点,提供了对Shiro的所有功能的访问,并负责执行安全策略、协调身份验证和授权、管理会话等操作。

也就是说subject中的一些方法调用,都将全部委托给 SecurityManager对象来完成,它是真正"协调干活" 的人

下面是SecurityManager 三个重要的"能力": Authenticator(认证), Authorizer(鉴权/授权), SessionManager(会话管理)

2.3 Authentication(认证)

org.apache.shiro.authc.Authenticator 是个接口。 通过前面的例子,我们知道认证的过程其实就是 :

  1. 收集用户提供的身份信息,叫做(org.apache.shiro.authc.AuthenticationToken 认证令牌接口),它包含了两部分信息:

    • Principal: 身份信息,Object类型,可以是任意对象。 它与Subject中的Principal概念是一致的,都表示身份,比如用户名
    • Credentials:凭证信息,Object类型,可以是任意对象,比如密码,数字证书 等

    默认使用的是org.apache.shiro.authc.UsernamePasswordToken 实现类

  2. subject 调用login方法进行认证,这个调用转交给 SecurityManager(它继承了Authenticator接口),

  3. SecurityManager 调用对应的Realm, 获取认证信息(org.apache.shiro.authc.AuthenticationInfo ), 它包含了两部分信息:

    • PrincipalCollection 身份信息集合。 注意与 AuthenticationToken中的区别, AuthenticationInfo中是合法的,可以有多个,而 AuthenticationToken中是提交的身份,未认证的身份信息,只是一个
    • Credentials: 凭证信息。 注意与 AuthenticationToken中的区别, AuthenticationInfo中是合法凭证,如密码。 而 而 AuthenticationToken中是提交的凭证,未认过的。

    默认使用的是org.apache.shiro.authc.SimpleAuthenticationInfo 实现类。

    如果为SecurityManager配置了缓存管理器,SecurityManager会将这个缓存管理器应用到每个Reaml上, Reaml 获取的AuthenticationInfo就会被缓存起来了。

  4. Realm调用配置给它的匹配器 org.apache.shiro.authc.credential.CredentialsMatcher 将 AuthenticationToken和 AuthenticationInfo 进行对比,判定是否认证成功

  5. SecurityManager 调用SessionManager创建Session,并调用sessionDAO 保存session

2.4 Authorization(授权)

org.apache.shiro.authz.Authorizer是个接口, SecurityManager(它继承了Authorizer接口)。 前面例子中,在Controller方法上使用了 @RequiresRoles("admin"), @RequiresPermissions("employee:read") 等,此时就会执行授权流程,或者直接调用subject.checkPermission ,subject.isPermitted 方法就会进去授权流程。SecurityManager 同样会调用realm 来获取 org.apache.shiro.authz.AuthorizationInfo, 其中包含了权限与角色信息。常用的实现类是org.apache.shiro.authz.SimpleAuthorizationInfo

同样,如果配置了缓存管理器,AuthorizationInfo将会被缓存起来。

2.5 Realm(域)

org.apache.shiro.realm.Realm 是个接口,一般应用都会自定义Realm,都会继承org.apache.shiro.realm.AuthorizingRealm即可, Realm从数据源(如数据库)中获取用户身份(Principal)和权限信息,并根据这些信息进行认证和授权操作。在认证过程中,Realm根据传入的Principal(通常是用户名)从数据源中获取对应的密码和其他身份信息,然后与传入的凭证进行比较以验证身份的真实性。在授权过程中,Realm根据Principal获取对应的权限信息,并判断Subject是否具有某项操作的权限。

前面我们自己定义了SystemAccountRealm 用Map模拟了用户身份信息,角色,权限信息。自定义了一个匹配器 Sha256HashCredentialsMatcher 对密码加salt后进行了两次 hash计算,再与AuthenticationInfo 中的凭证进行比较。

2.6 SessionManager

org.apache.shiro.session.mgt.SessionManager 是个接口,SecurityManager继承了这个接口,用来管理session。前面我们定义了自己的SessionManager AccessTokenWebSessionManager 实现了在禁用Cookie的情况下,从请求头中获取SessionID来保持会话。

2.7 SessionDAO

org.apache.shiro.session.mgt.eis.SessionDAO 主要用来实现Session的增,删,改。前面我们实现了 ShiroRedisSessionDAO 用来把session保存到Redis中

2.8 CacheManager

org.apache.shiro.cache.CacheManager 是个接口,前面我们自己实现了ShiroRedisCacheManager,用来将 AuthenticationInfo ,和AuthorizationInfo 缓存到Redis中。当然值实现 CacheManager 是不行的,还写了一个 ShiroRedisCache 实现了 org.apache.shiro.cache.Cache 接口。

也可以为SessionManager设置缓存管理器,用来缓存活跃session数据

3. 对API接口访问的认证

如果现在我们的应用需要开放API接口供它方进行调用,一般我们会为它方应用分配一个以下几个参数:

  • access_key 身份标识符

  • secret_key 秘钥,一般用来对API请求进行签名,防止请求数据被劫持,篡改后重放。

  • app_id 应用ID。如果它方有多种不同的应用要接入,可以使用这个参数来标识不同的应用场景。这个参数不是必须的,可以根据实际情况来决定是否需要分配这个参数。

3.1 它方接入规范

它方拿到分配的参数后,我们需要制定接入规范,这里做一些简单的HTTP报文规范:

  1. 所有HTTP报文METHOD使用 POST

  2. 数据以JSON格式放入到 HTTP报文 BODY中。(文件传输除外)

  3. HTTP报文请求头加入 :

    • X-Access-Key 分配的身份标识

    • X-Access-Timestamp 请求发起的时间戳(Unix timestamp)毫秒单位

    • X-Access-Sign 请求数据签名

      签名算法:SignContent =JSON字符串(UTF-8编码)自然排序+时间戳 , Sign=SHA256(SignContent,secret_key )

    • X-Access-AppId 应用程序ID

3.2 服务端

它方按照上面的规范组织好报文,然后发送给服务端。服务端利用Shiro框架来进行认证和验证签名。

此时客户端提交的报文首先经过我们自己定义的Filter。前面代码也自定义了一个Filter,因为是使用用户名,密码的认证方式,所以它从org.apache.shiro.web.filter.authc.FormAuthenticationFilter 继承,使用的是 UsernamePasswordToken ,这个Token是框架自带的。

现在的情况发生了变化,提交的不再是用户名密码,而是分配的X-Access-KeyX-Access-AppId ,还有时间戳,签名等信息。所以我们要自定义AuthenticationToken,每个请求都需要进行认证。这个例子中就只做简单验证:X-Access-Key,X-Access-AppId 能和数据库中的信息对应上而且签名正确就认证成功。具体项目中根据安全级别可以自行设计更加复杂,安全性更高的认证算法。

身份信息保存在了数据库中,那么每次都要查询效率很低,所以需要引入缓存。

所以接下来需要做如下几件事情:

  1. 自定义AuthenticationToken , 直接实现org.apache.shiro.authc.AuthenticationToken 接口
  2. 自定义Filter,继承org.apache.shiro.web.filter.authc.AuthenticatingFilter在Filter中完成 AuthenticationToken 的创建,执行登录。因为只有执行登录,securityManager才会通过reaml来完成认证的动作
  3. 自定义Realm,继承org.apache.shiro.realm.AuthorizingRealm
  4. 自定义匹配器,继承 com.qinyeit.shirojwt.demos.shiro.matcher.CodecSupport
  5. 配置
    1. 配置filter
    2. 配置realm

4. 自定义AuthenticationToken

这里直接实现AuthenticationToken 接口

package com.qinyeit.shirojwt.demos.shiro.token;import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;
@Data
public class ApiAuthenticationToken implements AuthenticationToken {private String accessKey; // 身份标识private String accessTimestamp;// 访问时间戳private String accessSign;// 参数签名private String accessAppId; // 客户端应用IDprivate String requestBody; //请求报文Body,JSON格式public ApiAuthenticationToken(String accessKey, String accessTimestamp,String accessSign, String accessAppId, String requestBody) {this.accessKey = accessKey;this.accessTimestamp = accessTimestamp;this.accessSign = accessSign;this.accessAppId = accessAppId;this.requestBody = requestBody;}// 身份信息@Overridepublic Object getPrincipal() {return getAccessKey(); // 返回身份标识}// 凭证@Overridepublic Object getCredentials() {return accessSign; // 返回参数签名}
}

5. 自定义Filter

这个Filter 直接从 AuthenticatingFilter 继承。 在这个类中主要完成两个任务:

  1. 创建自定义的ApiAuthenticationToken对象。即从请求报文中取出需要的数据。
  2. 执行登录,让框架进行认证

5.1 构建ApiAuthenticationToken对象

我们首先需要从请求头上取出:

  • X-Access-Key 分配的身份标识
  • X-Access-Timestamp 请求发起的时间戳(Unix timestamp)毫秒单位
  • X-Access-Sign 请求数据签名
  • X-Access-AppId 应用程序ID
  • HTTP Body 内容

这里有一个问题: 取出body需要通过request中的Stream来读取其内容,一旦stream被读取之后,它是无法重置的,这样这个reqeust对象到达Spring Web框架的时候,Spring Controller 就无法获取请求的内容了。所以这里我们需要一个HttpServletRequestWrapper 类对reqeust对象进行包装,使得后续spring Controller中还可以继续获取内容。

Spring提供了org.springframework.web.util.ContentCachingRequestWrapper ,它从javax.servlet.http.HttpServletRequestWrapper 继承,从原始InputStream 流中读取内容,并包装到内部的ContentCachingInputStream中使得后续可以继续获取请求体的内容。

查看源码后发现,它对于底层的 ServletInputSream并没有很好的封装,我们现在需要的是在Filter中读取Request Body中的内容。但是实验发现读取不到。所以干脆就仿照 ContentCachingRequestWrapper 自己封装一个,名字还是叫做 ContentCachingRequestWrapper ,其思路就是在ContentCachingRequestWrapper 的构造方法中,立即读取ServletInputStream中的内容缓存起来,即将原始流中的内容拷贝到 ·ByteArrayOutputStream· 中。后面需要读取数据的时候,将缓存中所有的字节读取出来再次包装成 ServletInputSream,这样就可以重复读取数据了。

public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {private static final Logger                LOGGER = LoggerFactory.getLogger(ContentCachingRequestWrapper.class);private final        ByteArrayOutputStream cachedContent;private              Map<String, String[]> cachedForm;@Nullableprivate ServletInputStream inputStream;public ContentCachingRequestWrapper(HttpServletRequest request) {super(request);this.cachedContent = new ByteArrayOutputStream();this.cachedForm = new HashMap<>();cacheData();}@Overridepublic ServletInputStream getInputStream() throws IOException {this.inputStream = new ContentCachingInputStream(cachedContent.toByteArray());return this.inputStream;}@Overridepublic String getCharacterEncoding() {String enc = super.getCharacterEncoding();return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));}@Overridepublic String getParameter(String name) {String value = null;if (isFormPost()) {String[] values = cachedForm.get(name);if (null != values && values.length > 0) {value = values[0];}}if (StringUtils.isEmpty(value)) {value = super.getParameter(name);}return value;}@Overridepublic Map<String, String[]> getParameterMap() {if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {return cachedForm;}return super.getParameterMap();}@Overridepublic Enumeration<String> getParameterNames() {if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {return Collections.enumeration(cachedForm.keySet());}return super.getParameterNames();}@Overridepublic String[] getParameterValues(String name) {if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {return cachedForm.get(name);}return super.getParameterValues(name);}private void cacheData() {try {if (isFormPost()) {this.cachedForm = super.getParameterMap();} else {ServletInputStream inputStream = super.getInputStream();StreamUtils.copy(inputStream, this.cachedContent);}} catch (IOException e) {LOGGER.warn("[RepeatReadHttpRequest:cacheData], error: {}", e.getMessage());}}private boolean isFormPost() {String contentType = getContentType();return (contentType != null &&(contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ||contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) &&HttpMethod.POST.matches(getMethod()));}private class ContentCachingInputStream extends ServletInputStream {private final ByteArrayInputStream inputStream;public ContentCachingInputStream(byte[] bytes) {this.inputStream = new ByteArrayInputStream(bytes);}@Overridepublic int read() throws IOException {return this.inputStream.read();}@Overridepublic int readLine(byte[] b, int off, int len) throws IOException {return this.inputStream.read(b, off, len);}@Overridepublic boolean isFinished() {return this.inputStream.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener listener) {}}}

接着在Filter中进行包装:


@Slf4j
public class ApiAuthenticationFilter extends AuthenticatingFilter {private boolean isNeedWrapper(ServletRequest request) {// 因为请求先通过了 ShiroFilter,已经被包装成了ShiroHttpServletRequest// 如果没有包装成 ShiroHttpServletRequest,说明不是Shiro环境,就没有必要包装if (!(request instanceof ShiroHttpServletRequest)) {return false;}HttpServletRequest req           = WebUtils.toHttp(request);String             requestMethod = req.getMethod().toUpperCase();//只针对 json数据提交,并且是POST提交或者是PUT提交if (request.getContentType() != null&& request.getContentType().contains("application/json")&& ("POST".equals(requestMethod) || "PUT".equals(requestMethod))) {return true;}return false;}// 包装 request对象,使得请求到达SpringWeb框架后可以重复读取请求体内容@Overridepublic void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)throws ServletException, IOException {if (isNeedWrapper(request)) { // 满足条件才进行包装,否则不包装super.doFilterInternal(new ContentCachingRequestWrapper(WebUtils.toHttp(request)), response, chain);} else {super.doFilterInternal(request, response, chain);}}/*** 从请求中获取认证Token信息** @param request* @param response* @return* @throws Exception*/@Overrideprotected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest           req                  = WebUtils.toHttp(request);String                       accessKey            = req.getHeader("X-Access-Key");String                       accessTimestamp      = req.getHeader("X-Access-Timestamp");String                       accessSign           = req.getHeader("X-Access-Sign");String                       accessAppId          = req.getHeader("X-Access-AppId");ContentCachingRequestWrapper cachedRequestWrapper = (ContentCachingRequestWrapper) request;String requestBody = new String(cachedRequestWrapper.getContentAsByteArray(),cachedRequestWrapper.getCharacterEncoding());return new ApiAuthenticationToken(accessKey, accessTimestamp, accessSign, accessAppId, requestBody);}}

5.2 登录认证

在现在的场景下,每个API的每次调用都需要进行认证,不需要进行会话保持,每次请求过来都是未认证的,所以一定会调用onAccessDenied,所以只需要在这个方法中做登录认证操作即可


@Slf4j
public class ApiAuthenticationFilter extends AuthenticatingFilter {...@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {// 父类中的executeLogin 方法会调用 onLoginSuccess或者 onLoginFailure,所以要重写这两个方法return super.executeLogin(request, response);}// 认证成功直接放行@Overrideprotected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {return true;}// 认证失败响应消息@Overrideprotected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {Map<String, ?> result = Map.of("code", 401, "msg", "未授权,请联系我们");responseJsonResult(result, response);return false;}// 向调用方发送JSON数据private void responseJsonResult(Map<String, ?> result, ServletResponse response) {if (response instanceof HttpServletResponse res) {res.setContentType("application/json;charset=UTF-8");res.setStatus(200);res.setCharacterEncoding("UTF-8");try {// 输出JSON 数据res.getWriter().write(JSON.toJSONString(result));res.getWriter().flush();res.getWriter().close();} catch (Exception e) {log.error(e.getMessage(), e);}}} 
}

6. 自定义Realm

自定义的Realm直接继承AuthorizingRealm,声明它支持的Token类型是ApiAuthenticationToken

6.1 准备一些静态数据

@Data
@ToString
@Builder
public class ApiAccount implements Serializable {private String appId;private String accessKey;private String secretKey;
}

6.2 准备一个匹配器

匹配器是用来对比数据的,即对比提交的 AauthenticationToken 中的内容和 从Realm中获取的认证信息是否匹配。

这里我们需要做两个方面的验证:

  1. 提交的AccessKey和 AppID是否在我们的系统中存在,如果不存在则不允许访问
  2. 验证请求参数的签名
public class ApiAuthenticationCredentialsMatcher extends CodecSupport implements CredentialsMatcher {@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {// 取出真实身份信息Object primaryPrincipal = info.getPrincipals().getPrimaryPrincipal();// 取出 token 中的身份信息ApiAuthenticationToken apiAuthenticationToken = (ApiAuthenticationToken) token;// 如果身份信息是 SystemAccount 对象// 此时要注意,Realm 中要将 ApiAccount 对象放入到 AuthenticationInfo 中if (primaryPrincipal instanceof ApiAccount account) {String accessKey = account.getAccessKey();// 秘钥String secretKey = account.getSecretKey();String appId     = account.getAppId();//简单验证账号信息,这里可以根据需要增加验证复杂性if (accessKey.equals(apiAuthenticationToken.getAccessKey()) &&appId.equals(apiAuthenticationToken.getAccessAppId())) {// 验证签名return verifySign(secretKey, apiAuthenticationToken);}}return false;}// 验证签名 从realm中取出 secreKey 秘钥进行签名,然后与提交的签名进行对比private boolean verifySign(String secretKey, ApiAuthenticationToken apiAuthenticationToken) {// 提交的签名串String signInToken = apiAuthenticationToken.getAccessSign();if (StringUtils.isBlank(signInToken)) {return false;}log.info("body:{}", apiAuthenticationToken.getRequestBody());// SignContent =JSON字符串(UTF-8编码)字典排序+时间戳 , Sign=SHA256(SignContent,`secret_key` )// 字符串字典顺序排序char[] jsonChars = apiAuthenticationToken.getRequestBody().toCharArray();log.info("jsonChars:{}", jsonChars);Arrays.sort(jsonChars);log.info("jsonChars:{}", jsonChars);String signContent = new String(jsonChars) + apiAuthenticationToken.getAccessTimestamp();// 签名String sign = new Sha256Hash(signContent, secretKey).toHex();log.info("signContent:{}", signContent);log.info("signInToken:{}, timestamp:{}", signInToken, apiAuthenticationToken.getAccessTimestamp());log.info("sign:{}", sign);// 比较两个签名return signInToken.equals(sign);}
}

6.3 定义Realm

@Slf4j
public class ApiAuthenticationRealm extends AuthorizingRealm {private Map<String, ApiAccount> apiAccountMap = new HashedMap();// 模拟数据库public ApiAuthenticationRealm() {// 指定密码匹配器super(new ApiAuthenticationCredentialsMatcher());// key是 accessTokenapiAccountMap.put("db0f017ac3cacb", ApiAccount.builder().accessKey("db0f017ac3cacb").secretKey("cbce2d1aad0867f8317e7ebeb3427999").appId("123456").build());apiAccountMap.put("f0ac034fad089", ApiAccount.builder().accessKey("f0ac034fad089").secretKey("cbce2d1aad0867f8317e7ebeb3427888").appId("456789").build());}// 声明它只支持 ApiAuthenticationToken@Overridepublic boolean supports(AuthenticationToken token) {return token != null && ApiAuthenticationToken.class.isAssignableFrom(token.getClass());}@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {// 如果需要某些api需要授权才能访问,这里可以返回授权信息return null;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {// 1.从传过来的认证Token信息中,实际类型是ApiAuthenticationToken// ApiAuthenticationToken 重写了 getPrincipal() 返回的就是 accessKeyString accessKey = token.getPrincipal().toString();log.info("Realm accessKey:{}", accessKey);// 2.通过用户名到数据库中获取整个用户对象ApiAccount apiAccount = apiAccountMap.get(accessKey);if (apiAccount == null) {throw new UnknownAccountException();}// 3. 创建认证信息,即用户正确的用户名和密码。// 四个参数:// 第一个参数为主体,第二个参数为凭证,第三个参数为Realm的名称// 因为上面将凭证信息和主体身份信息都保存在 apiAccount,所以这里直接将 apiAccount 对象作为主体信息即可// 第二个参数表示凭证,匹配器中会从 SystemAccount中获取盐值,密码登凭证信息,所以这里直接传null。// 第三个参数,表示盐值,这里使用了自定义的SaltSimpleByteSource,之所以在这里new了一个自定义的SaltSimpleByteSource,// 是因为开启redis缓存的情况下,序列化会报错// 第四个参数表示 Realm的名称// 这里将 apiAccount 整个对象放进去,其它传空,匹配器中能获取到apiAccount 就可以进行对比认证了SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(apiAccount,null,null,getName());return authenticationInfo;}
}

7. 配置

因为api调用场景下,都是是无状态该的。所以基本上不会的session进行跟踪。所以无需再配置 sessionManager和 SessionDAO

@Configuration
@Slf4j
public class ShiroConfiguration {@Beanpublic Realm realm() {ApiAuthenticationRealm realm = new ApiAuthenticationRealm();// 开启全局缓存realm.setCachingEnabled(true);// 打开认证缓存realm.setAuthenticationCachingEnabled(true);// 认证缓存的名字,不设置也可以,默认由realm.setAuthenticationCacheName("shiro:authentication:cache");return realm;}@Beanpublic CacheManager cacheManager(RedisTemplate redisTemplate) {RedisSerializer<String> stringSerializer = RedisSerializer.string();// 设置key的序列化器redisTemplate.setKeySerializer(stringSerializer);// 设置 Hash 结构中 key 的序列化器redisTemplate.setHashKeySerializer(stringSerializer);return new ShiroRedisCacheManager(redisTemplate);}/*** 重要配置* ShiroFilter 的 FactoryBean** @param securityManager* @return*/@Beanprotected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();filterFactoryBean.setSecurityManager(securityManager);filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());filterFactoryBean.setFilters(getCustomerShiroFilter());return filterFactoryBean;}/*** URL配置** @return*/private ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();chainDefinition.addPathDefinition("/**", "authc");return chainDefinition;}/*** 自定义拦截器** @return*/private Map<String, Filter> getCustomerShiroFilter() {ApiAuthenticationFilter authcFilter = new ApiAuthenticationFilter();Map<String, Filter>     filters     = new HashMap<>();filters.put("authc", authcFilter);return filters;}

8. 准备Controller接收数据

@RestController
@Slf4j
@RequestMapping("/api/employees")
public class EmployeeApiController {@PostMappingpublic void create(@RequestBody Employee employee) {log.info("创建员工: {}", employee);}
}

9. 写一个用例计算签名

这里我们使用固定提交的数据,然后计算出签名

@Slf4j
public class ApiSignTest {@Testpublic void getSign() {// 请求地址   /api/employees// 请求参数Employee employee = new Employee();employee.setName("张三");employee.setGender("男");String jsonBody = JSON.toJSONString(employee);// 请求时间戳Long timestamp = System.currentTimeMillis();// 签名秘钥String secretKey = "cbce2d1aad0867f8317e7ebeb3427999";char[] jsonChars = jsonBody.toCharArray();Arrays.sort(jsonChars);String signContent = new String(jsonChars) + timestamp;// 签名String sign = new Sha256Hash(signContent, secretKey).toHex();log.info("请求地址:{}", "/api/employees");log.info("X-Access-Key:{}", "db0f017ac3cacb");log.info("X-Access-Timestamp:{}", timestamp);log.info("X-Access-Sign:{}", sign);log.info("X-Access-AppId:{}", "123456");log.info("Request Body:{}", jsonBody);}
}

输出:

请求地址:/api/employees
X-Access-Key:db0f017ac3cacb
X-Access-Timestamp:1711866992050
X-Access-Sign:987b71f4961d78b95acaa019a70ac0a6439a6a566d9bb800fa0078feba8d7864X-Access-AppId:123456Request Body:{"gender":"男","name":"张三"}

10. 发送报文

POST /api/employees HTTP/1.1
Host: 127.0.0.1:8080
X-Access-Key: db0f017ac3cacb
X-Access-Timestamp: 1711866992050
X-Access-Sign: 987b71f4961d78b95acaa019a70ac0a6439a6a566d9bb800fa0078feba8d7864
X-Access-AppId: 123456
Content-Type: application/json
Content-Length: 43{"name": "张三","gender": "男"}

如果access-key , appid没有对应,或者签名不正确则会返回:

{"code": 401,"msg": "未授权,请联系我们"
}

代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 7_springboot_shiro_jwt_多端认证鉴权_自定义AuthenticationToken 分支上.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/784287.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

go: go.mod file not found in current directory or any parent directory.如何解决?

这个错误表明你正在执行 go get 命令&#xff0c;但是当前目录或任何父目录中都找不到 go.mod 文件。这可能是因为你的项目还没有使用 Go Modules 进行管理。 要解决这个问题&#xff0c;有几种方法&#xff1a; go mod init <module-name> 其中 <module-name>…

第四篇:3.3 无效流量(Invalid traffic) - IAB/MRC及《增强现实广告效果测量指南1.0》

翻译计划 第一篇概述—IAB与MRC及《增强现实广告效果测量指南》之目录、适用范围及术语第二篇广告效果测量定义和其他矩阵之- 3.1 广告印象&#xff08;AD Impression&#xff09;第三篇广告效果测量定义和其他矩阵之- 3.2 可见性 &#xff08;Viewability&#xff09;第四篇广…

Unix信号处理

信号的基本概念我已经在上一节中简单介绍了&#xff0c;大家可以去看我的上一篇博客&#xff1a; Unix中的进程和线程-2-CSDN博客 1.信号的产生 kill函数&#xff1a; #include <signal.h> #include <fcntl.h> #include<t_stdio.h> //自定义信号处理函数,n为…

云服务器8核32G配置报价大全,腾讯云、阿里云和京东云

8核32G云服务器租用优惠价格表&#xff0c;云服务器吧yunfuwuqiba.com整理阿里云8核32G服务器、腾讯云8核32G和京东云8C32G云主机配置报价&#xff0c;腾讯云和京东云是轻量应用服务器&#xff0c;阿里云是云服务器ECS&#xff1a; 阿里云8核32G服务器 阿里云8核32G服务器价格…

鸿蒙手机cordova-plugin-camera不能拍照和图片不显示问题

鸿蒙手机cordova-plugin-camera不能拍照和图片不显示问题 一、运行环境 1、硬件 手机型号&#xff1a;NOVA 7 系统&#xff1a;HarmonyOS版本 4.0.0 2、软件 android SDK platforms&#xff1a;14.0(API Level 34)、13.0&#xff08;API Level 33&#xff09; SDK Build-T…

CentOS系统下Docker的安装教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

K8S之Secret的介绍和使用

Secret Secret的介绍Secret的使用通过环境变量引入Secret通过volume挂载Secret Secret的介绍 Secret是一种保护敏感数据的资源对象。例如&#xff1a;密码、token、秘钥等&#xff0c;而不需要把这些敏感数据暴露到镜像或者Pod Spec中。Secret可以以Volume或者环境变量的方式使…

【Linux】TCP网络套接字编程+守护进程

文章目录 日志类&#xff08;完成TCP/UDP套接字常见连接过程中的日志打印&#xff09;单进程版本的服务器客户端通信多进程版本和多线程版本守护进程化的多线程服务器 日志类&#xff08;完成TCP/UDP套接字常见连接过程中的日志打印&#xff09; 为了让我们的代码更规范化&…

车载电子电器架构 —— 诊断数据库开发

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

Python 后端 Flask 使用 Flask-SocketIO、前端 Vue3 实现长连接 Websocket 通信详细教程(更新中)

Flask 安装 Flask-Socketio Flask-SocketIO 第三方库使 Flask 应用程序可以实现客户端和服务器之间的低延迟双向通信。客户端应用程序可以使用 Javascript、Python、C、Java 和 Swift 中的任何 SocketIO 客户端库或任何其他兼容客户端来建立与服务器的永久连接。 Flask-Socke…

基于微信小程序的自习室预约系统的设计与实现

基于微信小程序的自习室预约系统的设计与实现 文章目录 基于微信小程序的自习室预约系统的设计与实现1、前言介绍2、功能设计3、功能实现4、开发技术简介5、系统物理架构6、系统流程图7、库表设计8、关键代码9、源码获取10、 &#x1f389;写在最后 1、前言介绍 伴随着信息技术…

AR-Net网络(图像篡改检测)

AR-Net网络 摘要AbstractAR-Net1. 文献摘要2. 研究背景3. 创新点4. AR-Net 网络架构5. 实验6. 结论总结 摘要 AR-Net使用自适应注意力机制来融合位置和通道维度的特征&#xff0c;使网络能够充分利用不同维度的被篡改特征&#xff0c;此外&#xff0c;AR-Net 改进了预测掩模&a…

民宿预定(源码+文档)

民宿预定系统&#xff08;小程序、ios、安卓都可部署&#xff09; 文件包含内容程序简要说明含有功能项目截图客户端注册页学生特权介绍页我的界面登录界面民宿界面推荐房形已完成订单首页邀请好友待支付页全部订单进行中订单 管理端关键字管理用户管理订单管理民宿管理 文件包…

【春秋云镜】CVE-2023-27179靶标Wp

0x01&#xff1a;漏洞点 他的标题已经告诉我们路径在哪里&#xff0c;所以我们直接访问/_admin/imgdownload.php OK啊白白的一片&#xff0c;直接丢Yakit里面去 教训他。 0x02&#xff1a;操作部分 报告长官&#xff0c;一切正常&#xff01;&#xff01;未发现连接错误&#…

C语言-malloc(申请函数)free(释放函数)

malloc和free的语法格式 malloc 函数是 C 语言标准库中的一个重要函数&#xff0c;用于动态分配内存。其语法如下&#xff1a; void *malloc(size_t size);这里的 void * 表示返回的是一个 void 类型的指针&#xff0c;实际上这个指针指向的是一个 char 类型的内存块。size_t …

HTTP/1.1 如何优化?(计算机网络)

有三种方法&#xff1a; 第一个思路是&#xff0c;通过缓存技术来避免发送 HTTP 请求。客户端收到第一个请求的响应后&#xff0c;可以将其缓存在本地磁盘&#xff0c;下次请求的时候&#xff0c;如果缓存没过期&#xff0c;就直接读取本地缓存的响应数据。如果缓存过期&#…

GridLayoutManager 中的一些坑

前言 如果GridLayoutManager使用item的布局都是wrap_cotent 那么会在布局更改时会出现一些出人意料的情况。&#xff08;本文完全不具备可读性和说教性&#xff0c;仅为博主方便查找问题&#xff09; 布局item: <!--layout_item.xml--> <?xml version"1.0&qu…

C语言-文件操作函数基础+进阶标准输入流输出流

学习的流程 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————…

RedisDesktopManager 安装

简介&#xff1a;安装redis可视化工具 一、下载压缩包 Redis 可视化工具 链接&#xff1a;https://pan.baidu.com/s/1P2oZx9UpQbXDsxJ3GPUeOQ 提取码&#xff1a;6rft Redis 命令窗口版本 链接&#xff1a;https://pan.baidu.com/s/1mIuxCEWwD__aoqp1Cx8MFQ 提取码&#xf…

Lucene及概念介绍

Lucene及概念介绍 基础概念倒排索引索引合并分析查询语句的构成 基础概念 Document&#xff1a;我们一次查询或更新的载体&#xff0c;对比于实体类 Field&#xff1a;字段&#xff0c;是key-value格式的数据&#xff0c;对比实体类的字段 Item&#xff1a;一个单词&#xff0…