1springboot集成shiro(单机版)和自定义过滤器

1springboot集成shiro(单机版)和自定义过滤器

1背景

我们做web项目的过程中,都涉及到登录和授权,那么我们采用一种适合自己的安全框架就很重要了,本章节主要讲springboot+shiro(单机版)

2简介

1.1.基本功能点

Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。其基本功能点如下图所示:

  1. Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
  2. Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  3. Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
  4. Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  5. Web Support:Web 支持,可以非常容易的集成到 Web 环境;
  6. Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
  7. Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  8. Testing:提供测试支持;
  9. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  10. Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。

1.2架构图

1.3核心的过滤器

hiro提供多个默认的过滤器,我们可以用这些过滤器来配置控制指定URL的权限,Shiro常见的过滤器如下:

配置缩写对应的过滤器功能身份验证相关的

  1. anonAnonymousFilter指定url可以匿名访问
  2. authcFormAuthenticationFilter基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure)
  3. authcBasicBasicHttpAuthenticationFilterBasic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application)
  4. logoutauthc.LogoutFilter退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/)
  5. userUserFilter用户拦截器,用户已经身份验证/记住我登录的都可
  6. 授权相关的
  7. rolesRolesAuthorizationFilter角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”
  8. permsPermissionsAuthorizationFilter权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]”
  9. portPortFilter端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
  10. restHttpMethodPermissionFilterrest风格拦截器,自动根据请求方法构建权限字符串(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)
  11. sslSslFilterSSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样
  12. noSessionCreationNoSessionCreationAuthorizationFilter

3环境搭建

pom依赖

        <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>    <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>

ShiroConfig

Configurationpublic 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.webp", "anon");        filterMap.put("/**", "authc");        filterMap.put("/**", "perms");        shiroFilter.setFilterChainDefinitionMap(filterMap);        return shiroFilter;    }}

UserRealm 自定义认证授权

@Componentpublic class UserRealm extends AuthorizingRealm {    @Autowired    private SysUserDao sysUserDao;    @Autowired    private SysMenuDao sysMenuDao;        /**     * 授权(验证权限时调用)     */@Overrideprotected 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;}/** * 认证(登录时调用) */@Overrideprotected 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;}    @Overridepublic void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();shaCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);shaCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);super.setCredentialsMatcher(shaCredentialsMatcher);}

过滤器配置

主要是增加shirofilter到ioc容器内

@Configurationpublic 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

    @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

/** * 所有用户列表 */@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支持四种权限的注解:

  1. RequiresPermissions 需要权限@RequiresPermissions( {"file:read", "write:aFile.txt"} )
  2. RequiresAuthentication

This annotation basically ensures that subject.isAuthenticated() === true

  1. RequiresGuest
  2. RequiresRoles :@RequiresRoles("aRoleName");需要角色
  3. RequiresUser

方案2 使用授权过滤器

官方提供了很多授权过滤器

上面的过滤器介绍中介绍了,

那我们怎么使用呢?我们自己自定义授权过滤器哈.比如我们现在有个需求,要根据url来判断是否有访问的权限,或者其他的需求呢,反正就是官方提供的过滤器不符合我们的要求,那么我们就可以自己完成自己的过滤器。

自定义权限过滤器

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

@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.webp", "anon");        filterMap.put("/**", "authc");        //除了anno所有的请求都是该权限过滤器        filterMap.put("/**", "url");        shiroFilter.setFilters(customisedFilter);        shiroFilter.setFilterChainDefinitionMap(filterMap);        return shiroFilter;    }

4.1问题2

如果我们不想使用cookie来传递session,我可以使用其他的方式么?

解决方案 DefaultWebSessionManager

查看DefaultWebSessionManager的源码我们可以看到如下的代码

  @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,代码如下:

    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的代码如下:

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方法

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;    }

参考:

blog.csdn.net/weixin_3027…

www.bbsmax.com/A/mo5kKKAQ5…

推荐阅读