【框架专题】工具组件——Shiro+Jwt权限搭建

本文深入探讨了Shiro权限系统的设计思想与实现细节,包括无状态登录、Token认证流程、权限信息生成、登录信息注入及匹配器认证比较等核心概念。通过分析Shiro的源码疑惑,解释了executeLogin如何整合登录信息以及权限验证前如何确保登录信息完整。

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

本章的设计思想是,所有登录都是无状态的,利用token在redis中认证,所以要关掉shiro的session,然后因为shiro在做权限校验的时候需要在subject(存在每一次请求的ThreadLocal里面)拿出用户信息,所以需要做一个登录信息的注入,不然就会报出用户未认证的异常,必须补充登录信息

权限系统——理解——shiro内置的数据封装

SimpleAuthenticationInfo

用于封装用户是身份信息+认证信息,认证信息是用于调用匹配器做检验的核心关键

public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, SaltedAuthenticationInfo {
  	protected PrincipalCollection principals;//用户的身份信息,用于存储在本地拿出用户信息的数据
    protected Object credentials;//用户的认证信息,用于校验合法
    protected ByteSource credentialsSalt;//信息加密盐
     /*
      常用构造方法(1)
      @principal:/用户的身份信息,用于存储在本地拿出用户信息的数据
      @hashedCredentials:用户的认证信息,用于校验合法
      @credentialsSalt:用户身份信息需要的盐
      @realmName:每个reaml配置不同的,以便于用户信息根据reaml的不同分key存储
      */
     public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {
        this.principals = new SimplePrincipalCollection(principal, realmName);
        this.credentials = hashedCredentials;
        this.credentialsSalt = credentialsSalt;
    }
     /*
      常用构造方法(2)
      @principal:用户身份信息
      @credentials:用户认证信息
      @realmName:每个reaml配置不同的,以便于用户信息根据reaml的不同分key存储
      */
    public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
        this.principals = new SimplePrincipalCollection(principal, realmName);
        this.credentials = credentials;
    }
}

SimplePrincipalCollection

用户自己提交的身份信息

public class SimplePrincipalCollection implements MutablePrincipalCollection {
   /*
     不同Reaml,她的principal会用不同的key存起来,一个key对应一个set
    */
    private Map<String, Set> realmPrincipals;
      /*
      针对数据类型做处理
     */
    public SimplePrincipalCollection(Object principal, String realmName) {
    
        if (principal instanceof Collection) {
            addAll((Collection) principal, realmName);
        } else {
            add(principal, realmName);
        }
    }
    /*
      将数据添加进hashmap
     */
     public void add(Object principal, String realmName) {
        if (realmName == null) {
            throw new IllegalArgumentException("realmName argument cannot be null.");
        }
        if (principal == null) {
            throw new IllegalArgumentException("principal argument cannot be null.");
        }
        this.cachedToString = null;
        getPrincipalsLazy(realmName).add(principal);
    }
    /*
      懒加载hashMap
     */
    protected Collection getPrincipalsLazy(String realmName) {
        if (realmPrincipals == null) {
            realmPrincipals = new LinkedHashMap<String, Set>();
        }
        Set principals = realmPrincipals.get(realmName);
        if (principals == null) {
            principals = new LinkedHashSet();
            realmPrincipals.put(realmName, principals);
        }
        return principals;
    }
}

权限系统——操作——整体配置

maven依赖

       <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>

ShiroSessionManager

@Component("sessionManager")
public class ShiroSessionManager extends DefaultWebSessionManager {
    /**
     * @初始化
     * 解决自定义sessionId
     */
    public ShiroSessionManager(){
        super.setGlobalSessionTimeout(43200000);
        super.setSessionValidationSchedulerEnabled(true);
        super.setDeleteInvalidSessions(true);
        super.setSessionIdCookie(new SimpleCookie("sessionId"));
    }
}

ShiroBaseConfig

