SpringBoot之Shiro整合JWT

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: SpringBoot之Shiro整合JWT

前言

大家好,一直以来我都本着用最通俗的话理解核心的知识点, 我认为所有的难点都离不开 基础知识 的铺垫。目前正在出一个SpringBoot长期系列教程,从入门到进阶, 篇幅会较多~


适合人群

  • 学完Java基础
  • 想通过Java快速构建web应用程序
  • 想学习或了解SpringBoot
  • SpringBoot进阶学习

大佬可以绕过 ~


背景

如果你是一路看过来的,很高兴你能够耐心看完。之前带大家学了Springboot基础部分,对基本的使用有了初步的认识, 接下来的几期内容将会带大家进阶使用,会先讲解基础中间件的使用和一些场景的应用,或许这些技术你听说过,没看过也没关系,我会带大家一步一步的入门,耐心看完你一定会有收获~


情景回顾

上期带大家学习了Shiro中如何进行缓存以及它的Session会话管理,还带大家实现了一个在线用户管理的例子,本期将带大家学习Shiro中如何整合JWT以及跨域处理,本篇是这个系列的终极篇, 同样的,我们集成到Springboot中。


啥是JWT

在实现之前,我们一起来了解一下啥是jwt。首先它的全称是JSON Web Token, 它是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名


使用场景

  • 授权场景 : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • 信息交换 : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。


工作原理

在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。


无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema


服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要。


如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie。


环境搭建

首先我们要引入相关依赖,在pom.xml中添加如下:

<!-- jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.1</version>
</dependency>
复制代码


添加配置 ShiroConfig

为了不混淆之前的配置,我们新建一个配置,放到authentication包下, 这里直接贴完整例子,没啥好说的,之前都讲过

@Configuration
public class ShiroConfig {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private Integer redisPort;
    private static final Integer expireAt = 1800;
    private static final Integer timeout = 3000;
    @Value("${spring.redis.password}")
    private String redisPassword;
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        String prefix = "/api";
        shiroFilterFactoryBean.setLoginUrl(prefix + "/notLogin");
        shiroFilterFactoryBean.setUnauthorizedUrl(prefix + "/notRole");
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filters);
        // 所有请求都要经过 jwt过滤器
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        logger.warn("Shiro jwt 拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(customRealm());
        // 设置缓存
        securityManager.setCacheManager(cacheManager());
        // 设置会话
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    /**
     * 自定义身份认证 realm;
     * <p>
     * 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
     * 否则会影响 CustomRealm类 中其他类的依赖注入
     */
    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }
    /**
     * 加入redis缓存,避免重复从数据库获取数据
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisHost);
        redisManager.setPort(redisPort);
        redisManager.setPassword(redisPassword);
        redisManager.setExpire(expireAt);
        redisManager.setTimeout(timeout);
        return redisManager;
    }
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
    /**
     * session 会话管理
     */
    @Bean
    public RedisSessionDAO sessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }
    @Bean
    public SimpleCookie sessionIdCookie(){
        SimpleCookie cookie = new SimpleCookie("X-Token");
        cookie.setMaxAge(-1);
        cookie.setPath("/");
        cookie.setHttpOnly(false);
        return cookie;
    }
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionIdCookie(sessionIdCookie());
        sessionManager.setSessionIdCookieEnabled(true);
        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        listeners.add(new ShiroSessionListener());
        sessionManager.setSessionListeners(listeners);
        sessionManager.setSessionDAO(sessionDAO());
        return sessionManager;
    }
}
复制代码


实现JwtFilter过滤器

实际上核心是实现JwtFilter这个过滤器, 下面贴个完整案例给大家参考一下:

