【Shiro探索】

本文详细介绍了如何将Shiro与Redis集成,用于认证和授权的缓存,并展示了如何结合JWT实现无状态认证。内容包括Shiro的配置、自定义Realm、Redis缓存管理、Session管理以及JWT的实现思路和代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.shiro导包

   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.80</version>
        </dependency>

        <!-- 工具包 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.6</version>
        </dependency>

        <!-- mybatis-plus基本包 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- mybatis-plus代码生成器 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!-- mybatis-plus代码生成器  添加模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.3</version>
        </dependency>

        <!---->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <!--ehcache缓存-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.5.3</version>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- Token生成与解析-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

2.注意点

  • 若同时使用RequiresPermissions和RequiresRoles两个注解,条件是且,不是或

        @RequiresPermissions("hello:helloadmin")
        @RequiresRoles("admin")
        @GetMapping("/helloAdmin")
        public String helloAdmin(){
            return "hello admin";
        }
    

    上面这个代码,一定是要有admin角色,且有hello:helloadmin权限才能访问

  • 无法使用权限注解的问题

    我使用如下版本,并无此问题

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-web-starter</artifactId>
        <version>1.5.3</version>
    </dependency>
    

    若出现了无法使用注解的情况,则在ShiroConfig中加入如下配置

        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                    new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
        /**
         * 下面的代码是添加注解支持
         *
         * @return
         */
        @Bean
        @DependsOn("lifecycleBeanPostProcessor")
        public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
            creator.setProxyTargetClass(true);
            return creator;
        }
    
  • AdminAuthorizingRealm中,doGetAuthenticationInfo返回的SimpleAuthenticationInfo第一个参数是认证主体,网上教程返回的是用户名,但我更喜欢使用当前登录对象ComAdmin admin,这样在获取用户主体时候,可以通过如下代码获得,否则只能获取到用户名:

    Subject currentUser = SecurityUtils.getSubject();
    ComAdmin admin = (ComAdmin) currentUser.getPrincipal();
    

    getPrincipal() 获取的就是SimpleAuthenticationInfo传入的第一个参数

3.简单集成

只集成基本功能,认证和授权均直接从数据库读取,

3.1 创建表结构(5张)

  • com_admin: 密码为加盐且散列后的数据

    CREATE TABLE `com_admin` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(63) DEFAULT NULL,
      `password` varchar(63) DEFAULT NULL,
      `salt` varchar(255) DEFAULT NULL COMMENT '盐',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='用户表';
    
  • com_role

    CREATE TABLE `com_role` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(63) DEFAULT NULL COMMENT '角色名称',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='角色表';
    
  • com_admin_role

    CREATE TABLE `com_admin_role` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `admin_id` int(11) DEFAULT NULL,
      `role_id` int(11) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
    
  • com_perms

    CREATE TABLE `com_perms` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(127) DEFAULT NULL COMMENT '权限名称',
      `url` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
    
  • com_role_perms

    CREATE TABLE `com_role_perms` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `role_id` int(11) DEFAULT NULL,
      `perms_id` int(11) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
    

3.2 配置ShiroConfig

import com.example.shirodemo.shiro.CustomerRealm;
import com.example.shirodemo.shiro.cache.RedisCacheManager;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/1 17:12
 * 用来整合shiro相关的配置类
 */
@Configuration
public class ShiroConfig {
    //1. 创建shiroFilter 负责拦截所有请求,
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 给Filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 配置系统受限资源
        // 配置系统公共资源
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 一定要放在通配符的上面
        filterChainDefinitionMap.put("/auth/nologin", "anon");    // anon 不需要认证和授权, url可以匿名访问
        filterChainDefinitionMap.put("/auth/login", "anon");
        filterChainDefinitionMap.put("/auth/register", "anon");

        filterChainDefinitionMap.put("/**", "authc");  // authc 请求这个资源需要认证和授权
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        // 默认认证界面路径
        shiroFilterFactoryBean.setLoginUrl("/auth/nologin");

        return shiroFilterFactoryBean;
    }

    //2. 创建安全管理器
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 给安全管理器设置Realm
        securityManager.setRealm(realm);
        return securityManager;
    }

    //3. 创建自定义realm
    @Bean
    public Realm realm() {
        CustomerRealm customerRealm = new CustomerRealm();
        // 修改凭证校验匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置散列次数,必须跟注册一样
        credentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(credentialsMatcher);

        return customerRealm;
    }
}