@Configuration
public class ShiroBaseConfig {
    /**
     * @Shiro最终集成
     * 加入shiroUserRealm
     * 加入sessionManager
     */
    @Bean(name="securityManager")
    public SessionsSecurityManager securityManager(DefaultWebSessionManager sessionManager,
                                                   ShiroUserRealm shiroUserRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroUserRealm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }
    /**
     * @注册——Shiro专用的Aop增强器
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * @Spring开启Aop
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        autoProxyCreator.setProxyTargetClass(true);
        return autoProxyCreator;
    }
}

权限系统——操作——利用Filter完成token校验

/**
 * @主要是token鉴权
 * 主要替代shiro内部的登录状态验证机制,提前先完成验证
 */
@Slf4j
@WebFilter(urlPatterns = "/**")
@Configuration
@Order(-1)
public class TokenVaildFilter implements Filter {
    public static final String swagger_url = "swagger";
    private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(
            Arrays.asList(
                    "/v1/login/mobilephonelogin",
                    "/v1/login/sendsms",
                    "/v2/api-docs",
                    "/public/getToken",
                    "/public/getApproveToken",
                    "/public/getTestToken"
                    )));

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
        /**
         * @(0)关闭shiro的session状态存储
         */
        httpServletRequest.setAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED, Boolean.FALSE);

        /**
         * @(1)swageer请求或者白名单跳过
         */
        String servletPath = httpServletRequest.getServletPath();
        boolean allowedPath = ALLOWED_PATHS.contains(servletPath);
        boolean isSwaggerUrl = servletPath.indexOf(swagger_url) > -1;
        if (isSwaggerUrl ||!allowedPath){
            filterChain.doFilter(httpServletRequest, servletResponse);
        }
        /**
         * @(2)校验token,并将数据加入到threadLocal里面
         */
        CombineUserInfo combineUserInfo = JwtUtil.parseToken(TokenUtils.getRequestToken(servletRequest));
        if(JwtUtil.isVerify(combineUserInfo)){
            ShiroUtil.createUserInfoOnThread(httpServletRequest,httpServletResponse,combineUserInfo.getUserInfo());
            filterChain.doFilter(httpServletRequest, servletResponse);
        }
        ShiroUtil.failResponse(servletRequest,servletResponse);
    }
    @Override
    public void destroy() {
        ShiroUtil.destroyUserInfoOnThread();
    }

}

权限系统——操作——利用Filter完成登录信息注入

需要重新shiro内置的token实体类,调用executeLogin方法才能用户信息合并到当前Subject中,用于权限校验时的基础判断

/**
 * @shiro登录状态适配器
 * shiro的session仅在登录机完成来的认证,在分布式环境下,其他机器则会丢失认证,我们进行权限校验的时候就会返回null
 */
