关于spring boot 中 shiro 的使用

ouyu69 发布于 2025-10-30 12 次阅读


依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>2.0.5</version>
</dependency>

编写配置类

@Configuration
public class ShiroConfig {
    @Resource
    private RedisUtils redisUtils;
    @Resource
    private JwtFilter jwtFilter;

    private long expire = 86400000L;

    public ShiroConfig() {
    }

    // 配置Shiro的核心安全管理器并交给bean
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        ObjectMapper objectMapper = new ObjectMapper();
        // 设置 Relam,告诉 shiro 要用这个relam 所联系的对象来进行权限管理 。relam 其实就可以看作 shiro 需要用来处理权限认证的相关信息的桥梁,
        // 比如说这里 AccountRelam 就是把 shiro 和 AccountProfile 联系起来了
        securityManager.setRealm(accountRealm);
        SecurityUtils.setSecurityManager(securityManager);
        // 配置缓存
        ShiroCacheManager shiroCacheManager = new ShiroCacheManager();
        shiroCacheManager.setCacheLiveTime(expire);
        shiroCacheManager.setCacheKeyPrefix(ShiroConstant.SHIRO_AUTHORIZATION_CACHE);
        shiroCacheManager.setRedisUtils(redisUtils);
        shiroCacheManager.setObjectMapper(objectMapper);
        // 设置缓存
        securityManager.setCacheManager(shiroCacheManager);
        /**
         * 关闭shiro自带的session
         */
        DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(defaultSubjectDAO);
        return securityManager;
    }
    // 配置过滤器与url之间的绑定关系
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }
    //加载过滤器
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }
    //开启注解代理
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

}

编写relam

@Slf4j
@Component
public class AccountRealm  extends AuthorizingRealm {
    @Resource
    private JwtUtils jwtUtils;
    @Resource
    private UserRoleService userRoleService;
    @Resource
    private RoleAuthService roleAuthService;
    @Resource
    UserService userService;
    /**
     * 获取用户认证信息,执行认证逻辑
     * @param jwtToken
     * @return
     * @throws AuthenticationException
     */
    //在执行shiro 的 login 时就会执行这个方法,解析token获取uuid,进而获取当前登录用户的信息,并存储
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken jwtToken) throws AuthenticationException {
        JwtToken jwt = (JwtToken) jwtToken;
        String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
        User user = userService.getUserInfoByUid(userId);
        if(user == null){
            throw new UnknownAccountException("用户不存在");
        } else if(!Constants.UserStatus.NORMAL.getCode().equals(user.getStatus())){
            throw new LockedAccountException("用户已被封禁或被删除,请联系管理员处理!");
        }
        AccountProfile accountProfile = new AccountProfile();
        BeanUtil.copyProperties(user, accountProfile);
        accountProfile.setUuid(userId);
        return new SimpleAuthenticationInfo(accountProfile, jwt.getCredentials(), getName());
    }

    /**
     * 获取用户权限信息,执行授权逻辑
     * @param principalCollection
     * @return
     */
    //这个方法是在判断权限和角色的时候才会进行,具体的来说及比如说我们访问一个被 @RequiresPermissions 注解标识的handler,呢么就会执行这个方法,然后存储权限信息于缓存,如果你设置缓存的话,接着就会从缓存的权限信息中判断是否有权限执行
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        AccountProfile accountProfile = (AccountProfile) principalCollection.getPrimaryPrincipal();
        // 角色权限表
        List<String> permissionsNameList= new LinkedList<>();
        // 用户角色表
        List<String> roleNameList= new LinkedList<>();
        List<Role> roleList = userRoleService.getRolesByUid(accountProfile.getId());
        roleNameList = roleList.stream()
                .map(Role::getName)
                .collect(Collectors.toList());
        for(Role role : roleList){
            for(Auth auth : roleAuthService.getAuthsByRoleId(role.getId())){
                permissionsNameList.add(auth.getPermission());
            }
        }
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(roleNameList);
        authorizationInfo.addStringPermissions(permissionsNameList);
        return authorizationInfo;
    }
    // 注意需要支持我们 JWTtoken,
    @Override
    public boolean supports(AuthenticationToken token) {
//        log.info("Checking token support: {}", token.getClass().getName());
        return token instanceof JwtToken;
    }
}

JWT_TOKEN

//需要实现 AuthenticationToken 这个接口
public class JwtToken implements AuthenticationToken{
    String token;

    public JwtToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return this.token;
    }

    @Override
    public Object getCredentials() {
        return this.token;
    }
}

编写过滤器

// 需要继承AuthenticatingFilter
@Component
@Slf4j(topic = "haustoj")
public class JwtFilter extends AuthenticatingFilter {

    @Resource
    private JwtUtils jwtUtils;
    // 我这里是要实现外部缓存,shiro也支持内部缓存
    @Resource
    private RedisUtils redisUtils;

