红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 其它
  3. 正文

Shiro 学习笔记6:JWT

2023年9月27日 1114点热度 0人点赞 0条评论

之前我们的 Shiro 的会话状态跟踪是基于 Session 和 Cookie 的,客户端会保存服务端返回的 Shiro SessionID 到 Cookie,并且在请求时传输 Cookie 中会包含 SessionID 信息。

但如果用户禁用 Cookie 或者使用的是不支持 Cookie 的客户端,这种方式就是无效的。此时我们无法跟踪用户的会话状态,也就无法进行登录状态检查和权限检验等。

1.用请求头传递 SessionID

其实解决上述问题最简单的方式是客户端不再通过 Cookie 传递 SessionID,而是通过请求报文头传递。

1.1.获取 SessionID

服务端是通过 Shiro 的 DefaultWebSessionManager 获取请求的 SessionID 的:

public class DefaultWebSessionManager extends DefaultSessionManager implements WebSessionManager {
    // ...
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        return getReferencedSessionId(request, response);
    }
    
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        // 从 Cookie 中获取 SessionID
        String id = getSessionIdCookieValue(request, response);
        if (id != null) {
            // 如果获取到,设置 SessionID 来源为 Cookie
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
        } else {
            // 没有获取到,尝试从 url 路径参数中获取 SessionID
            id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
            if (id == null && request instanceof HttpServletRequest) {
                // 没有获取到,尝试从 url 查询参数中获取
                String name = getSessionIdName();
                HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
                String queryString = httpServletRequest.getQueryString();
                if (queryString != null && queryString.contains(name)) {
                    id = request.getParameter(name);
                }
                if (id == null && queryString != null && queryString.contains(name.toLowerCase())) {
                    // 没有获取到,尝试用小写的参数名获取
                    id = request.getParameter(name.toLowerCase());
                }
            }
            if (id != null) {
                //获取到,设置 SessionID 来源为 url
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                        ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
            }
        }
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            // 如果已经获取到 SessionID,设置 SessionId 为有效的
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        }
​
        // 设置 SessionId 是否重写
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
​
        return id;
    }
}

我们可以仿照源码编写一个自定义SessionManager,并且重写getSessionId方法:

public class HeaderWebSessionManager extends DefaultWebSessionManager {
    private static final String HEADER_SESSION_ID_SOURCE = "header";
    @Autowired
    private ShiroProperties shiroProperties;
​
    /**
     * 获取 SessionID
     *
     * @param request  HTTP 请求
     * @param response HTTP 响应
     * @return SessionID
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        // 尝试从请求报文头获取 SessionID
        String sessionId = WebUtils.toHttp(request).getHeader(getSessionIdName());
        if (sessionId != null) {
            // 成功获取到 SessionID
            // 设置 SessionID 来源
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    HEADER_SESSION_ID_SOURCE);
            // 设置 SessionID 为有效
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            // 设置 SessionID 重写
            request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
            return sessionId;
        }
        // 如果没有获取到,使用原有的从 Cookie 和 url 中获取逻辑
        return super.getSessionId(request, response);
    }
​
    /**
     * 获取 SessionID 的名称
     *
     * @return SessionID 名称
     */
    private String getSessionIdName() {
        return shiroProperties.getSessionIdHeaderName();
    }
}

用于传输 SessionID 的请求头由配置文件 shiro.ynl 定义:

my:
  shiro:
    session-id-header-name: SESSIONID

1.2.返回 SessionID

现在服务端已经可以从请求头中获取 SessionID 并利用这个 SessionID 从 Redis 获取 Session 对象。现在的问题是,如果客户端没有 SessionID 或 Session 已经失效,怎么将新的 SessionID 告知客户端。

这里我定义了一个 SpringMVC 的拦截器,用于在特定请求时在响应报文头中写入 SessionID:

