赞
踩
本小节会先对Shiro的核心流程进行一次回顾,并进行梳理。然后会介绍如果应用是以API接口的方式提供给它方进行调用,那么在这种情况下如何使用Shiro框架来完成接口调用的认证和授权。
引用官方的架构图:
org.apache.shiro.subject.Subject
接口,翻译为主体,主体代表当前与软件系统交互的用户、程序或任何其他实体。Subject可以是实际用户(例如登录的用户),也可以是程序(例如后台任务或定时任务)。Shiro将Subject视为与安全相关操作的主要入口点,它封装了与安全相关的操作和状态。
与 Subject 相关的概念:
在应用开发中,一般我们这样使用Subject:
SecurityUtils.getSubject()
方法获取当前执行代码的Subject对象。login(AuthenticationToken token)
方法进行认证。hasRole(String role)
、isPermitted(String permission)
等方法来检查当前用户是否具有某个角色或权限。通过前面章节的分析,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
进行了封装,分别提供了绑定和解绑 securityManager
和subject
对象的方法。
因为底层使用的是
java.lang.InheritableThreadLocal
所以在主线程以及这个主线程创建的子线程中获取到的Subject信息是一致的
在Web应用中,每个HTTP请求都会对应一个Subject对象,而DefaultWebSubjectFactory
负责在每个请求到达时创建对应的Subject对象。
定义:
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(会话管理)
org.apache.shiro.authc.Authenticator
是个接口。 通过前面的例子,我们知道认证的过程其实就是 :
收集用户提供的身份信息,叫做(org.apache.shiro.authc.AuthenticationToken
认证令牌接口),它包含了两部分信息:
默认使用的是org.apache.shiro.authc.UsernamePasswordToken
实现类
subject 调用login方法进行认证,这个调用转交给 SecurityManager(它继承了Authenticator接口),
SecurityManager 调用对应的Realm, 获取认证信息(org.apache.shiro.authc.AuthenticationInfo
), 它包含了两部分信息:
默认使用的是org.apache.shiro.authc.SimpleAuthenticationInfo
实现类。
如果为SecurityManager配置了缓存管理器,SecurityManager会将这个缓存管理器应用到每个Reaml上, Reaml 获取的AuthenticationInfo就会被缓存起来了。
Realm调用配置给它的匹配器 org.apache.shiro.authc.credential.CredentialsMatcher
将 AuthenticationToken和 AuthenticationInfo 进行对比,判定是否认证成功
SecurityManager 调用SessionManager创建Session,并调用sessionDAO 保存session
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将会被缓存起来。
org.apache.shiro.realm.Realm
是个接口,一般应用都会自定义Realm,都会继承org.apache.shiro.realm.AuthorizingRealm
即可, Realm从数据源(如数据库)中获取用户身份(Principal)和权限信息,并根据这些信息进行认证和授权操作。在认证过程中,Realm根据传入的Principal(通常是用户名)从数据源中获取对应的密码和其他身份信息,然后与传入的凭证进行比较以验证身份的真实性。在授权过程中,Realm根据Principal获取对应的权限信息,并判断Subject是否具有某项操作的权限。
前面我们自己定义了SystemAccountRealm
用Map模拟了用户身份信息,角色,权限信息。自定义了一个匹配器 Sha256HashCredentialsMatcher
对密码加salt后进行了两次 hash计算,再与AuthenticationInfo 中的凭证进行比较。
org.apache.shiro.session.mgt.SessionManager
是个接口,SecurityManager继承了这个接口,用来管理session。前面我们定义了自己的SessionManager AccessTokenWebSessionManager
实现了在禁用Cookie的情况下,从请求头中获取SessionID来保持会话。
org.apache.shiro.session.mgt.eis.SessionDAO
主要用来实现Session的增,删,改。前面我们实现了 ShiroRedisSessionDAO
用来把session保存到Redis中
org.apache.shiro.cache.CacheManager
是个接口,前面我们自己实现了ShiroRedisCacheManager
,用来将 AuthenticationInfo
,和AuthorizationInfo
缓存到Redis中。当然值实现 CacheManager 是不行的,还写了一个 ShiroRedisCache
实现了 org.apache.shiro.cache.Cache
接口。
也可以为SessionManager设置缓存管理器,用来缓存活跃session数据
如果现在我们的应用需要开放API接口供它方进行调用,一般我们会为它方应用分配一个以下几个参数:
access_key
身份标识符
secret_key
秘钥,一般用来对API请求进行签名,防止请求数据被劫持,篡改后重放。
app_id
应用ID。如果它方有多种不同的应用要接入,可以使用这个参数来标识不同的应用场景。这个参数不是必须的,可以根据实际情况来决定是否需要分配这个参数。
它方拿到分配的参数后,我们需要制定接入规范,这里做一些简单的HTTP报文规范:
所有HTTP报文METHOD使用 POST
数据以JSON格式放入到 HTTP报文 BODY中。(文件传输除外)
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
它方按照上面的规范组织好报文,然后发送给服务端。服务端利用Shiro框架来进行认证和验证签名。
此时客户端提交的报文首先经过我们自己定义的Filter。前面代码也自定义了一个Filter,因为是使用用户名,密码的认证方式,所以它从org.apache.shiro.web.filter.authc.FormAuthenticationFilter
继承,使用的是 UsernamePasswordToken
,这个Token是框架自带的。
现在的情况发生了变化,提交的不再是用户名密码,而是分配的X-Access-Key
和 X-Access-AppId
,还有时间戳,签名等信息。所以我们要自定义AuthenticationToken
,每个请求都需要进行认证。这个例子中就只做简单验证:X-Access-Key,X-Access-AppId 能和数据库中的信息对应上而且签名正确就认证成功。具体项目中根据安全级别可以自行设计更加复杂,安全性更高的认证算法。
身份信息保存在了数据库中,那么每次都要查询效率很低,所以需要引入缓存。
所以接下来需要做如下几件事情:
AuthenticationToken
, 直接实现org.apache.shiro.authc.AuthenticationToken
接口org.apache.shiro.web.filter.authc.AuthenticatingFilter
在Filter中完成 AuthenticationToken
的创建,执行登录。因为只有执行登录,securityManager才会通过reaml来完成认证的动作org.apache.shiro.realm.AuthorizingRealm
com.qinyeit.shirojwt.demos.shiro.matcher.CodecSupport
这里直接实现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; // 客户端应用ID
private 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;
}
// 身份信息
@Override
public Object getPrincipal() {
return getAccessKey(); // 返回身份标识
}
// 凭证
@Override
public Object getCredentials() {
return accessSign; // 返回参数签名
}
}
这个Filter 直接从 AuthenticatingFilter
继承。 在这个类中主要完成两个任务:
我们首先需要从请求头上取出:
X-Access-Key
分配的身份标识X-Access-Timestamp
请求发起的时间戳(Unix timestamp)毫秒单位X-Access-Sign
请求数据签名X-Access-AppId
应用程序ID这里有一个问题: 取出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;
@Nullable
private ServletInputStream inputStream;
public ContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
this.cachedContent = new ByteArrayOutputStream();
this.cachedForm = new HashMap<>();
cacheData();
}
@Override
public ServletInputStream getInputStream() throws IOException {
this.inputStream = new ContentCachingInputStream(cachedContent.toByteArray());
return this.inputStream;
}
@Override
public String getCharacterEncoding() {
String enc = super.getCharacterEncoding();
return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
@Override
public 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;
}
@Override
public Map<String, String[]> getParameterMap() {
if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
return cachedForm;
}
return super.getParameterMap();
}
@Override
public Enumeration<String> getParameterNames() {
if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
return Collections.enumeration(cachedForm.keySet());
}
return super.getParameterNames();
}
@Override
public 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);
}
@Override
public int read() throws IOException {
return this.inputStream.read();
}
@Override
public int readLine(byte[] b, int off, int len) throws IOException {
return this.inputStream.read(b, off, len);
}
@Override
public boolean isFinished() {
return this.inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public 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框架后可以重复读取请求体内容
@Override
public 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
*/
@Override
protected 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);
}
}
在现在的场景下,每个API的每次调用都需要进行认证,不需要进行会话保持,每次请求过来都是未认证的,所以一定会调用onAccessDenied
,所以只需要在这个方法中做登录认证操作即可
@Slf4j
public class ApiAuthenticationFilter extends AuthenticatingFilter {
...
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 父类中的executeLogin 方法会调用 onLoginSuccess或者 onLoginFailure,所以要重写这两个方法
return super.executeLogin(request, response);
}
// 认证成功直接放行
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
return true;
}
// 认证失败响应消息
@Override
protected 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);
}
}
}
}
自定义的Realm直接继承AuthorizingRealm
,声明它支持的Token类型是ApiAuthenticationToken
。
@Data
@ToString
@Builder
public class ApiAccount implements Serializable {
private String appId;
private String accessKey;
private String secretKey;
}
匹配器是用来对比数据的,即对比提交的 AauthenticationToken 中的内容和 从Realm中获取的认证信息是否匹配。
这里我们需要做两个方面的验证:
public class ApiAuthenticationCredentialsMatcher extends CodecSupport implements CredentialsMatcher {
@Override
public 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);
}
}
@Slf4j
public class ApiAuthenticationRealm extends AuthorizingRealm {
private Map<String, ApiAccount> apiAccountMap = new HashedMap();
// 模拟数据库
public ApiAuthenticationRealm() {
// 指定密码匹配器
super(new ApiAuthenticationCredentialsMatcher());
// key是 accessToken
apiAccountMap.put("db0f017ac3cacb", ApiAccount.builder()
.accessKey("db0f017ac3cacb")
.secretKey("cbce2d1aad0867f8317e7ebeb3427999")
.appId("123456")
.build());
apiAccountMap.put("f0ac034fad089", ApiAccount.builder()
.accessKey("f0ac034fad089")
.secretKey("cbce2d1aad0867f8317e7ebeb3427888")
.appId("456789")
.build());
}
// 声明它只支持 ApiAuthenticationToken
@Override
public boolean supports(AuthenticationToken token) {
return token != null && ApiAuthenticationToken.class.isAssignableFrom(token.getClass());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 如果需要某些api需要授权才能访问,这里可以返回授权信息
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1.从传过来的认证Token信息中,实际类型是ApiAuthenticationToken
// ApiAuthenticationToken 重写了 getPrincipal() 返回的就是 accessKey
String 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;
}
}
因为api调用场景下,都是是无状态该的。所以基本上不会的session进行跟踪。所以无需再配置 sessionManager和 SessionDAO
@Configuration
@Slf4j
public class ShiroConfiguration {
@Bean
public Realm realm() {
ApiAuthenticationRealm realm = new ApiAuthenticationRealm();
// 开启全局缓存
realm.setCachingEnabled(true);
// 打开认证缓存
realm.setAuthenticationCachingEnabled(true);
// 认证缓存的名字,不设置也可以,默认由
realm.setAuthenticationCacheName("shiro:authentication:cache");
return realm;
}
@Bean
public 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
*/
@Bean
protected 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;
}
@RestController
@Slf4j
@RequestMapping("/api/employees")
public class EmployeeApiController {
@PostMapping
public void create(@RequestBody Employee employee) {
log.info("创建员工: {}", employee);
}
}
这里我们使用固定提交的数据,然后计算出签名
@Slf4j
public class ApiSignTest {
@Test
public 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:987b71f4961d78b95acaa019a70ac0a6439a6a566d9bb800fa0078feba8d7864
X-Access-AppId:123456
Request Body:{"gender":"男","name":"张三"}
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 分支上.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。