Spring Boot2.x 整合 Shiro (JWT模式)

发布时间 2024-01-05 12:40:15作者: 夏秋初

参考

注意

  1. 多模块项目报错 Description: No bean of type ‘org.apache.shiro.realm.Realm‘ found 是因为Spring Boot 默认加载当前包及包下的 @Bean ,如果 Shiro 配置在其他子模块则需要添加注解@ComponentScan(value = {"当前包", "Shiro 相关所在包"})让当前模块入口文件扫描。(多种加载不同模块@Bean的方式,这只是其中一种。)

  2. shiro 所有请求都会被拦截,并且404。(同上)

  3. 参考若依流程是:

    1. 登录->加载用户信息(权限等)->保存redis
    2. 网关过滤器检测token->如果url需要验证则判定token是否存在、过期等。
    3. 自定义注解(切面)->redis取出用户信息->根据注解参数鉴权。
  4. 参考jeecg-boot流程是:

    1. 登录储存用户信息。
    2. 网关不负责校验,直接转发请求。
    3. 控制器根据 Shiro 注解校验权限。

环境

环境 版本 说明
Windows 10
VS Code 1.85.1
Spring Boot Extension Pack v0.2.1 vscode插件
Extension Pack for Java v0.25.15 vscode插件
JDK 11
Springboot 2.3.12.RELEASE
shiro-spring-boot-web-starter 1.13.0 mvn依赖
hutool-all 5.8.24 mvn依赖
Apache Maven 3.8.6

正文

准备

  1. pom.xml 引入,注意:多模块项目中 Shiro 可能放到公共模块,虽然公共模块没有入口文件,但是 shiro 自定义 Filter 需要用到 servlet.Filter,所以引入 spring-boot-starter-web
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.8.24</version>
</dependency>
<!-- shiro过滤器依赖servlet -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
 <dependency>
	 <groupId>org.apache.shiro</groupId>
	 <artifactId>shiro-spring-boot-web-starter</artifactId>
	 <version>1.13.0</version>
</dependency>
  1. 创建 Jwt 工具类。
package com.xiaqiuchu.common.config.shiro;

import java.util.Map;

import org.springframework.stereotype.Component;

import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import lombok.Data;

@Component
@Data
public class ShiroJwtUtil {

    private static final byte[] key = "换成你的密匙".getBytes();

    /*
     * 创建
     */
    public String createToken(Map<String, Object> payload){
        return JWTUtil.createToken(payload, ShiroJwtUtil.key);
    }


    /*
     * 解析
     */
    public JWT parseToken(String token){
        JWT jwt = JWTUtil.parseToken(token);
        return jwt;
    }


    /*
     * 验证
     */
    public Boolean verify(String token){
        return JWTUtil.verify(token, ShiroJwtUtil.key);
    }
}
  1. 创建 Jwt 类。
package com.xiaqiuchu.common.config.shiro;

import org.apache.shiro.authc.AuthenticationToken;

//这个就类似UsernamePasswordToken
public class ShiroJwtToken implements AuthenticationToken {

    private String jwt;

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

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

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

  1. 自定义过滤器 Shiro ShiroJwtFilter.java
package com.xiaqiuchu.common.config.shiro;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.web.filter.AccessControlFilter;
import lombok.extern.slf4j.Slf4j;

/*
 * 自定义一个Filter,用来拦截所有的请求判断是否携带Token
 * isAccessAllowed()判断是否携带了有效的JwtToken
 * onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问
 * */
@Slf4j
public class ShiroJwtFilter extends AccessControlFilter {

    ShiroJwtFilter(){}