@Component
public class SessionIdInterceptor implements HandlerInterceptor {
    @Autowired
    private ShiroProperties shiroProperties;
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果是 API 的请求,在返回报文头中设置 SessionID
        String requestURI = request.getRequestURI();
        if (requestURI.indexOf("/api/") == 0) {
            String sessionIdHeaderName = shiroProperties.getSessionIdHeaderName();
            Session session = SecurityUtils.getSubject().getSession();
            response.setHeader(sessionIdHeaderName, session.getId().toString());
        }
        return true;
    }
}

在这个示例项目中,假设所有的 json 格式请求都位于/api/路径下。

还需要在 SpringMVC 配置中注册这个拦截器:

@Configuration
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
    @Autowired
    private SessionIdInterceptor sessionIdInterceptor;
​
    // ...
​
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(sessionIdInterceptor);
    }
}

这样所有 /api/xxx 的请求的响应报文都会有一个响应头SESSIONID,客户端只需要获取这个响应头并存储起来。每次请求时发送,并根据需要更新本地的SESSIONID即可。

1.3.错误处理

现在还有一个遗留问题,Shiro 默认的过滤器对于错误处理是以转发到特定页面实现的,比如如果使用authc过滤器检查请求是否登录,如果没有登录,就会将请求转发到登录页面。如果使用perms过滤器检查是否有权限,如果没有权限,就会将请求转发到缺少权限的页面。

这里的登录页面和缺少权限页面,指 Shiro 过滤器中设置的相应路径。

如果是项目集成 JSP 的形式,这样做是没错的,但如果是前后端分离,客户端以 Restful 接口访问,就会导致服务端返回的响应信息中不是 json 格式的错误信息,而是对应的 Html 页面。

如果要解决这个问题,就需要使用自定义过滤器,并重写相应的错误处理逻辑。

下面是自定义的用于检查 API 请求是否登录的过滤器:

public class ApiAuthenticationFilter extends FormAuthenticationFilter {
    /**
     * 请求被拒绝时执行的处理
     * @param request HTTP 请求
     * @param response HTTP 响应
     * @return 是否放行
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 如果是 API 请求,返回 JSON 格式的报错信息
        String requestURI = WebUtils.toHttp(request).getRequestURI();
        if (requestURI.indexOf("/api/") == 0){
            ApiResult<Object> apiResult = ApiResult.fail(ApiResult.ERROR_NEED_LOGIN, "请登录");
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write(JSON.toJSONString(apiResult));
            return false;
        }
        return super.onAccessDenied(request, response);
    }
}

在重写过滤器时我们只需要关心两个方法:

public abstract class AccessControlFilter extends PathMatchingFilter {
    // 当前请求是否可以访问资源
    protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
    // 如果请求不被允许时的处理逻辑
    protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
    // ...
}

在上面这个示例中,authc过滤器对应的类FormAuthenticationFilter的isAccessAllowed方法是通过 Session 检查是否登录,这与我们的需求不矛盾,当前系统虽然改用请求头传递 SessionID,但 Session 机制本身并没有修改,所以这只需要重写onAccessDenied方法,在不允许访问(没有登录)时返回 JSON 格式的响应报文即可。

当然,需要在 Shiro 过滤器工厂中添加这个过滤器:

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 设置过滤器关联的安全管理器
    shiroFilterFactoryBean.setSecurityManager(webSecurityManager());
    Map<String, Filter> filters = new HashMap<>();
    // 添加自定义过滤器
    filters.put("orRoles", new OrRolesAuthorizationFilter());
    filters.put("onlineLimit", onlineLimitFilter());
    filters.put("apiAuth", new ApiAuthenticationFilter());
    // ...
}

现在配置文件 shiro.yml 中的/api/xxx 路径就可以使用apiAuth这个过滤器进行登录检验:

my:
  shiro:
    urls:
      - /api/user/login=anon
      - /api/user/**=apiAuth
      - /api/brand/**=apiAuth, orRoles[sys_manager,dep_manager], perms["brand:view"]

类似的,对于用于/api/xxx 请求的 Shiro 过滤器,都需要重新定义并返回 JSON 格式的错误信息,这里不再赘述。

2.JWT

JWT的全称是 JSON Web Tokens(JSON 网络令牌),就像字面意思,它是一个 JSON 格式的,用于网络传输的令牌。

官网有一个 JWT 令牌的简单示例:

image-20230927154213819

左侧是编码后的 JWT 令牌,右侧是解码后的 JWT 令牌内容。

令牌内容分为两部分:Header 和 Payload,Header 中保存加密方式等信息,Payload 中保存一般数据,比如常见的有:

  • sub(subject),签发令牌的主体

  • iat(issueAt),令牌签发时间

  • jti(JwtId),令牌id,一般使用 SessionID

  • exp(ExpiresAt),令牌的过期时间点

除了这些 JWT 预定义的内容,我们还可以在令牌中添加自定义内容。不过需要注意的是,令牌的所有内容都是以明码的方式传输的,如果要包含敏感信息,需要自行加密。JWT 令牌的用途并不是对内容加密,而是通过签名验证的方式确保内容不会在传输过程中被篡改。

和之前直接在请求头中使用 SessionID 相比,使用 JWT 包装 SessionID 的好处是可以附加信息以实现一些额外功能,比如为令牌设置单独的过期时间。但缺点是会增加系统的复杂度,实现难度较高。

下面我们看具体如何实现用 JWT 传输 SessionID。

JWT 是一种规范和标准,任何人都可以按照这套标准实现 JWT 令牌,有很多 JWT 的实现库可以使用:

  • JSON Web Token Libraries - jwt.io

这里使用 java-jwt 这个实现库。

添加依赖:

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>

2.1.JwtService

根据这个库的 API 编写一个处理 JWT 编码和解码的服务类:

@Service
public class JwtServiceImpl implements JwtService {
    private static final String CUSTOM_CLAIM_NAME = "custom-map-claim";

    @Data
    @AllArgsConstructor
    public static class JwtInfo {
        private String id;
        private Map<String, Object> claim;
    }

    @Autowired
    private JwtProperties jwtProperties;

    private Algorithm algorithm;

    @PostConstruct
    public void init() {
        // 设置 JWT 签名算法
        algorithm = Algorithm.HMAC256(jwtProperties.getBase64EncodeSecretKey());
    }

    @Override
    public String encodeToken(Map<String, Object> claim,
                              String subject,
                              @NonNull String sessionId,
                              long timeout) {
        if (claim == null) {
            claim = new HashMap<>();
        }
        if (ObjectUtils.isEmpty(subject)) {
            subject = "system";
        }
        long currentTimeMillis = System.currentTimeMillis();
        JWTCreator.Builder jwtBuilder = JWT.create()
                .withClaim(CUSTOM_CLAIM_NAME, claim) // 附加信息
                .withSubject(subject) // 设置签名人
                .withJWTId(sessionId) // 将 SessionID 设置为 JWT 的id
                .withIssuedAt(new Date(currentTimeMillis)); // 设置签名时间
        if (timeout > 0) {
            // 过期时间为当前时间 + 有效时长
            long expireTimeMillis = currentTimeMillis + timeout;
            jwtBuilder.withExpiresAt(new Date(expireTimeMillis));
        }
        // 用指定算法生成签名
        return jwtBuilder.sign(algorithm);
    }

    @Override
    public String encodeToken() {
        Session session = SecurityUtils.getSubject().getSession();
        String sessionId = session.getId().toString();
        long tokenTimeout = jwtProperties.getTokenTimeout() * 1000;
        if (tokenTimeout <= 0) {
            tokenTimeout = session.getTimeout();
        }
        return encodeToken(null, null, sessionId, tokenTimeout);
    }

    @Override
    public JwtInfo decodeToken(String jwtToken, boolean checkExpire) {
        DecodedJWT jwt;
        if (!checkExpire) {
            long newExpire = (System.currentTimeMillis() + 5 * 60 * 1000) / 1000;
            jwt = JWT.require(algorithm)
                    // 设置过期顺延时间,让 JWT token 在解析的时候不会因为过期解析失败
                    .acceptExpiresAt(newExpire)
                    .build()
                    .verify(jwtToken);
        } else {
            jwt = JWT.require(algorithm)
                    .build()
                    .verify(jwtToken);
        }
        Map<String, Object> claim = jwt.getClaim(CUSTOM_CLAIM_NAME).asMap();
        String id = jwt.getId();
        return new JwtInfo(id, claim);
    }

    @Override
    public JwtInfo decodeToken(String jwtToken) {
        return decodeToken(jwtToken, true);
    }

    @Override
    public void verifyToken(String jwtToken) {
        JWT.require(algorithm)
                .build()
                .verify(jwtToken);
    }
}

这里用algorithm = Algorithm.HMAC256(jwtProperties.getBase64EncodeSecretKey());指定签名算法,这个算法需要指定一个用于生成和验证签名的密钥。

这里通过配置文件指定:

my:
  jwt:
    secret-key: appple
    jwt-header-name: JWT-TOKEN
    token-timeout: 60 #令牌有效时长,单位秒。如果小于等于0,将使用 Session 剩余有效时长

对应的配置类:

@Configuration
@PropertySource(value = "classpath:/shiro.yml", factory = YamlPropertySourceFactory.class)
@ConfigurationProperties(prefix = "my.jwt")
@Data
public class JwtProperties {
    /**
     * 用于加密 JWT 的密钥
     */
    @NotBlank
    private String secretKey;
    /**
     * JWT 令牌使用的报文头名称
     */
    @NotBlank
    private String jwtHeaderName;
    /**
     * 令牌有效时长,单位秒
     */
    @NotNull
    private Long tokenTimeout;