3.3 自定义Realm

此处用的盐,是处理了序列化和反序列化后的自定义MySimpleByteSource,是为了解决redis反序列化失败后的问题,如不需要redis,可直接使用**SimpleByteSource.Util.bytes(admin.getSalt())**替换

import com.example.shirodemo.pojo.Admin;
import com.example.shirodemo.pojo.Perms;
import com.example.shirodemo.pojo.Role;
import com.example.shirodemo.service.AdminService;
import com.example.shirodemo.service.PermsService;
import com.example.shirodemo.service.RoleService;
import com.example.shirodemo.shiro.salt.MySimpleByteSource;
import com.example.shirodemo.utils.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/1 17:23
 */
@Slf4j
public class CustomerRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("================ 授权 ===============");
        // 获取身份信息
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        RoleService roleService = (RoleService) BeanUtil.getBean("roleService");
        PermsService permsService = (PermsService) BeanUtil.getBean("permsService");


        if (principals.getPrimaryPrincipal() instanceof String) {
            // 用username当SimpleAuthenticationInfo主身份信息
            String username = (String) principals.getPrimaryPrincipal();

            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            List<Role> roleList = roleService.getRoleByUsername(username);
            List<Perms> permsList = permsService.getPermsByUsername(username);

            if (!CollectionUtils.isEmpty(roleList)) {
                Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.setRoles(roleSet);
            }
            if (!CollectionUtils.isEmpty(permsList)) {
                Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.addStringPermissions(permsSet);
            }
            return simpleAuthorizationInfo;
        } else if (principals.getPrimaryPrincipal() instanceof Admin) {
            // 整个用户对象当主用户信息
            Admin admin = (Admin) principals.getPrimaryPrincipal();

            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            List<Role> roleList = roleService.getRoleByUserId(admin.getId());
            List<Perms> permsList = permsService.getPermsByUserId(admin.getId());

            if (!CollectionUtils.isEmpty(roleList)) {
                Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.setRoles(roleSet);
            }
            if (!CollectionUtils.isEmpty(permsList)) {
                Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.addStringPermissions(permsSet);
            }
            return simpleAuthorizationInfo;
        }
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("================ 认证 ===============");
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        // 在工厂中获取service对象
        AdminService adminService = (AdminService) BeanUtil.getBean("adminService");
        List<Admin> adminList = adminService.getAdminByUsername(upToken.getUsername());
        Assert.state(adminList.size() < 2, "同一个用户名存在两个账户");
        if (adminList.size() == 0) {
            throw new UnknownAccountException("找不到用户(" + upToken.getUsername() + ")的帐号信息");
        }
        Admin admin = adminList.get(0);
        // 1、主身份信息,在授权存入缓存时候,会以第一个参数当key,用对象当key也可以
        //return new SimpleAuthenticationInfo(admin.getUsername(), admin.getPassword(), new MySimpleByteSource(admin.getSalt()), this.getName());
        return new SimpleAuthenticationInfo(admin, admin.getPassword(), new MyByteSource(admin.getSalt()), this.getName());
    }
}

3.4 添加注解

在Controller里的方法使用注解即可进行认证授权

    // @RequiresPermissions("hello:helloadmin:*")
    @RequiresRoles("admin")
    @GetMapping("/helloAdmin")
    public String helloAdmin(){
        return "hello admin";
    }

    // @RequiresPermissions("hello:hellouser:*")
    @RequiresRoles("user")
    @GetMapping("/helloUser")
    public String helloUser(){
        return "hello user";
    }

    @RequiresPermissions("hello:helloadmin:*")
    @GetMapping("/helloAdminPerm")
    public String helloAdminPerm(){
        return "hello adminPerm";
    }

    @RequiresPermissions("user:hello:*")
    @GetMapping("/helloUserPerm")
    public String helloUserPerm(){
        return "hello userPerm";
    }

3.5 登录

返回sessionId当做token

    @PostMapping("login")
    public ResponseResult login(@RequestBody JSONObject obj) {
        String username = obj.getString("username");
        String password = obj.getString("password");

        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username, password));
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            log.info("用户名错误");
            return ResponseResult.fail("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            log.info("密码错误");
            return ResponseResult.fail("密码错误");
        }

        return ResponseResult.ok(subject.getSession().getId());
    }