    /**
     * 判断是否允许访问
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    //判断当前请求是否允许访问,我这里是判断是否为公开接口,如果是则通过,否则拒绝并交给 onAccessAllowed 继续处理
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        httpRequest.setAttribute("org.springframework.web.util.ServletRequestPathUtils.PATH",
                RequestPath.parse(httpRequest.getRequestURI(), httpRequest.getContextPath()));

        try {
            WebApplicationContext ctx = RequestContextUtils.findWebApplicationContext(httpRequest);
            RequestMappingHandlerMapping mapping = ctx.getBean(
                    "requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
            HandlerExecutionChain handler = mapping.getHandler(httpRequest);

            // 如果没有找到处理器,允许访问(由其他过滤器处理)
            if (handler == null) {
                return true;
            }

            Object handlerObj = handler.getHandler();
            // 如果处理器不是HandlerMethod类型,允许访问
            if (!(handlerObj instanceof HandlerMethod)) {
                return true;
            }

            HandlerMethod handlerClazz = (HandlerMethod) handlerObj;
            // 判断请求是否访问的是公共接口,如果拥有@AnonApi注解则不再走登录认证,直接访问controller对应的方法
            AnonApi anonApi = ServiceContextUtils.getAnnotation(handlerClazz.getMethod(),
                    handlerClazz.getBeanType(),
                    AnonApi.class);
            // 有AnonApi注解,说明无需登录就可以访问

            if (anonApi != null) {

                // 即使api标记了不用登录,但如果请求头携带了token,可以尝试着进行登录验证。
                String jwt = httpRequest.getHeader("Authorization");
                if (jwt != null && !"null".equals(jwt)) {
                    try {
                        Claims claim = jwtUtils.getClaimByToken(jwt);
                        if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                            // 如果已经过期,则不进行登录尝试
                            return true;
                        }
                        String userId = claim.getSubject();
                        boolean hasToken = jwtUtils.hasToken(userId);
                        // 缓存中不存在,说明了token失效,则不进行登录尝试
                        if (!hasToken) {
                            return true;
                        }
                        AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
                        if (userRolesVo == null) {
                            // 尝试手动登录
                            JwtToken jwtToken = new JwtToken(jwt);
                            SecurityUtils.getSubject().login(jwtToken);
                        }
                    } catch (Exception ignored) {
                        // 即使出错,也不影响正常访问无鉴权接口
                    }
                }
                return true;
            } else {
                // 没有@AnonApi注解,需要进行认证
                return false;
            }
        } catch (Exception e) {
            // 出现异常时,默认需要认证
            return false;
        }
    }

    /** 
     * @param servletRequest
     * @param servletResponse
     * @return AuthenticationToken
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 获取 token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StrUtil.isBlank(jwt)) {
            return null;
        }
        return new JwtToken(jwt);
    }

    // 这里就是处理非公开接口,也就是判断用户的令牌是否合法
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String token = httpRequest.getHeader("Authorization");
        if (StrUtil.isBlank(token)) {
            return this.onLoginFailure(null,
                    new AuthenticationException("缺少认证令牌"), request, response);
        } else {
            // 验证JWT token有效性
            Claims claim = jwtUtils.getClaimByToken(token);
            if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                return this.onLoginFailure(null,
                        new AuthenticationException("登录状态已失效,请重新登录!"), request, response);
            }

            try {
                // 手动登录,这里很关键,这里就会将用户的角色、权限的等认证信息存储于缓存中
                boolean loginResult = executeLogin(request, response);
                Subject subject = SecurityUtils.getSubject();
                log.info("认证后Subject状态 - isAuthenticated: {}, principal: {}",
                        subject.isAuthenticated(), subject.getPrincipal());

                if (!subject.isAuthenticated()) {
                    log.warn("认证失败,Subject未正确设置");
                    return false;
                }

                return loginResult;
            } catch (Exception e) {
                log.error("认证过程中发生异常", e);
                return this.onLoginFailure(null,
                        new AuthenticationException("认证失败"), request, response);
            }
        }
    }

    /**
     * 刷新Token,并更新token到前端
     *
     * @param request
     * @param userId
     * @param response
     * @return
     */
    private void refreshToken(HttpServletRequest request, HttpServletResponse response, String userId) throws IOException {
        String requestId = UUID.randomUUID().toString();
        boolean locked = redisUtils.getLock(ShiroConstant.SHIRO_TOKEN_LOCK + userId, 20, requestId);// 获取锁20s
        if (locked) {
            String newToken = jwtUtils.generateToken(userId);
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Authorization", newToken); //放到信息头部
            response.setHeader("Access-Control-Expose-Headers", "Refresh-Token,Authorization,Url-Type"); //让前端可用访问
            response.setHeader("Url-Type", request.getHeader("Url-Type")); // 为了前端能区别请求来源
            response.setHeader("Refresh-Token", "true"); //告知前端需要刷新token
        }
        redisUtils.releaseLock(ShiroConstant.SHIRO_TOKEN_LOCK + userId, requestId);
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        returnErrorResponse(request, response, e, ResultStatus.ACCESS_DENIED);
        return false;
    }