@Order(Integer.MAX_VALUE)
public class ShiroLoginApadterFilter extends AuthenticatingFilter {
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        UserInfo userInfo = TokenUtils.getRequestUserInfo(request);
        return new OAuth2Token(TokenUtils.getRequestToken(request), userInfo);
    }
    /**
     * @登录状态特殊跳过,比如白名单
     * 如果已经进入了这个,则证明token是合法的或者是白名单的url
     * 返回false则会进入登录状态补充进入onAccessDenied
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
       /**
         * @白名单的跳过
         */
        String token = TokenUtils.getRequestToken(request);
        UserInfo userInfo = (UserInfo)request.getAttribute(UserInfo.class.getSimpleName());
        if (StringUtils.isBlank(token) || userInfo == null) {
            return true;
        }
        /**
         * @需要进行登录状态数据的补充,最后调用onAccessDenied
         */
        return false;
    }
    /**
     * @登录状态数据的补充
     * shiro权限验证时会首先判断用户是否登录
     * 首先必须调用executeLogin,最后会调用createToken拿到用户信息完成状态数据的补充
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        boolean executeLogin = executeLogin(request, response);
        if (!executeLogin) {
            ShiroUtil.failResponse(request, response);
        }
        return executeLogin;
    }
    /**
     * @登录失败则回调
     * 返回一个二进制json数据给前端
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        ShiroUtil.failResponse(request,response);
        return false;
    }
}

权限系统——操作——利用Realm 生成真实信息

@Slf4j
@Component
public class ShiroUserRealm extends AuthorizingRealm{
    @Autowired
    RedisUtil redisUtil;
    /**
     * @注入密码匹配器
     * 由于身份认证已经在最前面的token拦截中,借用redis完成认证,所以直接调用空匹配器返回ture就不借用shiro完成认证了
     * 如果是第一次登陆则用单独开启一个服务先匹配密码再生成token,也不借用shiro了
     */
    public ShiroUserRealm(@Autowired ShiroEmptyCredentialMatcher shiroEmptyCredentialMatcher) {
        super();
        super.setCredentialsMatcher(shiroEmptyCredentialMatcher);
    }
    /**
     * @生成认证信息的模板方法
     * 先生成用户真实的完整信息,包括认证部分(一般是利用用户的token信息数据库查询)
     * (1)用于调用匹配器匹配token中的认证信息和真实用户信息的比较
     * (2)查出完整的用户身份信息,封装在本地缓存中
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UserInfo baseInputVO = (UserInfo) authenticationToken.getPrincipal();
        String accessToken = (String) authenticationToken.getCredentials();
        return new SimpleAuthenticationInfo(baseInputVO, accessToken, getName());
    }

    /**
     * @生成权限信息 的模板方法
     * 拿到用户所有的principalCollection,就是所有reaml的身份信息
     * 用于身份信息去redis拿出所有菜单、角色
     * 最后用于aop去匹配和比较
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo auth = new SimpleAuthorizationInfo();
        UserInfo inputVO = (UserInfo) principals.getPrimaryPrincipal();
        Set<String> roleId = null;
        Set<String> auths = null;
        if (inputVO != null) {
            roleId = inputVO.getRoleId();
            if (CollectionUtils.isEmpty(roleId)) {
                return auth;
            }

            String authsCacheKey = AuthCacheKey.getAuthsCacheKey(inputVO.getCorpId());
            Map<Object, Object> roleMenus = redisUtil.getHashEntries(authsCacheKey);

            if (roleMenus.size() != 0) {
                Set<String> finalRoleId = roleId;
                auths = roleMenus.entrySet().stream()
                        .filter(map -> (finalRoleId.contains(map.getKey())))
                        .map(o -> o.getValue())
                        .flatMap(set -> ((HashSet<String>) set).stream())
                        .collect(Collectors.toSet());
            } else {
                ResponseVO<Set<String>> responseVO = sysAuthInfoRealmApi.findAllAuthByRole(inputVO);
                if (responseVO != null) {
                    if (responseVO.getCode() == ResponseCode.OK.value()) {
                        auths = responseVO.getData();
                    }
                }
            }
        }
        auth.setRoles(roleId); // 保存所有的角色
        auth.setStringPermissions(auths); // 保存所有的权限
        return auth;
    }
}

权限系统——操作——利用匹配器完成认证比较

Token已经完成合法验证,不需要匹配

@Component
public class ShiroEmptyCredentialMatcher extends SimpleCredentialsMatcher {
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        return true;
    }
}

权限系统——工具类

工具——jwt工具类

工具类,用于解析和校验token的合法性

public class JwtUtil {
    public static RedisUtil redisUtil;
    public static String jwtSecretKey = "userJwt";
    public static String tokenPrifix = "__tokenPrifix";

    /**
     * @签发token
     * 生成一个clientToken可以运算得到有效载荷
     */
    public static String createJWT(UserInfo user) {
        long now = System.currentTimeMillis();
        /**
         * 【创建有效载荷】
         * 并存人redis,设置过期时间作为token的失效时间
         */
        String redisToken = user.getEmployeeName() + ":" + UUID.randomUUID().toString();
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("employeeId", user.getEmployeeId());
        map.put("employeeName", user.getEmployeeName());
        map.put("employeeOrgCode", user.getEmployeeOrgCode());
        map.put("employeeOrgName", user.getEmployeeOrgCode());
        map.put("redisToken",redisToken);
        redisUtil.setForTimeMS(tokenPrifix+user.getEmployeeId(),user,500000);//决定token有效时长
        /**
         * 【加密有效载荷】
         * 最后生成一个可以摘要得到有效载荷的clientToken
         */
        JwtBuilder builder = Jwts.builder()
                .setClaims(map)//有效载荷
                .setId(UUID.randomUUID().toString())//JWT的唯一标识,一次性token,从而回避重放攻击
                .setIssuedAt(new Date(now))//签发时间
                .setSubject(user.getEmployeeId().toString())//签发主体,用户唯一标识
                .signWith(SignatureAlgorithm.HS256, jwtSecretKey);//签发算法+密钥
        return builder.compact();
    }
    public static CombineUserInfo parseToken(String token) {
        CombineUserInfo combineUserInfo =new CombineUserInfo();
        try {
            if(token==null){
                return null;
            }
            Claims claims = Jwts.parser()//设置签名的秘钥
                    .setSigningKey(jwtSecretKey)//设置需要解析的jwt
                    .parseClaimsJws(token).getBody();

            combineUserInfo.setClaims(claims);
            combineUserInfo.setUserInfo(new UserInfo(){{
                setEmployeeId((String) claims.get("employeeId"));
                setEmployeeName((String) claims.get("employeeName"));
                setEmployeeOrgCode((String) claims.get("employeeOrgCode"));
                setEmployeeOrgName((String) claims.get("employeeOrgName"));
            }});
            return combineUserInfo ;
        }catch (Exception e){
            log.error(e.getMessage());
            return combineUserInfo;
        }
    }
    /**
     * @校验token
     *  1、拿到有效载荷中的redisToken再去redis里面拿最终数据
     * 2、判断是不是空值、比较两边数据是否一致来判断token的有效性
     */
    public static Boolean isVerify(Claims claims) {
        /**
         * @(1)redis真实数据获取
         * 在有效载荷中取出redis数据作为对比基础,如果用是否是null做判断,可能会导致正好存在非当前用户的redis数据
         */
        String redisToken = claims.get("redisToken").toString();
        Object obj = redisUtil.get(redisToken);
        if(obj==null){
            throw  new ApiException(401,"未登录");
        }
        /**
         * @(2)token数据比对比redis数据
         * 证明不是恰好算出一个合法的token,还必须一致匹配
         */
        UserInfo userInfo=(UserInfo)obj;
        if (claims.get("employeeId").equals(userInfo.getEmployeeId())) {
            return true;
        }
        return false;
    }

    public static boolean isVerify(CombineUserInfo combineUserInfo) {
        Claims claims = combineUserInfo.getClaims();
        /**
         * @(1)redis真实数据获取
         * 在有效载荷中取出redis数据作为对比基础,如果用是否是null做判断,可能会导致正好存在非当前用户的redis数据
         */
        String redisToken = claims.get("redisToken").toString();
        Object obj = redisUtil.get(redisToken);
        if(obj==null){
            throw  new ApiException(401,"未登录");
        }
        /**
         * @(2)token数据比对比redis数据
         * 证明不是恰好算出一个合法的token,还必须一致匹配
         */
        UserInfo userInfo=(UserInfo)obj;
        if (claims.get("employeeId").equals(userInfo.getEmployeeId())) {
            return true;
        }
        return false;
    }
}