public class JwtFilter extends BasicHttpAuthenticationFilter {
    private Logger log = LoggerFactory.getLogger(this.getClass());
    private static final String TOKEN = "Authorization";
    private AntPathMatcher pathMatcher = new AntPathMatcher();
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // 这里大家可以处理白名单逻辑,这里就不实现了 比如 /login 我们需要放行
//        if (match) {
//            return true;
//        }
        if (isLoginAttempt(request, response)) {
            return executeLogin(request, response);
        }
        log.error("未传token {}", httpServletRequest.getRequestURI());
        return false;
    }
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(TOKEN);
        return token != null;
    }
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(TOKEN);
        JwtToken jwtToken = new JwtToken(token);
        try {
            getSubject(request, response).login(jwtToken);
            return true;
        } catch (Exception e) {
            request.setAttribute("fail", e.getMessage());
            log.error("executeLogin {}", e.getMessage());
            return false;
        }
    }
    /**
     * 对跨域提供支持(注意生产)
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}
复制代码

这一步可以处理白名单,处理跨域~


实现 JwtToken

下面我们开始实现jwt的逻辑, 首先定义一个实体

public class JwtToken implements AuthenticationToken {
    private static final long serialVersionUID = 1282057025599826155L;
    private String token;
    private String expireAt;
    public JwtToken(String token) {
        this.token = token;
    }
    public JwtToken(String token, String expireAt) {
        this.token = token;
        this.expireAt = expireAt;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
    public String getToken() {
        return token;
    }
    public void setToken(String token) {
        this.token = token;
    }
    public String getExpireAt() {
        return expireAt;
    }
    public void setExpireAt(String expireAt) {
        this.expireAt = expireAt;
    }
}
复制代码


封装jwt工具类

这里直接给大家封装好,直接用,一般写业务的时候,这种常用的工具最好封装起来,也方便别人使用

public class JwtUtil {
    private static Logger log = LoggerFactory.getLogger(JwtUtil.class);
    // 设置过期时间
    private static final long EXPIRE_TIME = 1000 * 72 * 36;
    // 设置秘钥 (这里推荐大家可以写入 yml配置文件里)
    private static final String Secret = "28ca017de15a57e206f0";
    /**
     * 校验 token是否正确
     *
     * @param token  密钥
     * @return 是否正确
     */
    public static boolean verify(String token, User user) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(Secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("userId", user.getId())
                    .withClaim("roleId", user.getRole())
                    .build();
            verifier.verify(token);
            log.info("token is valid");
            return true;
        } catch (Exception e) {
            log.error("token is invalid{}", e.getMessage());
            return false;
        }
    }
    /**
     * 从 token中获取用户id
     *
     * @return token中包含的用户id
     */
    public static String getUserId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("userId").asString();
        } catch (JWTDecodeException e) {
            log.error("error:{}", e.getMessage());
            return null;
        }
    }
    /**
     * 从 token中获取用户roleId
     *
     * @return token中包含的用户id
     */
    public static Integer getRoleId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("roleId").asInt();
        } catch (JWTDecodeException e) {
            log.error("error:{}", e.getMessage());
            return null;
        }
    }
    /**
     * 生成 token
     *
     * @param user
     * @return token
     */
    public static String sign(User user) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            // 这里可以加入秘钥
            Algorithm algorithm = Algorithm.HMAC256(Secret);
            // 这里可以存放于jwt中的内容信息,最后可以通过解密拿到
            return JWT.create()
                    .withClaim("userId", user.getId())
                    .withClaim("roleId", user.getRole())
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            log.error("error:{}", e);
            return null;
        }
    }
}
复制代码

相关注释已经写在上面了~


实现验证逻辑

我们知道Shiro的验证逻辑部分在于我们自己实现的CustomRealm, 所以下面我们来实现一下它:

