springboot3+jdk17+shiro+jwt+redis

springboot3+jdk17整合jwt+shiro+redis实现登录认证

注意,jdk17的规范是Jakarta EE,虽然最新版本shiro适配springboot3,但是部分包要单独适配

  • 首先讲一下整体流程
  1. 用户首次登录时,会发送一个包含用户名和密码的请求到服务器。这个请求通常是一个POST请求,发送到一个特定的登录URL,例如 /user/login。
  2. 服务器接收到请求后,会验证用户名和密码。如果验证成功,服务器会生成一个JWT,并将其发送回用户。这个JWT包含了用户的一些信息,例如用户ID,以及一个签名。签名是用服务器的私钥生成的,可以用来验证JWT的真实性和完整性。
  3. 用户收到JWT后,会将其存储在本地,例如在浏览器的localStorage中。然后,每次发送请求到服务器时,都会在请求的Header中携带这个JWT,通常是在 Authorization 字段中。
  4. 服务器接收到请求后,会首先检查请求的Header中是否包含JWT。如果没有包含,那么服务器就会拒绝这个请求,因为它无法验证请求的来源。如果包含了JWT,那么服务器就会验证这个JWT的签名。如果签名验证失败,那么服务器也会拒绝这个请求,因为这意味着JWT可能被篡改。如果签名验证成功,那么服务器就会从JWT中提取出用户信息,然后处理这个请求。
  5. 在处理请求时,服务器可能还需要进行授权检查,例如检查用户是否有权限访问某个资源。这个检查通常是通过查询数据库来完成的。
  6. 如果用户长时间没有活动,那么服务器会认为用户已经退出登录,此时的JWT就会过期。用户再次发送请求时,服务器就会发现JWT已经过期,然后拒绝这个请求。用户需要重新登录,以获取新的JWT。
  7. 在每次用户请求时,服务器会检查他们的JWT令牌是否即将过期。如果令牌即将过期,并且用户在Redis中被标记为活跃,那么服务器会自动为他们续签令牌。这个过程在JwtFilter类的onAccessDenied方法中实现。
  8. 如果用户在过去2小时内有任何活动,那么他们会被标记为活跃用户。这是通过在每次用户请求时调用markUserActive方法来实现的。这个方法会在Redis中设置一个键,键的格式是 “active_users:” 加上当前的日期和时间,并将这个键对应的位(由用户ID指定)设置为 true。这个键的过期时间为2小时。
  9. 如果用户在过去2小时内没有任何活动,那么他们的活跃状态就会被移除,即Redis中对应的键会过期并被删除。如果这时用户发送的请求中的JWT令牌即将过期,那么服务器不会为他们续签令牌,而是要求他们重新登录。
  • 整个认证流程登录和登录之后每一次的认证是不一样的,

    img

  • 登录后由工具类jwtUtil生成一个token,返回给前端用于之后每次请求

引入依赖

  • pom.xml,注意jwt相关包版本不能太低

    <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>3.5.5</version>
            </dependency>
    <!-- shiro -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <classifier>jakarta</classifier>
                <version>1.12.0</version>
                <!-- 排除仍使用了javax.servlet的依赖 -->
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.shiro</groupId>
                        <artifactId>shiro-core</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.apache.shiro</groupId>
                        <artifactId>shiro-web</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <!-- 引入适配jakarta的依赖包 -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
                <classifier>jakarta</classifier>
                <version>1.12.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-web</artifactId>
                <classifier>jakarta</classifier>
                <version>1.12.0</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.shiro</groupId>
                        <artifactId>shiro-core</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    <!--        jwt创建、解析、验证 JWT 的功能,并且支持对 JWT 进行签名和加密 -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.7.0</version>
            </dependency>
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.4.0</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!--       jedis 是 Redis 官方推荐的 Java 客户端,提供了比较全面的 Redis 命令的支持。-->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
            </dependency>
    	<dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.14.2</version>
            </dependency>
    

