Spring Boot2.x 整合 Shiro (JWT模式)
参考
- https://www.jianshu.com/p/9b6eb3308294
- https://segmentfault.com/a/1190000014479154
- https://blog.csdn.net/Yearingforthefuture/article/details/117384035
- https://blog.csdn.net/jiachunchun/article/details/90235496
- 本文代码 下载
注意
-
多模块项目报错
Description: No bean of type ‘org.apache.shiro.realm.Realm‘ found
是因为Spring Boot 默认加载当前包及包下的 @Bean ,如果 Shiro 配置在其他子模块则需要添加注解@ComponentScan(value = {"当前包", "Shiro 相关所在包"})
让当前模块入口文件扫描。(多种加载不同模块@Bean的方式,这只是其中一种。) -
shiro 所有请求都会被拦截,并且404。(同上)
-
参考若依流程是:
- 登录->加载用户信息(权限等)->保存redis
- 网关过滤器检测token->如果url需要验证则判定token是否存在、过期等。
- 自定义注解(切面)->redis取出用户信息->根据注解参数鉴权。
-
参考jeecg-boot流程是:
- 登录储存用户信息。
- 网关不负责校验,直接转发请求。
- 控制器根据 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 |
正文
准备
- 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>
- 创建 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);
}
}
- 创建 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
}
- 自定义过滤器 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;
}
}
- 创建
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;
// }
}
测试
- 入口文件。(如果你的不是多模块项目就可以不添加
@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);
}
}
- 编写控制器
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();
}
}
博 主 :夏秋初
地 址 :https://www.cnblogs.com/xiaqiuchu/p/17946908
如果对你有帮助,可以点一下 推荐 或者 关注 吗?会让我的分享变得更有动力~
转载时请带上原文链接,谢谢。
地 址 :https://www.cnblogs.com/xiaqiuchu/p/17946908
如果对你有帮助,可以点一下 推荐 或者 关注 吗?会让我的分享变得更有动力~
转载时请带上原文链接,谢谢。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!