springboot集成shiro(单机版)和自定义过滤器
1. 基本功能点
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
1.1 Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。其基本功能点如下图所示:
-
Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
-
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
-
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
-
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
-
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
-
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
-
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
-
Testing:提供测试支持;
-
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
-
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。
1.2 架构图:
1.3 核心的过滤器
shiro 提供多个默认的过滤器,我们可以用这些过滤器来配置控制指定 URL 的权限,Shiro 常见的过滤器如下:
配置缩写 对应的过滤器 功能 身份验证相关的
-
anon AnonymousFilter 指定 url 可以匿名访问
-
authc FormAuthenticationFilter 基于表单的拦截器;如 “/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username);passwordParam:表单提交的密码参数名(password);rememberMeParam:表单提交的密码参数名(rememberMe);loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址;failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure)
-
authcBasic BasicHttpAuthenticationFilter Basic HTTP 身份验证拦截器,主要属性:applicationName:弹出登录框显示的信息(application)
-
logout authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/)
-
user UserFilter 用户拦截器,用户已经身份验证 / 记住我登录的都可
-
授权相关的
-
roles RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性:loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]”
-
perms PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms[“user:create”]”
-
port PortFilter 端口拦截器,主要属性:port(80):可以通过的端口;示例 “/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样
-
rest HttpMethodPermissionFilter rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例 “/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete” 权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
-
ssl SslFilter SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样
-
noSessionCreation NoSessionCreationAuthorizationFilter
2. 环境搭建
2.1 pom 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency><br> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> |
2.2 ShiroConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | @Configuration public class ShiroConfig { /** * 单机环境,session交给shiro管理 */ @Bean public DefaultWebSessionManager sessionManager( @Value ( "${renren.globalSessionTimeout:3600}" ) long globalSessionTimeout){ DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionValidationSchedulerEnabled( true ); sessionManager.setSessionIdUrlRewritingEnabled( false ); sessionManager.setSessionValidationInterval(globalSessionTimeout * 1000 ); sessionManager.setGlobalSessionTimeout(globalSessionTimeout * 1000 ); return sessionManager; } @Bean ( "securityManager" ) public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setCacheManager( new EhCacheManager()); securityManager.setRealm(userRealm); securityManager.setSessionManager(sessionManager); securityManager.setRememberMeManager( null ); return securityManager; } @Bean ( "shiroFilter" ) public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); shiroFilter.setLoginUrl( "/login.html" ); shiroFilter.setUnauthorizedUrl( "/" ); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put( "/swagger/**" , "anon" ); filterMap.put( "/v2/api-docs" , "anon" ); filterMap.put( "/swagger-ui.html" , "anon" ); filterMap.put( "/webjars/**" , "anon" ); filterMap.put( "/swagger-resources/**" , "anon" ); filterMap.put( "/statics/**" , "anon" ); filterMap.put( "/login.html" , "anon" ); filterMap.put( "/sys/login" , "anon" ); filterMap.put( "/favicon.ico" , "anon" ); filterMap.put( "/captcha.jpg" , "anon" ); filterMap.put( "/**" , "authc" ); filterMap.put( "/**" , "perms" ); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } } |
2.3 UserRealm 自定义认证授权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | @Component public class UserRealm extends AuthorizingRealm { @Autowired private SysUserDao sysUserDao; @Autowired private SysMenuDao sysMenuDao; /** * 授权(验证权限时调用) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal(); Long userId = user.getUserId(); List<String> permsList; //系统管理员,拥有最高权限 if (userId == Constant.SUPER_ADMIN){ List<SysMenuEntity> menuList = sysMenuDao.selectList( null ); permsList = new ArrayList<>(menuList.size()); for (SysMenuEntity menu : menuList){ permsList.add(menu.getPerms()); } } else { permsList = sysUserDao.queryAllPerms(userId); } //用户权限列表 Set<String> permsSet = new HashSet<>(); for (String perms : permsList){ if (StringUtils.isBlank(perms)){ continue ; } permsSet.addAll(Arrays.asList(perms.trim().split( "," ))); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(permsSet); return info; } /** * 认证(登录时调用) */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken)authcToken; //查询用户信息 SysUserEntity user = sysUserDao.selectOne( new QueryWrapper<SysUserEntity>().eq( "username" , token.getUsername())); //账号不存在 if (user == null ) { throw new UnknownAccountException( "账号或密码不正确" ); } //账号锁定 if (user.getStatus() == 0 ){ throw new LockedAccountException( "账号已被锁定,请联系管理员" ); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); return info; } @Override public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher(); shaCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName); shaCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations); super .setCredentialsMatcher(shaCredentialsMatcher); } |
2.4 过滤器配置
主要是增加 shirofilter 到 ioc 容器内
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @Configuration public class FilterConfig { @Bean public FilterRegistrationBean shiroFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter( new DelegatingFilterProxy( "shiroFilter" )); //该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 registration.addInitParameter( "targetFilterLifecycle" , "true" ); registration.setEnabled( true ); registration.setOrder(Integer.MAX_VALUE - 1 ); registration.addUrlPatterns( "/*" ); return registration; } @Bean public FilterRegistrationBean xssFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setDispatcherTypes(DispatcherType.REQUEST); registration.setFilter( new XssFilter()); registration.addUrlPatterns( "/*" ); registration.setName( "xssFilter" ); registration.setOrder(Integer.MAX_VALUE); return registration; } |
4.问题
上述搭建的环境只是简单的可以认证,很多小伙伴都很好奇的问,那我授权怎么做呢?我们将从方案 2 中来解决
方案 1 使用注解方式 shiroConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Bean ( "lifecycleBeanPostProcessor" ) public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 开始shiro的权限注解 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } |
RequiresPermissions
1 2 3 4 5 6 7 8 9 10 | /** * 所有用户列表 */ @RequestMapping ( "/list" ) @RequiresPermissions ( "sys:user:list" ) public R list( @RequestParam Map<String, Object> params){ PageUtils page = sysUserService.queryPage(params); return R.ok().put( "page" , page); } |
shiro 支持四种权限的注解:
-
RequiresPermissions 需要权限 @RequiresPermissions({"file:read", "write:aFile.txt"} )
-
RequiresAuthentication
This annotation basically ensures that subject.isAuthenticated() === true
-
RequiresGuest
-
RequiresRoles :@RequiresRoles("aRoleName"); 需要角色
-
RequiresUser
方案 2 使用授权过滤器
官方提供了很多授权过滤器
上面的过滤器介绍中介绍了,
那我们怎么使用呢?我们自己自定义授权过滤器哈. 比如我们现在有个需求,要根据 url 来判断是否有访问的权限,或者其他的需求呢,反正就是官方提供的过滤器不符合我们的要求,那么我们就可以自己完成自己的过滤器。
自定义权限过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class CustomAutorizatioinFilter extends AuthorizationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { //获取请求的url String servletPath = ((HttpServletRequest) request).getServletPath(); Subject subject = SecurityUtils.getSubject(); PrincipalCollection principals = subject.getPrincipals(); //可以从db或者redis中获取你拥有的权限,然后判断是否有权限 //比如从reids中获取改用户的可以访问的url List<String> urls = Lists.newArrayList( "a" , "b" ); if (urls.contains(servletPath)) { return true ; } return true ; } } |
配置ShiroConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @Bean ( "shiroFilter" ) public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); // 自定义拦截器 Map<String, Filter> customisedFilter = new HashMap<>(); customisedFilter.put( "url" , new CustomAutorizatioinFilter()); shiroFilter.setLoginUrl( "/login.html" ); shiroFilter.setUnauthorizedUrl( "/" ); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put( "/swagger/**" , "anon" ); filterMap.put( "/v2/api-docs" , "anon" ); filterMap.put( "/swagger-ui.html" , "anon" ); filterMap.put( "/webjars/**" , "anon" ); filterMap.put( "/swagger-resources/**" , "anon" ); filterMap.put( "/statics/**" , "anon" ); filterMap.put( "/templates/**" , "anon" ); filterMap.put( "/modules/**" , "anon" ); filterMap.put( "/login.html" , "anon" ); filterMap.put( "/sys/login" , "anon" ); filterMap.put( "/favicon.ico" , "anon" ); filterMap.put( "/captcha.jpg" , "anon" ); filterMap.put( "/**" , "authc" ); //除了anno所有的请求都是该权限过滤器 filterMap.put( "/**" , "url" ); shiroFilter.setFilters(customisedFilter); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } |
4.1 问题 2
如果我们不想使用 cookie 来传递 session,我可以使用其他的方式么?
解决方案: DefaultWebSessionManager
查看 DefaultWebSessionManager 的源码我们可以看到如下的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Override protected void onStart(Session session, SessionContext context) { super .onStart(session, context); if (!WebUtils.isHttp(context)) { log.debug( "SessionContext argument is not HTTP compatible or does not have an HTTP request/response " + "pair. No session ID cookie will be set." ); return ; } HttpServletRequest request = WebUtils.getHttpRequest(context); HttpServletResponse response = WebUtils.getHttpResponse(context); if (isSessionIdCookieEnabled()) { Serializable sessionId = session.getId(); storeSessionId(sessionId, request, response); } else { log.debug( "Session ID cookie is disabled. No cookie has been set for new session with id {}" , session.getId()); } request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE); } |
然后我们可以看到有个方法:storeSessionId,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) { if (currentId == null ) { String msg = "sessionId cannot be null when persisting for subsequent requests." ; throw new IllegalArgumentException(msg); } Cookie template = getSessionIdCookie(); Cookie cookie = new SimpleCookie(template); String idString = currentId.toString(); cookie.setValue(idString); cookie.saveTo(request, response); log.trace( "Set session ID cookie for session with id {}" , idString); } |
上面得代码分析可得到,session 放在了 cookie 里。
获取 sessionId 的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) { String id = getSessionIdCookieValue(request, response); if (id != null ) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); } else { //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting): //try the URI path segment parameters first: id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME); if (id == null ) { //not a URI path segment parameter, try the query parameters: String name = getSessionIdName(); id = request.getParameter(name); if (id == null ) { //try lowercase: id = request.getParameter(name.toLowerCase()); } } if (id != null ) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.URL_SESSION_ID_SOURCE); } } if (id != null ) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); //automatically mark it valid here. If it is invalid, the //onUnknownSession method below will be invoked and we'll remove the attribute at that time. request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); } // always set rewrite flag - SHIRO-361 request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled()); return id; } |
通过上述的思路,我们就可以自定义 DefaultWebSessionManager 该类的 storeSessionId,和 getReferencedSessionId 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | public class CustomDefaultWebSessionManager extends DefaultWebSessionManager { private static final Logger log = LoggerFactory.getLogger(CustomDefaultWebSessionManager. class ); private final String X_AUTH_TOKEN = "x-auth-token" ; // 请求头中获取 sessionId 并把sessionId 放入 response 中 private String getSessionIdHeaderValue(ServletRequest request, ServletResponse response) { if (!(request instanceof HttpServletRequest)) { log.debug( "Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null." ); return null ; } else { HttpServletRequest httpRequest = (HttpServletRequest) request; // 在request 中 读取 x-auth-token 信息 作为 sessionId String sessionId = httpRequest.getHeader( this .X_AUTH_TOKEN); // 每次读取之后 都把当前的 sessionId 放入 response 中 HttpServletResponse httpResponse = (HttpServletResponse) response; if (StringUtils.isNotEmpty(sessionId)) { httpResponse.setHeader( this .X_AUTH_TOKEN, sessionId); log.info( "Current session ID is {}" , sessionId); } return sessionId; } } //获取sessionid private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) { String id = this .getSessionIdHeaderValue(request, response); //DefaultWebSessionManager 中代码 直接copy过来 if (id != null ) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header" ); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); } //不会把sessionid放在URL后 request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, Boolean.FALSE); return id; } } |
然后再 shiroConfig 中加入我们自定义的 session 管理类
public DefaultWebSessionManager sessionManager(@Value("${renren.globalSessionTimeout:3600}") long globalSessionTimeout){
CustomDefaultWebSessionManager sessionManager = new CustomDefaultWebSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionValidationInterval(globalSessionTimeout * 1000);
sessionManager.setGlobalSessionTimeout(globalSessionTimeout * 1000);
return sessionManager;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)