    public String getBase64EncodeSecretKey(){
        return Base64.encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
    }
}

这里提供一个getBase64EncodeSecretKey方法返回 Base64 编码后的字符串,这样做是确保作为密钥使用的内容是 ASCII 字符,这样我们的原始密钥就可以是任意的 utf-8 字符串,包括中文。

我使用的这个 JWT 库解码时必须经过验证,这就出现一个问题,如果是令牌过期但 Session 没有过期的情况,按照正常处理逻辑,应该使用令牌中的 SessionID 重新签发令牌,并告知客户端。此时客户端可以使用新的令牌继续访问。但这里的问题在于此时令牌已经过期,解析令牌的时候会抛出异常,这样就无法解析令牌,也无法获取其中的 SessionID。

这里我想到的一个解决方式是通过在解析时,重新设置验证器的过期时间为当前时间5分钟后,也就是说令牌的过期时间将失效,这样就可以正常解析一个已经过期的令牌:

jwt = JWT.require(algorithm)
    // 设置过期顺延时间,让 JWT token 在解析的时候不会因为过期解析失败
    .acceptExpiresAt(newExpire)
    .build()
    .verify(jwtToken);

在方法参数中添加一个开关checkExpire来控制是否使用这段代码。

为了方便使用,这里decodeToken返回一个自定义的JwtInfo类型,包含一个可以添加自定义附加信息的claim属性和保存 SessionID 的id属性。

2.2.JwtWebSessionManager

定义一个使用 JWT 传递 SessionID 的 SessionManager:

public class JwtWebSessionManager extends DefaultWebSessionManager {
    private static final String JWT_SESSION_ID_SOURCE = "jwt";
    @Autowired
    private JwtProperties jwtProperties;
    @Autowired
    private JwtService jwtService;

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        // 尝试通过 JWT 令牌获取 SessionID
        String jwtToken = WebUtils.toHttp(request).getHeader(jwtProperties.getJwtHeaderName());
        if (!ObjectUtils.isEmpty(jwtToken)) {
            // 解析并检查 JWT 令牌是否合法
            JwtServiceImpl.JwtInfo jwtInfo;
            try {
                // 忽略令牌过期的问题,令牌过期检查由 Shiro 过滤器处理
                jwtInfo = jwtService.decodeToken(jwtToken, false);
                String sessionId = jwtInfo.getId();
                this.setRequestAttributeAfterGetSessionId(request);
                return sessionId;
            } catch (JWTVerificationException e) {
                // 令牌解析出错,视作 Session 过期
                return super.getSessionId(request, response);
            }
        }
        return super.getSessionId(request, response);
    }

    /**
     * 成功获取到 SessionID 后设置请求的相关域属性
     *
     * @param request HTTP 请求
     */
    private void setRequestAttributeAfterGetSessionId(ServletRequest request) {
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                JWT_SESSION_ID_SOURCE);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
    }
}

简单起见,在 SessionManager 中,解析令牌获取 SessionID 时不考虑令牌过期的问题,令牌过期的问题由 Shiro 过滤器处理。

2.3.JwtAuthenticationFilter

定义用 JWT 令牌访问接口的登录检查过滤器:

public class JwtAuthenticationFilter extends FormAuthenticationFilter {
    private JwtService jwtService;
    private JwtProperties jwtProperties;

    public JwtAuthenticationFilter(JwtService jwtService, JwtProperties jwtProperties, SessionManager sessionManager) {
        this.jwtService = jwtService;
        this.jwtProperties = jwtProperties;
        this.sessionManager = sessionManager;
    }

    private SessionManager sessionManager;

    @Override
    protected boolean isAccessAllowed(ServletRequest request,
                                      ServletResponse response,
                                      Object mappedValue) {
        // 检查 JWT 令牌是否可以解析
        String jwtToken = WebUtils.toHttp(request).getHeader(jwtProperties.getJwtHeaderName());
        if (ObjectUtils.isEmpty(jwtToken)) {
            // 缺少 JWT 令牌,不允许访问
            return false;
        }
        JwtServiceImpl.JwtInfo jwtInfo;
        try {
            jwtInfo = jwtService.decodeToken(jwtToken);
        } catch (TokenExpiredException tee) {
            // token 过期
            // 获取令牌中的 SessionID
            jwtInfo = jwtService.decodeToken(jwtToken, false);
            String sessionID = jwtInfo.getId();
            if (ObjectUtils.isEmpty(sessionID)) {
                // 令牌中缺少 SessionID,不允许访问
                return false;
            }
            // 获取对应的 Session
            Session session;
            try {
                session = sessionManager.getSession(new DefaultSessionKey(sessionID));
            } catch (SessionException se) {
                // Session 已经过期或无法获取,不允许访问
                return false;
            }
            // Session 没有过期,重新生成令牌
            // 令牌有效期使用配置中的设置
            long timeout = jwtProperties.getTokenTimeout() * 1000;
            if (timeout <= 0) {
                // 如果配置中的设置小于等于0,使用 Session 剩余时长作为令牌有效时长
                timeout = session.getTimeout();
            }
            jwtToken = jwtService.encodeToken(null, null, sessionID, timeout);
            WebUtils.toHttp(response).setHeader(jwtProperties.getJwtHeaderName(), jwtToken);
            return false;
        } catch (JWTVerificationException jve) {
            // 其它令牌解析错误
            // 不允许访问
            return false;
        }
        // 使用原有的身份验证逻辑检查是否是登录状态
        return super.isAccessAllowed(request, response, mappedValue);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 不允许访问时,让用户去登录
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json; charset=utf-8");
        ApiResult<Object> apiResult = ApiResult.fail(ApiResult.ERROR_NEED_LOGIN, "需要登录");
        response.getWriter().write(JSON.toJSONString(apiResult));
        return false;
    }
}