ShiroConfig

  • config目录下创建一个Shiro配置类

    package com.example.englishhub.config;
    
    /**
     * @Author: hahaha
     * @Date: 2024/4/11 16:29
     */
    
    import com.example.englishhub.security.JwtFilter;
    import com.example.englishhub.security.JwtRealm;
    import jakarta.servlet.Filter;
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    
    /**
     * shiro的三个重要配置
     * 1、Realm
     * 2、DefaultWebSecurityManager
     * 3、ShiroFilterFactoryBean
     */
    
    /**
     1.用户请求,不携带token,就在JwtFilter处进行错误处理返回,让它去登陆
     2.用户请求,携带token,就到JwtFilter中获取jwt,使用JwtRealm进行认证,若token过期则返回401状态码
     3.在JwtRealm中进行认证判断这个token是否有效,
     */
    
    @Configuration
    public class ShiroConfig {
    
        @Bean("securityManager")
        public DefaultWebSecurityManager securityManager(@Qualifier("jwtRealm") JwtRealm jwtRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置自定义Realm
            securityManager.setRealm(jwtRealm);
            // 关闭shiroDao功能,关闭session
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            // 不需要将ShiroSession中的东西存到任何地方包括Http Session中)
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            // securityManager.setSubjectFactory(subjectFactory());
            return securityManager;
        }
    
        @Bean("shiroFilterFactoryBean")
        public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
    
            /**
             *  注册jwt过滤器,除/login,/register外都先经过jwtFilter
             *
             *   先经过过滤器,如果检测到请求头存在 token,则用 token 去 login,接着走 Realm 去验证
             */
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
            HashMap<String, Filter> filterMap = new HashMap<>();
            filterMap.put("jwt", new JwtFilter());
            shiroFilter.setFilters(filterMap);
            LinkedHashMap<String, String> map = new LinkedHashMap<>();
    
            map.put("/user/login", "anon");
            map.put("/user/register", "anon");
            // 所有请求通过我们自己的JWT Filter
            map.put("/**", "jwt");
            shiroFilter.setFilterChainDefinitionMap(map);
            return shiroFilter;
        }
    
        /**
         * 解决@RequiresAuthentication注解不生效的配置
         */
        @Bean("lifecycleBeanPostProcessor")
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        @Bean
        @DependsOn({"lifecycleBeanPostProcessor"})
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        /**
         * 为Spring-Bean开启对Shiro注解的支持
         */
        @Bean("authorizationAttributeSourceAdvisor")
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    }
    
  • 在启动后,首先加载 securityManager,关闭了原有的session(因为使用了jwtToken),将自定义的Realm注入(在其中完成对jwtToken的验证)

  • 之后添加我们自定义的jwt过滤器,起到拦截请求的作用(就相当于拦截器),排除特殊接口如登录注册以及接口文档

JwtToken

package com.example.englishhub.security;

/**
 * @Author: hahaha
 * @Date: 2024/4/11 17:26
 */

import org.apache.shiro.authc.AuthenticationToken;

/**
 * 继承AuthenticationToken,跟JwtRealmh中的doGetAuthenticationInfo的参数类型保持一致
 */
public class JwtToken implements AuthenticationToken {
    private String token;

    public JwtToken(String token){
        this.token = token;
    }


    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

JwtFilter

  • 我在项目目录下新建了一个 security存放安全认证相关的类

    img

    package com.example.englishhub.security;
    
    import com.example.englishhub.exception.JwtValidationException;
    import com.example.englishhub.utils.Result;
    import com.example.englishhub.utils.ResultType;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import jakarta.servlet.ServletOutputStream;
    import jakarta.servlet.ServletRequest;
    import jakarta.servlet.ServletResponse;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.web.filter.AccessControlFilter;
    import org.apache.shiro.web.util.WebUtils;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    
    import javax.security.sasl.AuthenticationException;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    
    /**
     * @Description: JwtFilter
     * shiro的过滤器,用于拦截请求,
     * @Author: hahaha
     * @Date: 2024/4/11 17:26
     */
    
