pom.xml

        <!-- shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.9.1</version>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!-- jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>


ShiroConfig

import com.lz.jwt.JwtDefaultSubjectFactory;
import com.lz.jwt.JwtFilter;
import com.lz.jwt.JwtRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


/**
 * @Author zf
 * @ClassName ShiroConfig.java
 * @ProjectName demo
 */
//springBoot整合jwt实现认证有三个不一样的地方,对应下面abc
@Configuration
public class ShiroConfig {
    /*
     * a. 告诉shiro不要使用默认的DefaultSubject创建对象,因为不能创建Session
     * */
    @Bean
    public SubjectFactory subjectFactory() {
        return new JwtDefaultSubjectFactory();
    }

    @Bean
    public Realm realm() {
        return new JwtRealm();
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        /*
         * b
         * */
        // 关闭 ShiroDAO 功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        //禁止Subject的getSession方法
        securityManager.setSubjectFactory(subjectFactory());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager());
        shiroFilter.setLoginUrl("/unauthenticated");
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        /*
         * c. 添加jwt过滤器,并在下面注册
         * 也就是将jwtFilter注册到shiro的Filter中
         * 指定除了login和logout之外的请求都先经过jwtFilter
         * */
        Map<String, Filter> filterMap = new HashMap<>();
        //这个地方其实另外两个filter可以不设置,默认就是
        filterMap.put("anon", new AnonymousFilter());
        filterMap.put("jwt", new JwtFilter());
        filterMap.put("logout", new LogoutFilter());
        shiroFilter.setFilters(filterMap);

        // 拦截器
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/account/**", "anon");
        filterRuleMap.put("/logout", "logout");
        filterRuleMap.put("/qklCarbonMark/**","jwt");
        filterRuleMap.put("/merchant/**","jwt");
        filterRuleMap.put("/user/**","jwt");

        //白名单
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilter;
    }
}


JwtDefaultSubjectFactory

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

/**
 * @Author zf
 * @ClassName JwtDefaultSubjectFactory.java
 * @ProjectName demo
 */
public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        // 不创建 session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}


JwtFilter

import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author zf
 * @ClassName JwtFilter.java
 * @ProjectName demo
 */
/*
 * 自定义一个Filter,用来拦截所有的请求判断是否携带Token
 * isAccessAllowed()判断是否携带了有效的JwtToken
 * onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问
 * */
@Slf4j
public class JwtFilter extends AccessControlFilter {
    /*
     * 1. 返回true,shiro就直接允许访问url
     * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        log.warn("isAccessAllowed 方法被调用");
        //这里先让它始终返回false来使用onAccessDenied()方法
        return false;
    }

    /**
     * 返回结果为true表明登录通过
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.warn("onAccessDenied 方法被调用");
        //这个地方和前端约定,要求前端将jwtToken放在请求的Header部分

        //所以以后发起请求的时候就需要在Header中放一个Authorization,值就是对应的Token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        log.info("请求的 Header 中藏有 jwtToken : {}", jwt);
        JwtToken jwtToken = new JwtToken(jwt);
        /*
         * 下面就是固定写法
         * */
        try {
            // 委托 realm 进行登录认证
            //所以这个地方最终还是调用JwtRealm进行的认证
            getSubject(servletRequest, servletResponse).login(jwtToken);
            //也就是subject.login(token)
        } catch (Exception e) {
            System.out.println(e.getMessage());
            String message = null;
//            if (ObjectUtil.isNotNull(e.getMessage())){
//                message = e.getCause().getMessage();
//            }
            onLoginFail(servletResponse, message);
            //调用下面的方法向客户端返回错误信息
            return false;
        }

        return true;
        //执行方法中没有抛出异常就表示登录成功
    }

    //登录失败时默认返回 401 状态码
    private void onLoginFail(ServletResponse response, String message) throws IOException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        if (ObjectUtil.isNotNull(message)){
            httpResponse.getWriter().write(message);
        } else {
            httpResponse.getWriter().write("login error!");
        }
    }
}


JwtRealm

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Value;

/**
 * @Author zf
 * @ClassName JwtRealm.java
 * @ProjectName demo
 */
@Slf4j
public class JwtRealm extends AuthorizingRealm {

    @Value("${user.admin.username}")
    private String userName;

    /*
     * 多重写一个support
     * 标识这个Realm是专门用来验证JwtToken
     * 不负责验证其他的token(UsernamePasswordToken)
     * */
    @Override
    public boolean supports(AuthenticationToken token) {
        //这个token就是从过滤器中传入的jwtToken
        return token instanceof JwtToken;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //认证
    //这个token就是从过滤器中传入的jwtToken
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String jwt = (String) token.getPrincipal();
        if (jwt == null) {
            throw new NullPointerException("jwtToken Cannot be empty!");
        }
        //判断
        JwtUtil jwtUtil = new JwtUtil();
        if (!jwtUtil.isVerify(jwt)) {
            throw new UnknownAccountException();
        }
        //下面是验证这个user是否是真实存在的
        String username = (String) jwtUtil.decode(jwt).get("username");
        //判断数据库中username是否存在
        if (userName.equals(username)){
            log.info("在使用token登录"+username);
            return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
            //这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。还需要一个该Realm的类名
        }else {
            throw new RuntimeException("User does not exist!");
        }
    }
}


JwtToken

import org.apache.shiro.authc.AuthenticationToken;

/**
 * @Author zf
 * @ClassName JwtToken.java
 * @ProjectName demo
 */
//这个就类似UsernamePasswordToken
public class JwtToken implements AuthenticationToken {