这里比较棘手的是令牌过期问题,如果令牌和 Session 都过期了,那用户只能重新登录。但更常见的应该是令牌过期但 Session 没有过期。毕竟令牌不能像 Session 一样,每次客户端请求时都刷新过期时间。我这里是添加了一个处理逻辑,如果令牌过期但 Session 没有过期,就重新使用这个 SessionID 生成新的令牌,并在响应报文头中返回新的令牌。同时,因为访问被拒绝,onAccessDenied方法中会返回需要登录等相关信息。

此时客户端可以检查返回的 Header 中是否有新的令牌,如果有,可以替换本地令牌并尝试重新访问,如果没有,就只能让用户重新登录。

这里的处理并不完美,更好的处理方式可能是在令牌过期但 Session 没过期时生成新的令牌,并且用新的令牌创建一个 Request 对象,将请求转发到这个 Request 对象,这样做的好处是,虽然本次请求客户端传递的令牌已经过期,但依然可以正常返回数据,以及新的令牌,客户端只需要更新令牌即可,不需要重新发送请求。当然,这只是一个想法,这里我并没有实现。

2.4.配置类

最后,在配置类中添加过滤器和 WebSessionManager:

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(JwtService jwtService, JwtProperties jwtProperties) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 设置过滤器关联的安全管理器
    shiroFilterFactoryBean.setSecurityManager(webSecurityManager());
    Map<String, Filter> filters = new HashMap<>();
    // 添加自定义过滤器
    filters.put("orRoles", new OrRolesAuthorizationFilter());
    filters.put("onlineLimit", new OnlineLimitFilter(objectRedisTemplate, webSessionManager(), redisSessionDao(), shiroProperties));
    filters.put("apiAuth", new ApiAuthenticationFilter());
    filters.put("jwtAuthc", new JwtAuthenticationFilter(jwtService, jwtProperties, webSessionManager()));
    shiroFilterFactoryBean.setFilters(filters);
   	// ...
}

注意,Shiro 过滤器不能定义为 Spring Bean,否则会被 Shiro 认为是全局过滤器,对于任何路径都会使用该过滤器(无视过滤器链的匹配规则),这样会导致一些莫名其妙的 Bug。

使用 JwtWebSessionManager 作为 SessionManager:

@Bean
public DefaultWebSessionManager webSessionManager() {
    DefaultWebSessionManager webSessionManager = new JwtWebSessionManager();
    // ...
}

在配置文件中的过滤器链上可以使用新的过滤器:

my:
  shiro:
    urls:
      - /api/user/login=anon
      - /api/user/**=jwtAuthc
      - /api/brand/**=jwtAuthc, orRoles[sys_manager,dep_manager], perms["brand:view"]

现在通过/api/user/login登录成功后,可以从响应头获取 JWT 令牌,然后每次请求都需要在请求头带上 JWT 令牌进行访问。如果令牌过期,服务端会返回需要登录的错误信息,此时客户端可以检查响应头中是否携带新的令牌,如果有,就更新本地令牌并重新访问,如果没有,就让用户进行登录以获取新令牌。

最后需要说明的是,令牌的有效时长必须设置为小于 Session 的有效时长。否则会出现令牌没有过期,但 Session 已经过期的情况,而用户的登录信息是保存在 Session 中的,这样会导致 Bug。当然,最简单的方式是将 Session 和令牌有效时长都设置一个比较长的时间,但这样会导致系统安全性降低,以及 Redis 上存在一些长期没有使用的 Session 占用空间的问题。

The End,谢谢阅读。

本文的完整示例可以从这里获取。

3.参考资料

  • java-jwt/EXAMPLES.md at master · auth0/java-jwt (github.com)

  • JSON Web Tokens - jwt.io

  • 黑马程序员Java高级工程师技术栈-由浅入深掌握Shiro权限框架

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: jwt shiro
最后更新:2023年9月27日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号