    @Slf4j
    @Component
    public class JwtFilter extends AccessControlFilter {
    
        /**
         * isAccessAllowed()判断是否携带了有效的JwtToken
         * onAccessDenied()是没有携带JwtToken的时候进行账号密码登录
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
            /**
             * 1. 返回true,shiro就直接允许访问url
             * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
             *  这里先让它始终返回false来使用onAccessDenied()方法
             *  如果带有 token,则对 token 进行检查,否则直接通过
             *
             */
            try {
                onAccessDenied(servletRequest, servletResponse);
    //            return true;
            } catch (Exception e) {
                log.error("isAccessAllowed error:", e);
                responseError(servletResponse, ResultType.UNAUTHORIZED.getCode(), "Authentication failed: " + e.getMessage());
    //            return false;
            }
            return true;
        }
    
    
        /**
         * @param servletRequest
         * @param servletResponse
         * @throws Exception
         * @return 返回结果为true表明登录通过
         */
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            /**
             *  跟前端约定将jwtToken放在请求的Header的token中,token:token
             */
            log.info("onAccessDenied方法被调用");
    //        HttpServletResponse response = (HttpServletResponse) servletResponse;
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String token = request.getHeader("token");
            //如果token为空的话,返回true,交给控制层进行判断;也会达到没有权限的作用
            if (token == null) {
                responseError(servletResponse, ResultType.UNAUTHORIZED.getCode(), "No token provided");
                return false;
            }
            JwtToken jwtToken = new JwtToken(token);
            try {
                //进行登录处理,委托realm进行登录认证,调用JwtRealm进行的认证,doGetAuthenticationInfo
                getSubject(servletRequest, servletResponse).login(jwtToken);
                return true;
            } catch (JwtValidationException e) {
                // Handle specific custom exceptions
                responseError(servletResponse, e.getStatusCode(), e.getMessage());
                return false;
            }
    //        catch (AuthenticationException e) {
    //            responseError(servletResponse, ResultType.UNAUTHORIZED.getCode(), "Authentication failed: " + e.getMessage());
    //            return false;
    //        }
        }
    
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            System.out.println("进入拦截器");
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
    
            // 设置CORS头部
            resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
            resp.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            resp.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,token");
            resp.setHeader("Access-Control-Allow-Credentials", "true");
    
            if ("OPTIONS".equals(req.getMethod())) {
                resp.setStatus(HttpServletResponse.SC_OK);
                return false; // 阻止后续的过滤器链执行
            }
    
            return super.preHandle(request, response);
        }
    
        //失败要执行的方法
        private void responseError(ServletResponse response, String statusCode, String message) throws IOException {
    
            HttpServletResponse resp = WebUtils.toHttp(response);
            resp.setStatus(HttpStatus.UNAUTHORIZED.value());
            resp.setCharacterEncoding("UTF-8");
            resp.setContentType("application/json; charset=utf-8");
            ObjectMapper mapper = new ObjectMapper();
    
            try (ServletOutputStream out = resp.getOutputStream()) {
                Result<String> result = new Result<>();
                result.setStatusCode(statusCode);
                result.setMessage(message);
                String json = mapper.writeValueAsString(result);
                out.write(json.getBytes(StandardCharsets.UTF_8));
            } catch (IOException e) {
                throw new AuthenticationException("Response writing failed with IOException: " + e.getMessage());
            }
        }
    }
    
  • 在登录之后的每次请求都会携带 token,这可以通过前端配置一个请求拦截器实现,在后端从header头中取出该token,通过JwtToken类封装成shrio接受的token,委托 JwtRealm认证,传入JwtToken

JwtRealm

package com.example.englishhub.security;

import com.example.englishhub.exception.JwtValidationException;
import com.example.englishhub.service.UserService;
import com.example.englishhub.utils.JwtUtil;
import com.example.englishhub.utils.ResultType;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Description: JwtRealm
 * shiro的Realm,用于处理JwtToken的认证和授权
 * @Author: hahaha
 * @Date: 2024/4/11 17:26
 */

