首页 OES之七:权限管理 Shiro
文章
取消

OES之七:权限管理 Shiro

Shiro 简介

Shiro 是一个Java安全框架,用于身份验证、授权、加密和会话管理

官方主页 [https://shiro.apache.org/]{https://shiro.apache.org/}

SpringBoot 集成 Shiro

引入依赖

1
2
3
4
5
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.8.0</version>
            </dependency>

Realm 设置

要想自定义实现身份认证和授权功能,就必须继承 org.apache.shiro.authc.AuthenticationException 类,实现自定义的认证 Realm

  • doGetAuthenticationInfo() 用于登录认证,可以在此处实现用户登录时的认证方式,我是用 邮箱+密码 的方式直接去数据库里找进行验证,这里的参数 token 就是在登录的时候传入的,具体见下文。

  • doGetAuthorizationInfo() 用于实现获取用户的角色和权限,在执行带有 @RequiresPermissions@RequiresRoles 的方法前、执行 subject.hasPermissions()subjwect.hasRoles() 方法时会执行此方法

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
@Component
public class ShiroRealm extends AuthorizingRealm {
    
    @Resource
    private UserService userService;
    @Resource
    private CacheManager cacheManager;

    @PostConstruct
    private void initConfig() {
        setAuthenticationCachingEnabled(false);
        setAuthorizationCachingEnabled(true);
        setCachingEnabled(true);
        setCacheManager(cacheManager);
    }

    /**
     * 授权模块,获取用户角色和权限
     *
     * @param principal principal
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        User user = (User) principal.getPrimaryPrincipal();
        user = userService.doGetUserAuthorization(user);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(user.getRoleNames());
        simpleAuthorizationInfo.setStringPermissions(user.getPermissions());
        return simpleAuthorizationInfo;
    }

    /**
     * 用户认证
     *
     * @param token AuthenticationToken 身份认证 token
     * @return AuthenticationInfo 身份认证信息
     * @throws AuthenticationException 认证相关异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取用户输入的邮箱和密码
        String email = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());

        // 通过用户名到数据库查询用户信息
        User user = this.userService.findByEmail(email);
        if (user == null || !StringUtils.isEquals(password, user.getPassword())) {
            throw new IncorrectCredentialsException("邮箱或密码错误!");
        }
        if (UserStatusEnum.LOCK.getCode().equals(user.getStatus())) {
            throw new LockedAccountException("账号已被锁定,请联系客服!");
        }
        return new SimpleAuthenticationInfo(user, password, getName());
    }
}

ShiroConfig

新建一个配置类,用于添加 Shiro 用到的所有的 bean

首先是一堆写在 spring.yml 中的配置,此处可以先忽略,直接看方法

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
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.oes.common.constans.OesConstant;
import org.oes.common.constans.Strings;
import org.oes.common.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Base64Utils;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

@Configuration(proxyBeanMethods = false)
public class ShiroConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.database:0}")
    private int database;

    @Value("${oes.shiro.session-timeout}")
    private long shiroSessionTimeout;
    @Value("${oes.shiro.cookie-timeout}")
    private int shiroCookieTimeout;

    // ... 方法见下文
}

对应的配置文件新增内容

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
spring:
  redis:
    # Redis数据库索引(默认为 0)
    database: 0
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis 密码
    password:
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 8
        # 连接池中的最大空闲连接
        max-idle: 500
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 2000
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: 10000
    # 连接超时时间(毫秒)
    timeout: 5000

# 系统常量配置
oes:
  shiro:
    session-timeout: 3600   # session 超时时间,单位为秒
    cookie-timeout: 864000  # rememberMe cookie有效时长,单位为秒

首先是一个 Redis 的管理器,用于登录 Redis 服务,进行存取操作,此处用到了,前五个配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    /**
     * shiro 中配置 redis 缓存
     *
     * @return RedisManager
     */
    private RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host + Strings.COLON + port);
        if (StringUtils.isNotBlank(password)) {
            redisManager.setPassword(password);
        }
        redisManager.setTimeout(timeout);
        redisManager.setDatabase(database);
        return redisManager;
    }

然后是缓存管理,我们直接使用 Redis 作为缓存管理器,创建一个缓存管理器的 bean

1
2
3
4
5
6
7
8
9
    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setExpire((int) shiroSessionTimeout);
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

然后是关于 Cookie 用户设置设置记住我的功能

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
    /**
     * cookie管理对象
     *
     * @return CookieRememberMeManager
     */
    private CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        // rememberMe cookie 加密的密钥
        String encryptKey = "KEY_OES";
        byte[] encryptKeyBytes = encryptKey.getBytes(StandardCharsets.UTF_8);
        String rememberKey = Base64Utils.encodeToString(Arrays.copyOf(encryptKeyBytes, 16));
        cookieRememberMeManager.setCipherKey(Base64.decode(rememberKey));
        return cookieRememberMeManager;
    }

    /**
     * rememberMe cookie 效果是重开浏览器后无需重新登录
     *
     * @return SimpleCookie
     */
    private SimpleCookie rememberMeCookie() {
        // 设置 cookie 名称,对应 login.html 页面的 <input type="checkbox" name="rememberMe"/>
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        // 设置 cookie 的过期时间,单位为秒,这里为一天
        cookie.setMaxAge(shiroCookieTimeout);
        return cookie;
    }