    private void returnErrorResponse(ServletRequest request, ServletResponse response, Exception e, ResultStatus resultStatus) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            CommonResult<Void> result = CommonResult.errorResponse(throwable.getMessage(), resultStatus);
            String json = JSONUtil.toJsonStr(result);
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.setHeader("Access-Control-Expose-Headers", "Refresh-Token,Authorization,Url-Type"); //让前端可用访问
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Url-Type", httpRequest.getHeader("Url-Type")); // 为了前端能区别请求来源
            httpResponse.setStatus(resultStatus.getCode());
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(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"));
        httpServletResponse.setHeader("Access-Control-Expose-Headers",
                "Refresh-Token,Authorization,Url-Type,Content-disposition,Content-Type"); //让前端可用访问
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

配置外部缓存(Redis)

public class ShiroCache<K,V> implements Cache<K,V> {
    private Long cacheLiveTime;
    private String cacheKeyPrefix;
    private RedisUtils redisUtils;
    private ObjectMapper objectMapper;
    public ShiroCache(Long cacheLiveTime, String cacheKeyPrefix, RedisUtils redisUtils,ObjectMapper objectMapper) {
        this.cacheLiveTime = cacheLiveTime;
        this.cacheKeyPrefix = cacheKeyPrefix;
        this.redisUtils = redisUtils;
        this.objectMapper = objectMapper;
    }

    /**
     * 缓存的key名称获取为shiro:cache:account
     *
     * @param key
     */
    private String getKey(K key) {
        String userId;
        if (key instanceof PrincipalCollection) {
            AccountProfile accountProfile = (AccountProfile) ((PrincipalCollection) key).getPrimaryPrincipal();
            userId = accountProfile.getId();
        } else {
            userId = key.toString();
        }
        return this.cacheKeyPrefix + userId;
    }

    /** 
     * @param k
     * @return V
     * @throws CacheException
     */
    @Override
    public V get(K k) throws CacheException {
        try {
            V v = (V) fromJson((String) this.redisUtils.get(getKey(k)), SimpleAuthorizationInfo.class);
            return v;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public V put(K k, V v) throws CacheException {
        try {
            String json = (String) this.toJson(v);
            this.redisUtils.set(getKey(k), json, cacheLiveTime);
            return v;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    @Override
    public V remove(K k) throws CacheException {
        if(!this.redisUtils.hasKey(getKey(k))){
            return null;
        }
        redisUtils.del(getKey(k));
        return null;
    }

    @Override
    public void clear() throws CacheException {
        Set<String> keys = this.redisUtils.keys(this.cacheKeyPrefix + "*");
        if (null != keys && keys.size() > 0) {
            this.redisUtils.del(keys);
        }
    }

    @Override
    public int size() {
        Set<String> keys = this.redisUtils.keys(this.cacheKeyPrefix + "*");
        return keys.size();
    }

    @Override
    public Set<K> keys() {
        return (Set<K>) this.redisUtils.keys(this.cacheKeyPrefix + "*");
    }

    @Override
    public Collection<V> values() {
        Set<String> keys = this.redisUtils.keys(this.cacheKeyPrefix + "*");
        if(CollectionUtils.isEmpty( keys)){
            return Collections.emptySet();
        }else{
            List<Object> values = new ArrayList<>(keys.size());
            Iterator iterator = keys.iterator();
            while(iterator.hasNext()){
                String value = (String) this.redisUtils.get((String) iterator.next());
                if(value != null){
                    values.add(value);
                }
            }
            return (Collection<V>) values;
        }
    }
    // 为了让数据在redis中为json格式,不占用过多内存
    private String toJson(Object obj) throws Exception {
        if (obj instanceof SimpleAuthorizationInfo) {
            SimpleAuthorizationInfo info = (SimpleAuthorizationInfo) obj;
            Map<String, Object> map = new HashMap<>();
            map.put("roles", info.getRoles());
            map.put("stringPermissions", info.getStringPermissions());
            map.put("objectPermissions", info.getObjectPermissions());
            return objectMapper.writeValueAsString(map);
        }
        return objectMapper.writeValueAsString(obj);
    }
    private <T> T fromJson(String json, Class<T> clazz) throws Exception {
        if (json == null || json.isEmpty()) {
            return null; // 或者抛出业务友好的异常
        }
        try {
            return objectMapper.readValue(json, clazz);
        } catch (Exception e) {
            throw new RuntimeException("JSON解析失败", e);
        }
    }
}

配置缓存管理

@Data
public class ShiroCacheManager implements CacheManager {
    private Long cacheLiveTime;
    private String cacheKeyPrefix;
    private RedisUtils redisUtils;
    private ObjectMapper objectMapper;

    /** 
     * @param s
     * @return Cache<K, V>
     * @throws CacheException
     */
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new ShiroCache<K, V>(cacheLiveTime, cacheKeyPrefix, redisUtils, objectMapper);
    }
}
我打算法竞赛,真的假的。
最后更新于 2025-11-23