Shiro(认证鉴权框架)

核心组件

Shiro的架构主要包括Subject、SecurityManager、Realm和Session等核心组件。【原文链接

  • Subject:Subject代表当前用户,是与应用程序交互的主体。Subject可以执行身份验证、授权和会话管理等操作,是Shiro的核心概念。
  • SecurityManager:SecurityManager是Shiro的核心组件,负责协调各个安全组件之间的交互。SecurityManager管理着所有的Subject,并负责对Subject进行身份验证、授权和会话管理等操作。
  • Realm:Realm是Shiro与应用程序交互的桥梁,负责从数据源中获取用户的身份信息和权限信息。开发人员可以通过实现自定义的Realm来连接不同的数据源,如数据库、LDAP等。
  • Session:Session代表用户的会话,用于存储用户的状态信息。Shiro提供了会话管理功能,可以管理用户的会话状态,包括会话的创建、销毁、超时等。

Shiro认证鉴权流程

  1. 客户端登录成功,生成会话信息保存在服务端(shiro-cache/redis),并返回token(token中携带sessionId信息)给客户端;
  2. 客户端访问服务端时,携带token;
  3. 服务端根据sessionId从缓存(shiro-cache/redis)中拿到会话信息;
  4. 客户端登出时(携带token),清除服务端相关会话信息;

认证鉴权

配置认证鉴权策略

shiro自带的策略:

  • anno: 无需认证就可以访问(匿名会话)
  • authc: 必须认证才能访问
  • user: 必须拥有 记住我 功能才能用
  • perms: 拥有对某个资源的权限才能访问
  • role: 拥有某个角色权限才能访问

本文采用:

  1. 登录/获取验证码接口:无需认证(anno);
  2. 其他接口:全局配置自定义认证策略(添加了 JwtFilter认证过滤器,然后采用 注解方式Controller 层对需要鉴权的接口添加鉴权注解)

认证鉴权方式(请求头)

具体根据自身业务选择:

  1. Cookie
  2. token

本文采用:token

代码实现

shiro全局配置

import cn.com.Constant;
import cn.com.filter.JWTFilter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.*;

@Slf4j
@Configuration
public class ShiroConfig {
    @Value("${shiro.cache.type}")
    private String cacheType ;

    @Value("${shiro.session-timeout}")
    private long tomcatTimeout;

    //shiro-redis提供的
    @Resource
    private RedisSessionDAO redisSessionDAO;

    //shiro-redis提供的
    @Resource
    private RedisCacheManager redisCacheManager;

    /**
     * 管理shiro生命周期使用,默认配置就行了
     */
    @Bean
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 若要使用Shiro注解,需添加如下方法
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * shiro拦截器配置
     *
     */
    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(){
            @Override
            protected FilterChainManager createFilterChainManager() {
                FilterChainManager manager = super.createFilterChainManager();
                // URL携带中文出现400响应,servletPath中文校验bug
                Map<String, Filter> filterMap = manager.getFilters();
                Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
                if (invalidRequestFilter instanceof InvalidRequestFilter) {
                    ((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
                }
                return manager;
            }
        };
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 添加自定义认证过滤器
        LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        // 添加自定义匹配指定路径的过滤器
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        /**
	     *         anno: 无需认证就可以访问
	     *         authc: 必须认证才能访问
	     *         user: 必须拥有 记住我 功能才能用
	     *         perms: 拥有对某个资源的权限才能访问
	     *         role: 拥有某个角色权限才能访问
	     */
        filterChainDefinitionMap.put("/system/login/**","anon");
        filterChainDefinitionMap.put("/system/logout", "anon");
        filterChainDefinitionMap.put("/system/getVerify", "anon");
		// 将过滤器应用于所有接口,自定义认证
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

	/**
	 * 自定义登录认证及授权
	 */
    @Bean
    AccountRealm accountRealm() {
        return new AccountRealm();
    }

	/**
	 * session缓存方式
	 */
    @Bean
    public SessionDAO sessionDAO() {
        if (Constant.CACHE_TYPE_REDIS.equals(cacheType)) {
            return redisSessionDAO;
        } else {
            return new MemorySessionDAO();
        }
    }

    /**
     * shiro session的管理
     */
    @Bean(name = "sessionManager")
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new CustomWebSessionManager();
        sessionManager.setGlobalSessionTimeout(tomcatTimeout * 1000L);
        // 设置SessionId不存于Cookie
        sessionManager.setSessionIdCookieEnabled(false);
        sessionManager.setSessionDAO(sessionDAO());
        Collection<SessionListener> listeners = new ArrayList<>();
        listeners.add(new BDSessionListener());
        sessionManager.setSessionListeners(listeners);
        return sessionManager;
    }

    /**
     * shiro 安全管理器配置,shiro的容器管理包
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        List<Realm> realms = new ArrayList<>();
        realms.add(accountRealm());
        securityManager.setRealms(realms);
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(redisCacheManager);
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
}

自定义会话管理器

import cn.com.common.config.Constant;
import cn.com.common.utils.JWTUtils;
import cn.com.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;

@Component
@Slf4j
public class CustomWebSessionManager extends DefaultWebSessionManager {

    @Resource
    private JWTUtils jwtUtils;

    public MyWebSessionManager(){
        super();
    }

    /**
     * 重写: 从token中获取SessionId
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String sessionId = null;
        HttpServletRequest req = WebUtils.toHttp(request);
        
        String token = req.getHeader("token");
        if(log.isTraceEnabled()){
            log.trace("Token = {}", token);
        }
        // token不为空,从token中取出sessionId
        if(StringUtils.isNotEmpty(token)){
            sessionId = jwtUtils.getSessionIdFromToken(req);
            if(StringUtils.isNotEmpty(sessionId)){
            	// 配置sessionId来源:url
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
                // 标志sessionId是否有效
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
                if(log.isTraceEnabled()){
                    log.trace("getSessionId = {} from Token = {}", sessionId, token);
                }
            }
        }
        // 设置是否启用 URL 重写来传递 。这是一个 Shiro 配置项,具体行为取决于 方法的实现。 `sessionId``isSessionIdUrlRewritingEnabled()`
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
        return sessionId;
    }

    /**
     * Shiro 创建新会话时调用,登录接口将token填充到response中。
     */
    @Override
    protected void onStart(Session session, SessionContext context) {
        if (!WebUtils.isHttp(context)) {
            log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response pair. No session ID cookie will be set.");
        }else{
            HttpServletRequest request = WebUtils.getHttpRequest(context);
            HttpServletResponse response = WebUtils.getHttpResponse(context);
            // 这个地方,shiro会给跳过认证(anno)的接口(形如/login登录接口/getVerity获取验证码接口)创建匿名会话
            Serializable sessionId = session.getId();
            this.storeSessionId(sessionId, request, response);
            request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
        }
    }

    /**
     * 作用: 登录接口,将SessionId生成Token,放入返回的响应头中
     */
    private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
         String uri = request.getRequestURI();
         if (!uri.contains("/system/login/account")) {
             return;
         }
         if (currentId == null) {
	          log.info("subject --> sessionId = null");
	          String msg = "sessionId cannot be null when persisting for subsequent requests.";
	          throw new IllegalArgumentException(msg);
     	 }
         String idString = currentId.toString();
         String token = jwtUtils.generateToken(idString);
         // 允许前端访问自定义请求头
         response.setHeader("Access-Control-Expose-Headers", Constant.HEADER_TOKEN + ",newToken");
         response.setHeader(Constant.HEADER_TOKEN, token);
    }
}

