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

1. 基本功能点

Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。

1.1  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 核心的过滤器

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

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

  1. anon AnonymousFilter 指定 url 可以匿名访问

  2. authc FormAuthenticationFilter 基于表单的拦截器;如 “/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username);passwordParam:表单提交的密码参数名(password);rememberMeParam:表单提交的密码参数名(rememberMe);loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址;failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure)

  3. authcBasic BasicHttpAuthenticationFilter Basic HTTP 身份验证拦截器,主要属性:applicationName:弹出登录框显示的信息(application)

  4. logout authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/)

  5. user UserFilter 用户拦截器,用户已经身份验证 / 记住我登录的都可

  6. 授权相关的

  7. roles RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性:loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]”

  8. perms PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms[“user:create”]”

  9. port PortFilter 端口拦截器,主要属性:port(80):可以通过的端口;示例 “/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样

  10. 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)

  11. ssl SslFilter SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样

  12. 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 支持四种权限的注解:

  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 来判断是否有访问的权限,或者其他的需求呢,反正就是官方提供的过滤器不符合我们的要求,那么我们就可以自己完成自己的过滤器。

自定义权限过滤器:

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

 

posted @   IT6889  阅读(836)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示