3.6 注册

	@Override
    public ResponseResult register(Admin admin) {
        String salt = SaltUtils.getSalt(8);
        admin.setSalt(salt);
        // 加盐,并散列1024次,与ShiroConfig中配置保持一致
        Md5Hash md5Hash = new Md5Hash(admin.getPassword(), salt, 1024);
        admin.setPassword(md5Hash.toHex());
        if (getCountByUsername(admin.getUsername()) > 0) {
            return ResponseResult.fail("用户名已存在");
        }
        if (save(admin)) {
            return ResponseResult.ok();
        } else {
            return ResponseResult.fail("注册失败");
        }
    }

4.认证和授权使用redis缓存

4.1 ShiroConfig

添加开启缓存

    @Bean
    public Realm realm() {
        CustomerRealm customerRealm = new CustomerRealm();
        // 修改凭证校验匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置散列次数,必须跟注册一样
        credentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(credentialsMatcher);

        // 开启缓存管理
        // customerRealm.setCacheManager(new EhCacheManager());
        customerRealm.setCacheManager(new RedisCacheManager());
        customerRealm.setCachingEnabled(true);
        customerRealm.setAuthorizationCachingEnabled(true);     // 授权
        customerRealm.setAuthorizationCacheName("authorizationCache");
        customerRealm.setAuthenticationCachingEnabled(true);    // 认证
        customerRealm.setAuthenticationCacheName("authenticationCache");
        return customerRealm;
    }

4.2 自定义RedisCacheManager

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/2 19:46
 */
public class RedisCacheManager implements CacheManager {
    /**
     *
     * @param cacheName 认证或者是授权缓存的名字
     * @param <K>
     * @param <V>
     * @return
     * @throws CacheException
     */
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println("cacheName:"+cacheName);
        return new RedisShiroCache<>(cacheName);
    }
}

4.3 自定义RedisShiroCache

若主体应用使用了FastJson2JsonRedisSerializerJackson2JsonRedisSerializer进行序列化,如果直接使用,会导致SimpleAuthenticationInfo反序列化失败,则这边需要自定义一个redisTemplate进行操作。

import com.example.shirodemo.utils.BeanUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.Collection;
import java.util.Set;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/2 19:52
 */
public class RedisShiroCache<K, V> implements Cache<K, V> {

    private String cacheName;

    public RedisShiroCache(String cacheName) {
        this.cacheName = cacheName;
    }

    @Override
    public V get(K k) throws CacheException {
        System.out.println("get key:" + cacheName + ":" + k);
        return (V) getRedisTemplate().opsForHash().get(cacheName, k.toString());
    }

    @Override
    public V put(K k, V v) throws CacheException {
        System.out.println("put key:" + cacheName + ":" + k);
        System.out.println("put value:" + v);
        getRedisTemplate().opsForHash().put(cacheName, k.toString(), v);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        getRedisTemplate().opsForHash().delete(cacheName, k.toString());
        return null;
    }

    @Override
    public void clear() throws CacheException {
        getRedisTemplate().delete(cacheName);
    }

    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(cacheName).intValue();
    }

    @Override
    public Set<K> keys() {
        return getRedisTemplate().opsForHash().keys(cacheName);
    }

    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(cacheName);
    }

    private RedisTemplate getRedisTemplate() {
        RedisTemplate redisTemplate = (RedisTemplate) BeanUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

4.4 自定义盐

复制SimpleByteSource添加无参构造函数,并把bytes的final去掉

import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/2 20:41
 *
 * 自定义salt实现序列化, 与SimpleByteSource的区别,只是为了序列化,
 * 反序列化需要一个无参的构造,所以又加上无参构造
 */

public class MySimpleByteSource implements ByteSource,Serializable {
    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;


    public MySimpleByteSource() {
    }

    public MySimpleByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MySimpleByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MySimpleByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MySimpleByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MySimpleByteSource(File file) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
    }

    public MySimpleByteSource(InputStream stream) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public byte[] getBytes() {
        return this.bytes;
    }

    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    public String toString() {
        return this.toBase64();
    }

    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource)o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}

5. session使用redis缓存

5.1 创建RedisSessionDAO

参考1 参考2