同时封装两种数据

public class CombineUserInfo {
   private UserInfo userInfo;
   private Claims claims;
}

静态工具类注入spring里面的redisUitls、

@Component
public class StaticVarivaleProcesser {

    @Autowired
    RedisUtil redisUtil;

    @PostConstruct
    public void processStaticVarivaleRef() {
        JwtUtil.redisUtil = redisUtil;
    }
}

工具——token工具类

主要用于在取出requesy域中的原token和解析后的token

public class TokenUtils {
    public static String getRequestToken(ServletRequest request) {
        HttpServletRequest httpServletRequest=(HttpServletRequest)request;
        String token = httpServletRequest.getHeader("Content-token");
        if (StringUtils.isBlank(token)) {
            token = httpServletRequest.getParameter("Content-token");
        }
        return token;
    }
    public static UserInfo getRequestUserInfo(ServletRequest request) {
        HttpServletRequest httpServletRequest=(HttpServletRequest)request;
        UserInfo userInfo = (UserInfo) httpServletRequest.getAttribute(UserInfo.class.getSimpleName());
        return userInfo;
    }
}

工具——shiro工具类

主要用于业务代码中取出request域里面的解析后的token

public class ShiroUtil {
    public static void failResponse(ServletRequest request, ServletResponse response) {
        response.setContentType("application/json;charset=utf-8");
        try {
            ResponseVO responseDTO = new ResponseVO(ResponseCodeEnum.UNLOGIN_EXCEPTION);
            String json = JackSonUtil.serializeDTO(responseDTO);
            response.getWriter().print(json);
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }
    private static final ThreadLocal<ContextProvider> USER_INFO = new ThreadLocal<>();
    /**
     * @UserInfo相关方法
     */
    public static UserInfo getUserInfo() {
        ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
        if (contextProvider == null) {
            throw new ApiException("没有使用@SessionInject注解,注入数据");
        }
        return contextProvider.getUserInfo();
    }
    /**
     * @生命周期相关方法
     */
    public static void createUserInfoOnThread(HttpServletRequest request, HttpServletResponse response, UserInfo userInfo) {
        USER_INFO.set(new ContextProvider(request, response, userInfo));
    }
    public static void destroyUserInfoOnThread() {
        USER_INFO.remove();
    }
    /**
     * @Servlet相关方法
     */
    public static HttpServletRequest getRequest() {
        ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
        return contextProvider.getRequest();
    }

    public static HttpServletResponse getResponse() {
        ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
        return contextProvider.getResponse();
    }
}

封装存储在ThreadLocal中的数据

@Getter
class ContextProvider {
    private HttpServletRequest request;
    private HttpServletResponse response;
    private UserInfo userInfo;
    ContextProvider(HttpServletRequest request, HttpServletResponse response, UserInfo userInfo) {
        this.request = request;
        this.response = response;
        this.userInfo = userInfo;
    }
}

权限系统——源码疑惑

excuteLoign如何将登录信息合并到DelegatingSubject的PrincipalCollection principals中?

首先必须清楚Subject只存在ThreadLocal中,如果开启了有状态session认证则会存在于内存的MemorySessionDAO
这个操作主要考虑的将用户的AuthenticationToken转化成AuthenticationInfo,最后存进subject里面供权限认证的时候去用

详细分析可以看我的源码文章:https://blog.youkuaiyun.com/weixin_44275259/article/details/107939737

权限验证前,如何校验登录信息完整?

public class DelegatingSubject implements Subject {
   /*
     所有权限验证前都会调用assertAuthzCheckPossible要,验证用户登录数据是否完整
    */
   public void checkPermission(String permission) throws AuthorizationException {
        assertAuthzCheckPossible();
        securityManager.checkPermission(getPrincipals(), permission);
    }
   protected void assertAuthzCheckPossible() throws AuthorizationException {
       if (!hasPrincipals()) {
            String msg = "This subject is anonymous - it does not have any identifying principals and " +
                    "authorization operations require an identity to check against.  A Subject instance will " +
                    "acquire these identifying principals automatically after a successful login is performed " +
                    "be executing " + Subject.class.getName() + ".login(AuthenticationToken) or when 'Remember Me' " +
                    "functionality is enabled by the SecurityManager.  This exception can also occur when a " +
                    "previously logged-in Subject has logged out which " +
                    "makes it anonymous again.  Because an identity is currently not known due to any of these " +
                    "conditions, authorization is denied.";
            throw new UnauthenticatedException(msg);
        }
   }
    protected boolean hasPrincipals() {
        return !isEmpty(getPrincipals());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值