    private String jwt;

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

    @Override//类似是用户名
    public Object getPrincipal() {
        return jwt;
    }

    @Override//类似密码
    public Object getCredentials() {
        return jwt;
    }
    //返回的都是jwt
}


JwtUtil

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.exceptions.ValidateException;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;


/**
 * @Author zf
 * @ClassName JwtUtil.java
 * @ProjectName demo
 */
/*
 * 总的来说,工具类中有三个方法
 * 获取JwtToken,获取JwtToken中封装的信息,判断JwtToken是否存在
 * 1. encode(),参数是=签发人,存在时间,一些其他的信息=。返回值是JwtToken对应的字符串
 * 2. decode(),参数是=JwtToken=。返回值是荷载部分的键值对
 * 3. isVerify(),参数是=JwtToken=。返回值是这个JwtToken是否存在
 * */
public class JwtUtil {
    //创建默认的秘钥和算法,供无参的构造方法使用
    private static final String defaultBase64EncodedSecretKey = "2A67A3E221C63EB882879A7332A0902A";
    private static final SignatureAlgorithm defaultSignatureAlgorithm = SignatureAlgorithm.HS512;

    private static String base64EncodedSecretKey = defaultBase64EncodedSecretKey;
    private static SignatureAlgorithm signatureAlgorithm = defaultSignatureAlgorithm;

    //用默认密钥和加密算法
    public JwtUtil() {
        this(defaultBase64EncodedSecretKey, defaultSignatureAlgorithm);
    }

    //自定义密钥和加密算法
    public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
        base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
        JwtUtil.signatureAlgorithm = signatureAlgorithm;
    }

    /*
     *这里就是产生jwt字符串的地方
     * jwt字符串包括三个部分
     *  1. header
     *      -当前字符串的类型,一般都是“JWT”
     *      -哪种算法加密,“HS512”或者其他的加密算法
     *      所以一般都是固定的,没有什么变化
     *  2. payload
     *      一般有四个最常见的标准字段(下面有)
     *      iat:签发时间,也就是这个jwt什么时候生成的
     *      jti:JWT的唯一标识
     *      iss:签发人,一般都是username或者userId
     *      exp:过期时间
     *
     * */
    public static String encode(String iss, long ttlMillis, Map<String, Object> claims) {
        //iss签发人,ttlMillis生存时间,claims是指还想要在jwt中存储的一些非隐私信息
        if (claims == null) {
            claims = new HashMap<>();
        }
        long nowMillis = System.currentTimeMillis();

        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())//2. 这个是JWT的唯一标识,一般设置成唯一的,这个方法可以生成唯一标识
                .setIssuedAt(new Date(nowMillis))//1. 这个地方就是以毫秒为单位,换算当前系统时间生成的iat
                .setSubject(iss)//3. 签发人,也就是JWT是给谁的(逻辑上一般都是username或者userId)
                .signWith(signatureAlgorithm, base64EncodedSecretKey);//这个地方是生成jwt使用的算法和秘钥
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);//4. 过期时间,这个也是使用毫秒生成的,使用当前时间+前面传入的持续时间生成
            builder.setExpiration(exp);
        }
        return builder.compact();
    }


    //相当于encode的方向,传入jwtToken生成对应的username和password等字段。Claim就是一个map
    //也就是拿到荷载部分所有的键值对
    public Claims decode(String jwtToken) {

        // 得到 DefaultJwtParser
        return Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(base64EncodedSecretKey)
                // 设置需要解析的 jwt
                .parseClaimsJws(jwtToken)
                .getBody();
    }

    //判断jwtToken是否合法
    public boolean isVerify(String jwtToken) {
        //这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
        Algorithm algorithm = null;
        switch (signatureAlgorithm) {
            case HS512:
                algorithm = Algorithm.HMAC512(Base64.decodeBase64(base64EncodedSecretKey));
                break;
            default:
                throw new RuntimeException("不支持该算法");
        }
        JWTVerifier verifier = JWT.require(algorithm).build();
        try {
            verifier.verify(jwtToken);  // 校验不通过会抛出异常
            //判断合法的标准:1. 头部和荷载部分没有篡改过。2. 没有过期
        }catch (JWTVerificationException e){
            System.out.println(e.getMessage());
            return false;
        }
        return true;
    }

    //校验token是否过期
    public static boolean checkTokenOverdue(String token) throws ValidateException {
        //解密token对象
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(defaultBase64EncodedSecretKey).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        //过期时间
        Long expirationTime = Long.parseLong(DateUtil.format(claims.getExpiration(), "yyyy-MM-dd HH:mm:ss").replaceAll("[^0-9]","").trim());
        //当前时间
        Long thisTime = Long.parseLong(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss").replaceAll("[^0-9]","").trim());

        //如果当前时间大于过期时间
        if (thisTime > expirationTime){
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        //生成token测试
        JwtUtil util = new JwtUtil("2A67A3E221C63EB882879A7332A0902A", SignatureAlgorithm.HS512);
        //以tom作为秘钥HS512加密
        HashMap<String, Object> map = new HashMap<>();
        map.put("username", "admin");
        map.put("password", "123456");
        //ttlMillis 令牌有效期,单位:毫秒 默认有效期:15分钟
        String jwtToken = JwtUtil.encode("tom", 900000, map);
        System.out.println(jwtToken);
        util.decode(jwtToken).entrySet().forEach((entry) -> {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        });
    }
}

最后修改:2022 年 08 月 31 日
给我一点小钱钱也很高兴啦!o(* ̄▽ ̄*)ブ