然后是会话管理器,设置 Shiro 所使用的会话管理的管理器对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /**
     * session 管理对象
     *
     * @return DefaultWebSessionManager
     */
    @Bean
    public SessionManager sessionManager(SessionDAO sessionDAO) {
        ShiroSessionManager sessionManager = new DefaultWebSessionManager();
        // 设置 session超时时间
        sessionManager.setGlobalSessionTimeout(shiroSessionTimeout * 1000L);
        sessionManager.setSessionDAO(sessionDAO);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    @Bean
    public SessionDAO sessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

最后是 securityManager,这是 Shiro 框架下的核心管理器,Shiro 所有的授权认证行为都将由此管理,在这里设置了认证使用的Realm、会话管理器、缓存管理器、记住我的Cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    @Bean
    public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,
                                                     SessionManager sessionManager,
                                                     CacheManager cacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 配置 SecurityManager,并注入 shiroRealm
        securityManager.setRealm(shiroRealm);
        // 配置 shiro session管理器
        securityManager.setSessionManager(sessionManager);
        // 配置 缓存管理类 cacheManager
        securityManager.setCacheManager(cacheManager);
        // 配置 rememberMeCookie
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

ShiroEarlyConfig

这个类其实可以和 ShiroConfig 合并,因为功能是一样的,但是为了减少对Bean后置处理器的影响,把它独立了出来,它主要实现了一个工厂 bean 创建 ShiroFilterFactoryBean

此处还有一个坑 DefaultAdvisorAutoProxyCreator bean在我的系统中Spring并不会自动创建,需要手动创建,否则 Shiro 中的 @RequiresPermissions 注解会失效

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
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.oes.common.constans.URIs;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

import java.util.LinkedHashMap;
import java.util.stream.Collectors;

/**
 * 将注册时机较早的Bean单独提取出来,并且相关依赖延迟注入,
 * 尽可能的缩小对Bean后置处理器的影响
 * <p>
 * https://github.com/spring-projects/spring-boot/issues/16097
 * https://issues.apache.org/jira/browse/SHIRO-743
 */
@Configuration(proxyBeanMethods = false)
public class ShiroEarlyConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Lazy DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置 securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登录的 url
        shiroFilterFactoryBean.setLoginUrl(URIs.LOGIN);
        // 登录成功后跳转的 url
        shiroFilterFactoryBean.setSuccessUrl(URIs.SUCCESS);
        // 未授权 url
        shiroFilterFactoryBean.setUnauthorizedUrl(URIs.UNAUTHORIZED);
        LinkedHashMap<String, String> filterChainDefinitionMap = URIs.unauthorized.stream()
                .collect(Collectors.toMap(url -> url, url -> "anon", (a, b) -> b, LinkedHashMap::new));
        filterChainDefinitionMap.put(URIs.LOGOUT, "logout");
        // 除登出以外所有 url都必须认证通过才可以访问,未通过认证自动访问 LoginUrl
        filterChainDefinitionMap.put(URIs.ALL, "user");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /*
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */

    /**
     * 配置此对象的目的是,在spring容器启动时,
     * 扫描所有的advisor(顾问)对象,基于advisor
     * 对象中切入点的描述,为目标对象创建代理对象
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Lazy DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

关于后置处理器,启动时会有如下的日志,虽然只是一个INFO,可能不会影响到系统功能

1
[main] INFO  o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'shiroEarlyConfig' of type [org.oes.start.tools.shiro.ShiroEarlyConfig] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

权限问题

在上一节的 ShiroFilterFactoryBean bean 创建的时候有配置一些对 URI 访问时的权限认证

使用的 URIs 常量类如下

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package org.oes.common.constans;

import java.util.ArrayList;
import java.util.List;

/**
 * 资源地址
 */
public class URIs {
    public static final String ALL = "/**";

    /**
     * 登录
     */
    public static final String LOGIN = "/login";

    /**
     * 注册
     */
    public static final String REGISTER = "/register";

    /**
     * 忘记密码
     */
    public static final String FORGET = "/forget";

    /**
     * 验证手机验证码
     */
    public static final String PHONE_VERIFICATION = "/phone";

    /**
     * 验证邮箱验证码
     */
    public static final String EMAIL_VERIFICATION = "/email";

    /**
     * 设置密码
     */
    public static final String PASSWORD = "/password";

    /**
     * 未授权
     */
    public static final String UNAUTHORIZED = "/unauthorized";

    /**
     * 登出
     */
    public static final String LOGOUT = "/logout";

    /**
     * 登录成功
     */
    public static final String SUCCESS = "/success";

    /**
     * 角色操作
     */
    public static final String ROLE = "/role";

    /**
     * 权限操作
     */
    public static final String PERMISSIONS = "/permissions";

    /**
     * 用户操作
     */
    public static final String USER = "/user";

    /**
     * 课程操作
     */
    public static final String COURSE = "/course";

    /**
     *  免认证部分的URL
     */
    public static List<String> unauthorized = new ArrayList<>();
    static {
        unauthorized.add(LOGIN);
        unauthorized.add(REGISTER);
        unauthorized.add(PHONE_VERIFICATION);
        unauthorized.add(EMAIL_VERIFICATION);
        unauthorized.add(UNAUTHORIZED);
        unauthorized.add(SUCCESS);
    }
}

权限使用

我仅使用到了 权限过滤 功能,角色过滤同理

1
2
3
4
5
    @RequestMapping(path = URIs.TEST, method = RequestMethod.GET)
    @RequiresPermissions("perms:test")
    public OesHttpResponse test() {
        return OesHttpResponse.getSuccess();
    }

访问成功则返回 success,否则会跳转到对应的地址

参考

https://blog.csdn.net/xiaoxiaole0313/article/details/105501799

https://blog.csdn.net/weixin_46504244/article/details/120385617

https://blog.csdn.net/palerock/article/details/73457415

本文由作者按照 CC BY 4.0 进行授权