Springboot集成Shiro(前后端分离)

2022-03-01 19:40:05 浏览数 (1)

Springboot集成Shiro(前后端分离)

思维导图

什么是Shiro

shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。

spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。

shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。

使用shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。

用户通过subject登陆,形成一个UsernamePasswordToken,令牌,在域realm里完成认证、授权,成功后加入缓存。(realm可以写、也可以用默认的,也可以写很多个域)

Shiro架构

  • subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
  • securityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。
  • authenticator:认证器,主体进行认证最终通过authenticator进行的。
  • authorizer:授权器,主体进行授权最终通过authorizer进行的。
  • sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
  • SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
  • cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
  • realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。 在realm中存储授权和认证的逻辑。

认证过程

认证执行流程

1、通过ini配置文件创建securityManager

2、调用subject.login方法主体提交认证,提交的token

3、securityManager进行认证,securityManager最终由ModularRealmAuthenticator进行认证。

4、ModularRealmAuthenticator调用IniRealm(给realm传入token) 去ini配置文件中查询用户信息

5、IniRealm根据输入的token(UsernamePasswordToken)从 shiro.ini查询用户信息,根据账号查询用户信息(账号和密码)

代码语言:javascript复制
如果查询到用户信息,就给ModularRealmAuthenticator返回用户信息(账号和密码)
如果查询不到,就给ModularRealmAuthenticator返回null

6、ModularRealmAuthenticator接收IniRealm返回Authentication认证信息

如果返回的认证信息是null,ModularRealmAuthenticator抛出异常(org.apache.shiro.authc.UnknownAccountException)

如果返回的认证信息不是null(说明inirealm找到了用户),对IniRealm返回用户密码 (在ini文件中存在) 和 token中的密码 进行对比,如果不一致抛出异常(org.apache.shiro.authc.IncorrectCredentialsException)

授权流程

1、对subject进行授权,调用方法isPermitted(”permission串”)

2、SecurityManager执行授权,通过ModularRealmAuthorizer执行授权

3、ModularRealmAuthorizer执行realm(自定义的Realm)从数据库查询权限数据

调用realm的授权方法:doGetAuthorizationInfo

4、realm从数据库查询权限数据,返回ModularRealmAuthorizer

5、ModularRealmAuthorizer调用PermissionResolver进行权限串比对

6、如果比对后,isPermitted中”permission串”在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。

实现

代码地址:https://github.com/cayzlh/spring-boot-shiro-demo

自定义Realm

CustRealm:

代码语言:javascript复制
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    @Lazy
    private AuthorizingService authorizingService;

    /**
     * Shiro 的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
     * 当访问到页面的时候,链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行,
     * 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = (String) super.getAvailablePrincipal(principalCollection);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        Set<String> roles = authorizingService.findRoleListByUsername(username);
        authorizationInfo.setRoles(roles);
        roles.forEach(role -> {
            Set<String> permissions = authorizingService.findPermissionsByRole(roles);
            authorizationInfo.addStringPermissions(permissions);
        });
        return authorizationInfo;
    }

    /**
     * 在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。
     * 因为在 Shiro 中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
     * 通常情况下,在 Realm 中会直接从我们的数据源中获取 Shiro 需要的验证信息。
     * 可以说,Realm 是专用于安全框架的 DAO. Shiro 的认证过程最终会交由 Realm 执行,
     * 这时会调用 Realm 的getAuthenticationInfo(token)方法。
     *
     * 该方法主要执行以下操作:
     *
     * 1、检查提交的进行认证的令牌信息
     * 2、根据令牌信息从数据源(通常为数据库)中获取用户信息
     * 3、对用户信息进行匹配验证。
     * 4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
     * 5、验证失败则抛出AuthenticationException异常信息。
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
            throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        UserInfo user = authorizingService.selectByUserName(username);
        if (null == user) {
            throw new UnknownAccountException("doGetAuthenticationInfo() has an UnknownAccountException: " username);
        }
        String passwordInToken = new String(token.getPassword());
        String passwordInDb = user.getPassword();
        if (!StringUtils.equals(passwordInDb, passwordInToken)) {
            throw new IncorrectCredentialsException("doGetAuthenticationInfo() has an IncorrectCredentialsException: " username);
        }
        return new SimpleAuthenticationInfo(username, passwordInToken, ByteSource.Util.bytes(user.getSalt()), getName());
    }
}

自定义SessionManager

在我们项目中, 由于使用前后端分离的架构,所以要自定义Shiro的session管理:

代码语言:javascript复制
public class CustomSessionManager extends DefaultWebSessionManager {

    private static final String HEADER_TOKEN = "token";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public CustomSessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN);
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {

            return super.getSessionId(request, response);
        }
    }
}

通过监听请求Header里的token字段,如果有值,则作为shiro的sessionid。

配置Shiro

代码语言:javascript复制
@Configuration
public class ShiroConfig {

    @Bean(name = "customRealm")
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

    @Bean(name = "sessionManager")
    public SessionManager sessionManager() {
        return new CustomSessionManager();
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(customRealm());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

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

}

其中:

代码语言:javascript复制
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    advisorAutoProxyCreator.setProxyTargetClass(true);
    return advisorAutoProxyCreator;
}

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

在spring boot中, shiro使用@RequiresRoles,@RequiresPermissions注解无效时,需要添加这两个配置。

其他代码

AuthorizingService:

代码语言:javascript复制
@Service
public class AuthorizingService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;

    @Autowired
    private PermissionDao permissionDao;

    public UserInfo selectByUserName(String username) {
        return userDao.selectByUsername(username);
    }

    public Set<String> findRoleListByUsername(String username) {
        return roleDao.selectByUsername(username);
    }

    public Set<String> findPermissionsByRole(Set<String> roles) {
        HashSet<String> permissions = Sets.newHashSet();
        roles.forEach(role -> permissions.addAll(permissionDao.selectByRole(role)));
        return permissions;
    }

    public String login(String username, String password) {
        UsernamePasswordToken token = new UsernamePasswordToken(username,
                password);
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.login(token);
        currentUser.getSession().setTimeout(60 * 60 * 1000);
        return currentUser.getSession().getId().toString();
    }

    public void logout() {
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.logout();
    }
}

PermissionDao:

代码语言:javascript复制
@Repository
public class PermissionDao {

    public Set<String> selectByRole(String role) {
        switch (role) {
            case "admin":
                return Sets.newHashSet("Idea", "navicat", "notepad", "webstorm",
                        "chrome");
            case "java":
                return Sets.newHashSet("Idea");
            case "mysql":
                return Sets.newHashSet("navicat");
            case "html":
                return Sets.newHashSet("notepad");
            case "javascript":
                return Sets.newHashSet("webstorm");
            case "guest":
                return Sets.newHashSet("chrome");
            default:
                return Sets.newHashSet();
        }
    }
}

RoleDao:

代码语言:javascript复制
@Repository
public class RoleDao {

    public Set<String> selectByUsername(String username) {
        switch (username) {
            case "zhangsan":
                return Sets.newHashSet("admin");
            case "lisi":
                return Sets.newHashSet("java", "mysql");
            case "wangwu":
                return Sets.newHashSet("html", "javascript");
            default:
                return Sets.newHashSet("guest");
        }
    }

}

UserDao:

代码语言:javascript复制
@Repository
public class UserDao {

    public UserInfo selectByUsername(String username) {

        switch (username) {
            case "zhangsan":
                return UserInfo.builder()
                        .userName("zhangsan").password("123456").salt("123456")
                        .build();
            case "lisi":
                return UserInfo.builder()
                        .userName("lisi").password("123456").salt("123456")
                        .build();
            case "wangwu":
                return UserInfo.builder()
                        .userName("wangwu").password("123456").salt("123456")
                        .build();
            default:
                return null;
        }


    }

}

这里没有使用数据库, 直接模拟数据库操作。

集群

在实际项目运行中,为了达到高可用的目的,通常要把应用部署在多台服务器上,这个时候就要对session进行集群的管理

添加redis支持
pom.xml
代码语言:javascript复制
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>
配置文件
代码语言:javascript复制
spring:
  application:
    name: shiro-demo
  redis:
    host: redistest.xxxx.com
    port: 6379
    database: 10
    password: test@2018
    timeout: 180000
    jedis:
      pool:
        max-active: 100
        max-wait: 360000
        min-idle: 0
        max-idle: 100
my:
  shiro:
    session:
      expireTime: 1800
      prefix: you-shiro-session
新增Java类:ShiroCacheManager
代码语言:javascript复制
public class ShiroCacheManager implements CacheManager {

    private RedisTemplate redisTemplate;

    private int expireTime;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        return new ShiroRedisCache<>(name, redisTemplate, expireTime);
    }

    public ShiroCacheManager(RedisTemplate redisTemplate, int expireTime) {
        this.redisTemplate = redisTemplate;
        this.expireTime = expireTime;
    }

    public class ShiroRedisCache<K, V> implements Cache<K, V> {

        private String cacheKey;

        private RedisTemplate redisTemplate;

        private int expireTime;

        private ShiroRedisCache(String cacheKey, RedisTemplate redisTemplate, int expireTime) {
            this.cacheKey = cacheKey;
            this.redisTemplate = redisTemplate;
            this.expireTime = expireTime;
        }

        private Object hashKey(K key) {
            if (key instanceof PrincipalCollection) {
                PrincipalCollection principalCollection = (PrincipalCollection) key;
                return principalCollection.getPrimaryPrincipal().toString();
            }
            return key;
        }

        @Override
        public V get(K key) throws CacheException {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            Object realKey = hashKey(key);
            return boundHashOperations.get(realKey);
        }

        @Override
        public V put(K key, V value) throws CacheException {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            Object realKey = hashKey(key);
            boundHashOperations.put((K) realKey, value);
            boundHashOperations.expire(expireTime, TimeUnit.SECONDS);
            return value;
        }

        @Override
        public V remove(K key) throws CacheException {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            Object realKey = hashKey(key);
            V value = boundHashOperations.get(realKey);
            boundHashOperations.delete(realKey);
            return value;
        }

        @Override
        public void clear() throws CacheException {
            redisTemplate.delete(cacheKey);
        }

        @Override
        public int size() {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            return boundHashOperations.size().intValue();
        }

        @Override
        public Set<K> keys() {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            return boundHashOperations.keys();
        }

        @Override
        public Collection<V> values() {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            return boundHashOperations.values();
        }
    }
}
新增java类:RedisSessionDAO,继承EnterpriseCacheSessionDAO
代码语言:javascript复制
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {

    private int expireTime;

    private String prefix;

    private RedisTemplate redisTemplate;

    public RedisSessionDAO(RedisTemplate redisTemplate, int expireTime, String prefix) {
        this.redisTemplate = redisTemplate;
        this.expireTime = expireTime;
        this.prefix = prefix;
    }

    @Override
    protected Serializable doCreate(Session session) {
        log.info("doCreate({})", session.getId());
        Serializable sessionId = super.doCreate(session);
        redisTemplate.opsForValue().set(prefix   ":"   sessionId.toString(), session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        log.info("doReadSession({})", sessionId);
        Session session = super.doReadSession(sessionId);
        if (session == null) {
            session = (Session) redisTemplate.opsForValue().get(prefix   ":"   sessionId.toString());
        }
        return session;
    }

    @Override
    protected void doUpdate(Session session) {
        super.doUpdate(session);
        String key = prefix   ":"   session.getId().toString();
        if (!redisTemplate.hasKey(key)) {
            redisTemplate.opsForValue().set(key, session);
        }
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    }

    @Override
    protected void doDelete(Session session) {
        log.info("doDelete({})", session.getId());
        super.doDelete(session);
        redisTemplate.delete(prefix   ":"   session.getId().toString());
    }
}
新增Java类:RedisObjectSerializer implements RedisSerializer
代码语言:javascript复制
public class RedisObjectSerializer implements RedisSerializer<Object> {
    private Converter<Object, byte[]> serializer = new SerializingConverter();
    private Converter<byte[], Object> deserializer = new DeserializingConverter();
    static final byte[] EMPTY_ARRAY = new byte[0];

    @Override
    public Object deserialize(byte[] bytes) {
        if (isEmpty(bytes)) {
            return null;
        }
        try {
            return deserializer.convert(bytes);
        } catch (Exception ex) {
            throw new SerializationException("Cannot deserialize", ex);
        }
    }

    @Override
    public byte[] serialize(Object object) {
        if (object == null) {
            return EMPTY_ARRAY;
        }
        try {
            return serializer.convert(object);
        } catch (Exception ex) {
            return EMPTY_ARRAY;
        }
    }

    private boolean isEmpty(byte[] data) {
        return (data == null || data.length == 0);
    }
}
修改ShiroConfig
代码语言:javascript复制
@Configuration
public class ShiroConfig implements InitializingBean {

    @Value("${my.shiro.session.expireTime:1800}")
    private int expireTime;

    @Value("${my.shiro.session.prefix:you-shiro-session}")
    private String prefix;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void afterPropertiesSet() {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new RedisObjectSerializer());
        redisTemplate.afterPropertiesSet();
    }

    @Bean
    public RedisTemplate redisTemplate() {
        return new RedisTemplate();
    }

    @Bean(name = "shiroCacheManager")
    public ShiroCacheManager shiroCacheManager() {
        return new ShiroCacheManager(redisTemplate, expireTime);
    }

    @Bean(name = "redisSessionDAO")
    public RedisSessionDAO redisSessionDAO() {
        return new RedisSessionDAO(redisTemplate, expireTime, prefix);
    }

    @Bean(name = "customRealm")
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

    @Bean(name = "sessionManager")
    public SessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(customRealm());

        securityManager.setCacheManager(shiroCacheManager());

        securityManager.setSessionManager(sessionManager());

        return securityManager;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

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

}
集群完成

^_^.

经过以上改造,shiro就可以在分布式应用中集群使用。

项目代码,在 with-redis 分支中。

测试

运行Demo,测试登录和请求其他接口:

分享计划

博客内容将同步至腾讯云 社区,邀请大家一同入驻:https://cloud.tencent.com/

许可协议

本文采用 署名-非商业性使用-相同方式共享 4.0 国际 许可协议,转载请注明出处。

0 人点赞