import com.example.shirodemo.utils.BeanUtil;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/3 14:29
 */
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {


    private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
    // session 在redis过期时间是30分钟30*60
    public static int SESSION_EXPIRE_TIME = 30 * 60 * 1000;

    private static String prefix = "shiro:redis:session:";

    private RedisTemplate redisTemplate;


    // 创建session,保存到数据库
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.doCreate(session);
        logger.debug("创建session:{}", session.getId());
        getRedisTemplate().opsForValue().set(prefix + sessionId.toString(), session);
        return sessionId;
    }

    // 获取session
    @Override
    protected Session doReadSession(Serializable sessionId) {
        logger.debug("获取session:{}", sessionId);
        // 先从缓存中获取session,如果没有再去数据库中获取
        Session session = super.doReadSession(sessionId);
        if (session == null) {
            session = (Session) getRedisTemplate().opsForValue().get(prefix + sessionId.toString());
        }
        return session;
    }

    // 更新session的最后一次访问时间
    @Override
    protected void doUpdate(Session session) {
        super.doUpdate(session);
        logger.debug("获取session:{}", session.getId());
        String key = prefix + session.getId().toString();
        if (!getRedisTemplate().hasKey(key)) {
            getRedisTemplate().opsForValue().set(key, session);
        }
        getRedisTemplate().expire(key, SESSION_EXPIRE_TIME, TimeUnit.MILLISECONDS);
    }

    // 删除session
    @Override
    protected void doDelete(Session session) {
        logger.debug("删除session:{}", session.getId());
        super.doDelete(session);
        getRedisTemplate().delete(prefix + session.getId().toString());
    }

    private RedisTemplate getRedisTemplate() {
        if (redisTemplate == null) {
            redisTemplate = new RedisTemplate();
            RedisTemplate redisTemp = (RedisTemplate) BeanUtil.getBean("redisTemplate");
            redisTemplate.setConnectionFactory(redisTemp.getConnectionFactory());
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.afterPropertiesSet();
        }
        return redisTemplate;
    }
}

5.2 创建SessionManager

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * 原来使用session
 */
public class AdminWebSessionManager extends DefaultWebSessionManager {

    public static final String LOGIN_TOKEN_KEY = "X-Shilaogaizao-Admin-Token";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(LOGIN_TOKEN_KEY);
        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);
        }
    }
}

5.3 配置ShiroConfig

5.3.1 SecurityManager配置session

    //2. 创建安全管理器
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 给安全管理器设置Realm
        securityManager.setRealm(realm);

        // 设置session
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

5.3.2 配置session相关

    /**
     * 配置Session管理器
     * @Author Sans
     * @CreateTime 2019/6/12 14:25
     */
    @Bean
    public SessionManager sessionManager() {
        AdminWebSessionManager shiroSessionManager = new AdminWebSessionManager();
        shiroSessionManager.setSessionDAO(new RedisSessionDAO());
        shiroSessionManager.setGlobalSessionTimeout(RedisSessionDAO.SESSION_EXPIRE_TIME);
        return shiroSessionManager;
    }

*setGlobalSessionTimeout设定为毫秒

6. 使用jwt

6.1 思路

之前以为只能定义一个Realm,所以一直绕不出来,

以为:(错误思路)

​ 1:账号密码登录,生成Token;

​ 2:用自定义拦截器,拦截Token,使用某种手段获取到用户信息,绑定给Shiro

后面参考:参考1 参考2 才发现思路完全错误

正确思路:

​ 1:定义一个Realm专门处理UsernamePasswordToken类型登录,只要验证过,就用JwtUtil生成一个Token返回给前端

​ 2:定义另一个Realm专门处理BearerToken(持票人,shiro自带),专门用来处理持有Token访问的请求

​ 3:定义一个全局的Filter JwtFilter用于拦截需要认证的请求,并封装成BearerToken调用login重走认证路,只不过当前只会走2定义的Realm

​ 4:禁用Session,并配置JwtFilter拦截所有需要认证的请求

说明:

​ 1:两个Realm只有认证部分有所不同,授权部分一模一样,使用思路是:

​ 1.1:账号密码登录,走UsernamePasswordToken,认证成功,生成jwtToken返回前端

​ 1.2:用户每次请求,在head持有jwtToken,请求时被JwtFilter拦截,调用login,走TokenValidateRealm

​ 2:由于CustomerRealm只用来处理账号密码登录,所以它里面的授权代码,压根就不会执行到,而是只会执行TokenValidateRealm里的授权

6.2 代码实现

6.2.1 修改CustomerRealm