@Slf4j
@Component
public class JwtRealm extends AuthorizingRealm {

    @Resource
    private JwtUtil jwtUtil;

    @Autowired
    private UserService userService;

    /**
     * 多重写一个support
     * 标识这个Realm是专门用来验证JwtToken
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String jwt = (String) token.getCredentials();
        // 获取jwt中关于用户id,解码过程中如果token过期或者被篡改会抛出异常
//        String id = null;
        log.info("处理jwt认证", jwt);
        try {
            // Validate the token
            String id = jwtUtil.validateToken(jwt);
            log.info("jwt认证成功,用户id:", id);
            // 标记用户为活跃状态
            userService.markUserActive(Integer.parseInt(id));
            return new SimpleAuthenticationInfo(jwt, jwt, getName());
        }
        catch (ExpiredJwtException e) {
            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "Token已过期,请重新登录");
        } catch (JwtException e) {
            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "无效的Token");
        }
    }

    /**
     * 授权时调用
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return new SimpleAuthorizationInfo();
    }
}
  • 调用工具类 JwtUtil,解析token获取其中的用户id,并对token过期等情况进行处理,抛出异常被捕获

  • 然后在filter层走失败的方法,返回结果给前端

JwtUtil

package com.example.englishhub.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.context.annotation.Configuration;

import java.util.Base64;
import java.util.Date;
import java.util.HashMap;

/**
 * jwt工具类
 *
 * @author hahaha
 */
@Configuration
public class JwtUtil {
    // 30 秒
    private static long EXPIRATION_TIME = 1000 * 30;
    // 1 hour
//    private static long EXPIRATION_TIME = 3600000 * 1;
    // 一天
//    private static long EXPIRATION_TIME = 3600000 * 1;
//private static long EXPIRATION_TIME = 10000 * 10;
//    private static String SECRET = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjY34DFDSSSd";// 秘钥
    // 使用Base64编码的密钥
    private static String SECRET = Base64.getEncoder().encodeToString("MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjY34DFDSSSd".getBytes());

    private static final String USER_ID = "id";


    /**
     * 生成jwtToken
     *
     * @param id
     * @return
     */
    public static String generateToken(String id) {
        HashMap<String, Object> map = new HashMap<>();
        // you can put any data in the map
        map.put(USER_ID, id);
        // 1. setClaims(map):将map中的数据存储到Claims中
        // 2. setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)):设置过期时间
        // 3. signWith(SignatureAlgorithm.HS512, SECRET):设置加密算法和密钥
        // 4. compact():生成token
        String token = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setClaims(map)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        return token;
    }

    /**
     * 校验jwtToken
     *
     * @param token
     * @return
     */
    // 优化验证逻辑
    public static String validateToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        return claims.get(USER_ID, String.class);
//        // 使用自定义异常
//        if (StringUtils.isBlank(token)) {
//            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "Token为空");
//        }
//        try {
//            Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
//            return claims.get(USER_ID, String.class);
//        } catch (ExpiredJwtException e) {
//            throw new JwtValidationException(ResultType.AGAIN_LOGIN.getCode(), "Token已过期,请重新登录");
//        } catch (JwtException e) {
//            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "无效的Token");
//        }
    }
  
    public static void main(String[] args) {
        String id = "hahaha15";

        String token = generateToken(id);
        System.out.println(token);

        //token = "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6IjY4NzZhYjFmYjk0MmZkNGYyN2Zm";
        id = validateToken(token);
        System.out.println(id);
//        HashMap<String, Object> map = new HashMap<>();
//        // you can put any data in the map
//        map.put("name", id);
//        token = Jwts.builder().setClaims(map).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
//                .signWith(SignatureAlgorithm.
上一篇:gpt-4o api申请开发部署应用:一篇全面的指南


下一篇:Agent能力的训练与评估