登录认证及授权

import cn.com.common.config.ApplicationContextRegister;
import cn.com.common.config.Constant;
import cn.com.system.bean.perm.Account;
import cn.com.system.dao.MenuMapper;
import cn.com.system.plugins.UserAware;
import cn.com.system.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
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 java.util.List;
import java.util.Set;

@Slf4j
public class AccountRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        /**
         * 多realm场景下这个判断才有用,当前只有一个realm
         * 默认多realm场景下权限点会累加,加上判断后则只有一个realm的权限点(匹配上的)
         */
        if(!principalCollection.getRealmNames().contains(getName())){
            return null;
        }
        Long userId = null;
        List list = principalCollection.asList();
        if (list != null && !list.isEmpty()) {
            userId = ((UserAware)list.get(0)).getUserId();
        }
        MenuMapper menuMapper = ApplicationContextRegister.getBean(MenuMapper.class);
        Set<String> perms = menuMapper.listPermsByUserId(userId);
        log.info("review perms: {}", perms.toString());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(perms);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        String password = new String((char[]) authenticationToken.getCredentials());

        AccountService accountService = ApplicationContextRegister.getBean(AccountService.class);
        // 查询用户信息
        if(!accountService.checkExist(username)){
            throw new UnknownAccountException("Account does not exist ! ");
        }
        Account user = accountService.getByName(username);
        // 账号不存在
        if (user == null) {
            throw new UnknownAccountException("Account or Password is not correct ! ");
        }
        // 密码错误
        if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("Account or Password is not correct ! ");
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }

}

自定义认证过滤器JwtFilter

import cn.com.common.config.ApplicationContextRegister;
import cn.com.common.utils.JWTUtils;
import cn.com.common.utils.StringUtils;
import cn.com.common.config.shiro.ShiroUtil;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
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;

@Component
public class JWTFilter extends BasicHttpAuthenticationFilter {

    /**
     * 前置方法,配置支持跨域
     */
    @Override
    public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        // 允许的来源域名
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        // 是否允许携带凭据(如 cookies)
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        // 允许的http请求方式
        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", "token,newToken");
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return true;
        }
        return super.onPreHandle(request, response, mappedValue);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return super.isAccessAllowed(request, response, mappedValue);
    }

    /**
     * 是否认证失败
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = WebUtils.toHttp(request);
        HttpServletResponse resp = WebUtils.toHttp(response);
        // 从Header里面获取Token
        String token = req.getHeader("token");
        // 如果Header里面token为空,则从参数里面去取
        if(StringUtils.isBlank(token)){
            token = req.getParameter("token");
        }
        // 校验token
        JWTUtils jwtUtils = ApplicationContextRegister.getBean(JWTUtils.class);
        String sessionId = jwtUtils.getSessionIdFromToken(req);
        if(StringUtils.isNotBlank(sessionId) && !ShiroUtil.validateSession(sessionId)){
            return true;
        }
        // 认证失败
        resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      	return false;
    }
}

controller层添加鉴权注解

@Slf4j
@RestController
@RequestMapping("/account")
public class AccountController {
	// 鉴权注解,只有拥有权限[account:get]才能访问该接口
	@RequiresPermissions("account:get")
    @GetMapping("/get")
    BaseResponse<String> getAccount(String accountName) {
    	return null;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值