    /*
     * 判断是否通过访问,如果不通过走 onAccessDenied。
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
            throws Exception {
        log.info("执行类:"+this.getClass().getName());
        log.info("执行方法:"+Thread.currentThread() .getStackTrace()[1].getMethodName());
        // 取出token并校验真实性(还应该校验一下是否过期)
        HttpServletRequest httpServletRequest   = (HttpServletRequest) request;
        String jwt                              = httpServletRequest.getHeader("Authorization");
        // 返回false来使用onAccessDenied()方法
        // 本类没有标记为@Component,所以无法自动注入。
        if(new ShiroJwtUtil().verify(jwt)){
            getSubject(request, response).login(new ShiroJwtToken(jwt));
            return true;
        }
        return false;
    }

    /**
     * 不通过处理,如果返回true,依旧可以通过。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.info("执行类:"+this.getClass().getName());
        log.info("执行方法:"+Thread.currentThread() .getStackTrace()[1].getMethodName());
        //
        throw new RuntimeException("身份验证异常");
    }
}

5.创建 Realm ShiroJwtRealm.java

package com.xiaqiuchu.common.config.shiro;

import java.util.HashSet;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.stereotype.Component;

import cn.hutool.jwt.JWT;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class ShiroJwtRealm extends AuthorizingRealm {

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

    /**
     * 用户认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("执行类:"+this.getClass().getName());
        log.info("执行方法:"+Thread.currentThread() .getStackTrace()[1].getMethodName());
        // 在 ShiroJwtFilter 中存入的 Principal,也就是 getSubject(request, response).login(new ShiroJwtToken(jwt));
        String jwt = token.getPrincipal().toString();
        // token可以被伪造,但是已经在 isAccessAllowed 中校验过 jwt了,所以这里信任提取数据即可
        JWT jwtObj = new ShiroJwtUtil().parseToken(jwt);
        // 数据库查询、校验用户是否存在等等、、、
        jwtObj.getPayload("id");
        // 返回一个shiro用户。
        return new SimpleAuthenticationInfo(jwt,jwt, this.getClass().getName());
        //这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。还需要一个该Realm的类名

    }
    /**
     * 权限认证(doGetAuthenticationInfo通过后执行权限获取)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("执行类:"+this.getClass().getName());
        log.info("执行方法:"+Thread.currentThread() .getStackTrace()[1].getMethodName());
        // 获取用户,获取用户权限,授权给 shiro用户类
        String jwt = principals.getPrimaryPrincipal().toString();
        // 授权信息可以走数据库
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 设置角色
        simpleAuthorizationInfo.setRoles(new HashSet<String>() {{
            add("admin");
            add("user");
        }});
        // 设置权限
        simpleAuthorizationInfo.addStringPermission("admin:*");
        // log.info("token"+token);
        return simpleAuthorizationInfo;
    }

    
}

  1. 创建 ShiroConfig.java
package com.xiaqiuchu.common.config.shiro;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.Filter;

import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;

@Configuration
public class ShiroConfig {

    // //创建自定义Realm
    @Bean
    public Realm realm() {
        return new ShiroJwtRealm();
    }

    // 创建安全管理器
    @Bean("securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		// 此处可以自动注入吧。
        securityManager.setRealm(realm());
        /*
         * 复制自jeecg-boot
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-
         * StatelessApplications%28Sessionless%29
         */
        // DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        // DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        // subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        // securityManager.setSubjectDAO(subjectDAO);
        //
        return securityManager;
    }

    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(getDefaultWebSecurityManager());
        // 未授权跳转
        shiroFilter.setUnauthorizedUrl("/index/unauthorized");
        // 登录地址(未登录则自动重定向到当前)
        // shiroFilter.setLoginUrl("/index/login");
        //
        Map<String, Filter> filterMap = new HashMap<>();
        //
        filterMap.put("jwt", new ShiroJwtFilter());
        //
        shiroFilter.setFilters(filterMap);

        // 拦截器 以及为什么用 linkedhashmap https://blog.csdn.net/sgx5666666/article/details/109276397
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        // 匿名访问
        filterRuleMap.put("/index/index", "anon");
        filterRuleMap.put("/index/login", "anon");
        filterRuleMap.put("/index/unauthorized", "anon");
        // 登录并具有 admin 角色
        // filterRuleMap.put("/index/admin", "authc,roles[admin]");
        // filterRuleMap.put("/index/admin", "jwt,roles[admin]");
        // 通过jwt校验,需登录才能访问(自行实现逻辑)
        filterRuleMap.put("/**", "jwt");
        //
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
        //
        return shiroFilter;
    }


    // creator.setUsePrefix(true) 在这里是解决另一个问题的。因为当为false时,realm里的doGetAuthorizationInfo会执行两次。另外setProxyTargetClass(true)像是多余的,springboot2.0之后默认使用cglib代理,不需要显示声明为true。
    // @Bean
    // public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
    //     DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
    //     defaultAdvisorAutoProxyCreator.setUsePrefix(true);
    //     return defaultAdvisorAutoProxyCreator;
    // }
}

测试

  1. 入口文件。(如果你的不是多模块项目就可以不添加@ComponentScan注解,因为会自动扫描)
package com.xiaqiuchu.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
/**
 * 默认只加载当前包下,通过 @ComponentScan 加载指定包(为了shiro加的)。有多种方式,这是其中一种。
 */
@ComponentScan(value = {"com.xiaqiuchu.common", "com.xiaqiuchu.api"})
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
  1. 编写控制器 IndexController.java
package com.xiaqiuchu.api.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import org.springframework.web.bind.annotation.RequestHeader;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;


@RequestMapping("/index")
@RestController
public class IndexController {
    
    // 公开界面
    @GetMapping("/index")
    public String index() {
        return Thread.currentThread() .getStackTrace()[1].getMethodName();
    }

    // 登录界面
    @GetMapping("/login")
    public String login() {
        return Thread.currentThread() .getStackTrace()[1].getMethodName();
    }

    // 管理员页面(需要登录)
    @GetMapping("/admin")
    public String admin() {
        return Thread.currentThread() .getStackTrace()[1].getMethodName();
    }
    /**
     * 管理员详情(注解方式,需要登录,需要具有指定角色才可访问,如:admin、user等)
     */
    @RequiresRoles("admin")
    @GetMapping("info")
    public String info() {
        return Thread.currentThread() .getStackTrace()[1].getMethodName();
    }

    /**
     * 权限字符串方式
     */
    @RequiresPermissions("admin:add")
    @GetMapping("role")
    public String role() {
        return Thread.currentThread() .getStackTrace()[1].getMethodName();
    }

    /**
     * 未授权重定向为本页面
     */
    @GetMapping("unauthorized")
    public String unauthorized() {
        return Thread.currentThread() .getStackTrace()[1].getMethodName();
    }
    
}