public class CustomRealm extends AuthorizingRealm {
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    /**
     * 授权模块,获取用户角色和权限
     * @param token token
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection token) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        String userId = JwtUtil.getUserId(token.toString());
        if(userId == null) {
            return simpleAuthorizationInfo;
        }
        String userRole = UserMock.getRoleById(userId);
        Set<String> role = new HashSet<>();
        role.add(userRole);
        simpleAuthorizationInfo.setRoles(role);
        simpleAuthorizationInfo.setStringPermissions(role);
        return simpleAuthorizationInfo;
    }
    /**
     * 用户认证
     *
     * @param authenticationToken 身份认证 token
     * @return AuthenticationInfo 身份认证信息
     * @throws AuthenticationException 认证相关异常
     */
    @Override
    protected SimpleAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        String userId = JwtUtil.getUserId(token);
        if (StringUtils.isBlank(userId)) {
            throw new AuthenticationException("验证失败");
        }
        String userRole = UserMock.getRoleById(userId);
        User userBean = new User();
        userBean.setUserId(userId);
        userBean.setRole(userRole);
        if (!JwtUtil.verify(token, userBean)) {
            throw new AuthenticationException("token失效");
        }
        return new SimpleAuthenticationInfo(token, token, "shiroJwtRealm");
    }
}
复制代码

这部分逻辑大家可以根据具体功能自有发挥,方法就那么几个~


如何去验证 & 注意事项

这里就不带大家一一去测试了,留给大家自己去思考。流程给大家简要说一下,首先是用户通过login,验证成功后,你需要调用jwtUtil去签发token给前端,前端拿到后,放入请求头中,这样每次请求都会去携带这个token,服务端会从请求头中获取这个token,然后进行验证,验证通过后继续执行,失败就会返回失败信息,这个之前教过大家如何去捕获。这里需要强调的是token的刷新机制,因为如果让用户频繁的跳登录这样体验是很不友好的,所以过期时间设置刷新机制这个大家要根据自身业务来定,如何去刷新,这个需要跟前端同学协商好~


结束语

本期内容就到这里结束了,总结一下,本节主要讲了Shiro如何进行整合jwt,大家可以举一反三,做一些小功能尝试尝试


下期预告

其实学到这里,我们去做一些业务基本上没啥太大问题了,有的时候我们写完代码需要我们自己去打包并部署到服务器,一般情况下有专门的同学会做这件事。但是,这里还是教大家一下如何去部署服务,这个技能对于服务端的同学还是要必会的,说不定哪天就是你发的呢,下期就带大家学习如何线上部署,将涉及到nginx部署教程,以及jar包的部署与服务启动, 还将会带大家如何搭建测试环境和线上环境。欢迎加群一起学习交流 ~

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
30天前
|
JSON 安全 算法
|
1月前
|
安全 Java 数据库
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
这篇文章是关于Apache Shiro权限管理框架的详细学习指南,涵盖了Shiro的基本概念、认证与授权流程,并通过Spring Boot测试模块演示了Shiro在单应用环境下的使用,包括与IniRealm、JdbcRealm的集成以及自定义Realm的实现。
45 3
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
|
30天前
|
存储 安全 Java
|
1月前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
30 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
20天前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
33 0
|
3月前
|
安全 Java 数据库
Spring Boot中集成 Shiro
本节主要介绍了 Shiro 安全框架与 Spring Boot 的整合。先介绍了 Shiro 的三大核心组件已经它们的作用;然后介绍了 Shiro 的身份认证、角色认证和权限认证;最后结合代码,详细介绍了 Spring Boot 中是如何整合 Shiro 的,并设计了一套测试流程,逐步分析 Shiro 的工作流程和原理,让读者更直观地体会出 Shiro 的整套工作流程。Shiro 使用的很广泛,希望读者将其掌握,并能运用到实际项目中。
|
Java Spring
spring boot 集成websocket与shiro的坑
spring boot 集成websocket与shiro的坑
391 0
|
6月前
|
前端开发 Java Spring
使用Spring Boot集成Shiro时出现了无法注入Service的问题
使用Spring Boot集成Shiro时出现了无法注入Service的问题
109 0
|
存储 安全 Java
SpringBoot集成shiro认证,实现Shiro认证的登录操作
SpringBoot集成shiro认证,实现Shiro认证的登录操作
234 0
SpringBoot集成shiro认证,实现Shiro认证的登录操作
|
缓存 安全 Java
springboot简单集成shiro权限管理
springboot简单集成shiro权限管理
163 0
springboot简单集成shiro权限管理

热门文章

最新文章