// 添加如下代码,其实主要是配置supports,配置上这个,主要是告诉shiro,这个Realm只处理这个类型的ShiroToken

    /**
     * 标识这个Realm只处理这种Token
     * 首先是覆盖getAuthenticationTokenClass方法,此时设定返回值为UsernamePasswordToken.class。
     * shiro的机制是根据login方法中传入的token类型来分配realm,
     * 步骤1中是UsernamePasswordToken,所以分配给本realm来处理。
     *
     * @return
     */
    @Override
    public Class getAuthenticationTokenClass() {
        return UsernamePasswordToken.class;
    }

    /**
     * 限定这个realm只能处理JwtToken(不加的话会报错)
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

6.2.2 新增TokenValidateRealm

import com.example.shirodemo.pojo.Admin;
import com.example.shirodemo.pojo.Perms;
import com.example.shirodemo.pojo.Role;
import com.example.shirodemo.service.AdminService;
import com.example.shirodemo.service.PermsService;
import com.example.shirodemo.service.RoleService;
import com.example.shirodemo.utils.BeanUtil;
import com.example.shirodemo.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/4 10:34
 */
@Slf4j
public class TokenValidateRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("================ 授权 ===============");
        // 获取身份信息
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        RoleService roleService = (RoleService) BeanUtil.getBean("roleService");
        PermsService permsService = (PermsService) BeanUtil.getBean("permsService");


        if (principals.getPrimaryPrincipal() instanceof String) {
            // 用username当SimpleAuthenticationInfo主身份信息
            String username = (String) principals.getPrimaryPrincipal();

            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            List<Role> roleList = roleService.getRoleByUsername(username);
            List<Perms> permsList = permsService.getPermsByUsername(username);

            if (!CollectionUtils.isEmpty(roleList)) {
                Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.setRoles(roleSet);
            }
            if (!CollectionUtils.isEmpty(permsList)) {
                Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.addStringPermissions(permsSet);
            }
            return simpleAuthorizationInfo;
        } else if (principals.getPrimaryPrincipal() instanceof Admin) {
            // 整个用户对象当主用户信息
            Admin admin = (Admin) principals.getPrimaryPrincipal();

            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            List<Role> roleList = roleService.getRoleByUserId(admin.getId());
            List<Perms> permsList = permsService.getPermsByUserId(admin.getId());

            if (!CollectionUtils.isEmpty(roleList)) {
                Set<String> roleSet = roleList.stream().map(Role::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.setRoles(roleSet);
            }
            if (!CollectionUtils.isEmpty(permsList)) {
                Set<String> permsSet = permsList.stream().map(Perms::getName).collect(Collectors.toSet());
                simpleAuthorizationInfo.addStringPermissions(permsSet);
            }
            return simpleAuthorizationInfo;
        }
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        BearerToken bearerToken = (BearerToken) token;
        String bearerTokenString = bearerToken.getToken();

        String username = JwtUtil.get(bearerTokenString, "username", String.class);

        if (StringUtils.isBlank(username)) {
            throw new UnknownAccountException("找不到用户(" + username + ")的帐号信息");
        }

        AdminService adminService = (AdminService) BeanUtil.getBean("adminService");
        List<Admin> adminList = adminService.getAdminByUsername(username);

        Assert.state(adminList.size() < 2, "同一个用户名存在两个账户");
        if (adminList.size() == 0) {
            throw new UnknownAccountException("找不到用户(" + username + ")的帐号信息");
        }
        Admin admin = adminList.get(0);
        // 1、主身份信息,在授权存入缓存时候,会以第一个参数当key,用对象当key也可以,但最好用username,所以此处修改一下

        // return new SimpleAuthenticationInfo(admin.getUsername(), bearerTokenString, this.getName());
        return new SimpleAuthenticationInfo(admin, bearerTokenString, this.getName());
    }


    @Override
    public Class getAuthenticationTokenClass() {
        //设置由本realm处理的token类型。BearerToken是在filter里自动装配的。
        return BearerToken.class;
    }

    /**
     * 限定这个realm只能处理JwtToken(不加的话会报错)
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof BearerToken;
    }
}

6.2.3 新增JwtFilter

import com.example.shirodemo.shiro.exception.TokenException;
import com.example.shirodemo.utils.JwtUtil;
import com.example.shirodemo.utils.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.BearerToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @version 1.0
 * @author: hjianfeng
 * @date: 2022/4/3 21:02
 */

/**
 * 鉴权登录拦截器
 **/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    public static final String LOGIN_TOKEN_KEY = "X-Shilaogaizao-Admin-Token";

    public static final String JWT_USER_ID = "userId";

    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            return executeLogin(request, response);
        } catch (Exception e) {
            throw new AuthenticationException(e.getMessage());
        }
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(LOGIN_TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            throw new TokenException(ResponseResult.UNLOGIN, "token为空,请重新登录");
        }
        if (JwtUtil.isTokenExpired(token)) {
            throw new TokenException(ResponseResult.UNLOGIN, "token已过期,请重新登录");
        }
        BearerToken jwtToken = new BearerToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;

    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

6.2.4 修改ShiroConfig

6.2.4.1 修改Realm
// 修改原始Realm以及新增tokenRealm

	// 这个基本不变,之前在配置SecurityManager时是用注入形式,两个直接使用方法调用
    //3. 创建自定义realm
    @Bean("loginRealm")
    public Realm realm() {
        CustomerRealm customerRealm = new CustomerRealm();
        // 修改凭证校验匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置散列次数,必须跟注册一样
        credentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(credentialsMatcher);

        // 开启缓存管理
        // customerRealm.setCacheManager(new EhCacheManager());
        customerRealm.setCacheManager(new RedisCacheManager());
        customerRealm.setCachingEnabled(true);
        customerRealm.setAuthorizationCachingEnabled(true);     // 授权
        customerRealm.setAuthorizationCacheName("authorizationCache");
        customerRealm.setAuthenticationCachingEnabled(true);    // 认证
        customerRealm.setAuthenticationCacheName("authenticationCache");
        return customerRealm;
    }
	// 创建专门用于处理Token的Realm
    @Bean("tokenRealm")
    public Realm tokenRealm() {
        TokenValidateRealm customerRealm = new TokenValidateRealm();
        // 开启缓存管理
        // customerRealm.setCacheManager(new EhCacheManager());
        customerRealm.setCacheManager(new RedisCacheManager());
        customerRealm.setCachingEnabled(true);
        customerRealm.setAuthorizationCachingEnabled(true);     // 授权
        customerRealm.setAuthorizationCacheName("authorizationCache");
        customerRealm.setAuthenticationCachingEnabled(true);    // 认证
        customerRealm.setAuthenticationCacheName("authenticationCache");
        return customerRealm;
    }
6.2.4.2 修改SecurityManager

配置多个Realm,禁用Session

@Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 给安全管理器设置Realm
        List<Realm> realms = new ArrayList<>();
        realms.add(realm());
        realms.add(tokenRealm());
        securityManager.setRealms(realms);
        // securityManager.setRealm(realm);

        // 设置session
        // securityManager.setSessionManager(sessionManager());

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-
         * StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }
6.2.4.3 修改shiroFilterFactoryBean

需要添加自定义过滤器,如下,并配置该过滤器为拦截所有需要认证的请求,猜测原本应该是有一个默认对应名称为**“authc”**的拦截器,名称需要一致

        // 添加自定义过滤器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

		filterChainDefinitionMap.put("/**", "jwt");  // authc 请求这个资源需要认证和授权

原本代码:

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 给Filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 配置系统受限资源
        // 配置系统公共资源
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 一定要放在通配符的上面
        filterChainDefinitionMap.put("/auth/nologin", "anon");    // anon 不需要认证和授权, url可以匿名访问
        filterChainDefinitionMap.put("/auth/login", "anon");
        filterChainDefinitionMap.put("/auth/register", "anon");

        filterChainDefinitionMap.put("/**", "authc");  // authc 请求这个资源需要认证和授权
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        // 默认认证界面路径
        shiroFilterFactoryBean.setLoginUrl("/auth/nologin");

        return shiroFilterFactoryBean;
    }

修改后:

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 给Filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 添加自定义过滤器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        
        // 配置系统受限资源
        // 配置系统公共资源
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 一定要放在通配符的上面
        filterChainDefinitionMap.put("/auth/nologin", "anon");    // anon 不需要认证和授权, url可以匿名访问
        filterChainDefinitionMap.put("/auth/login", "anon");
        filterChainDefinitionMap.put("/auth/register", "anon");

        filterChainDefinitionMap.put("/**", "jwt");  // authc 请求这个资源需要认证和授权
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        // 默认认证界面路径
        shiroFilterFactoryBean.setLoginUrl("/auth/nologin");

        return shiroFilterFactoryBean;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值