踩坑篇-Nacos+Sping-gateway+shiro实现分布式认证权限框架

(159) 2024-03-29 14:01:01

踩坑篇-Nacos+Sping-gateway+shiro实现分布式认证权限框架

  • 创作背景
    • 基本功能
    • 版本说明
    • 认证中心实现篇
    • 网关实现篇
    • 效果呈现
  • 简单的解释

创作背景

我们公司属于通信行业,做出的产品用户群体比较小众,所以不太追求像互联网公司那样的Tps,Qps之类的指标,单体springboot其实已经够用了。但是身为技术人嘛,总要有所追求。
另外一个原因,也是最重要的原因:shiro很少被用来做为微服务的安全框架,这让我替这个非常简单易用的框架鸣不,平开发这个,也是来证明微服务安全框架并不是只能选spring security。另外微服务中用shiro来实现安全框架的文章几乎,这对广大用户shiro很不友好。

基本功能

  1. Nacos作为注册中心和配置中心(包括logback配置)
  2. spring-gateway作为网关,具备熔断,负载,限流,统一操作日志,认证权限拦截等功能
  3. shiro+redis作为认证授权服务 oaa,提供为网关feign接口,用来验证权限

版本说明

网上贴的很多稳定版本,大家也可以自由选取,版本不要太低,不方便以后拓展。一下是我实践的一套,目前这套依赖完美契合:
nacos 1.1.4(网传最稳定版本);
springcloud Hoxton.SR8
springcloud alibaba 2.2.2.RELEASE
springboot 2.3.6.RELEASE

认证中心实现篇

1.登录类

package com.eastcom.oaa.controller;

import com.alibaba.fastjson.JSON;
import common.common.exception.ExceptionCast;
import common.common.util.GetRealIPUtil;
import common.entity.*;
import common.pojo.User;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * @description: 登陆
 */

@CrossOrigin
@RestController
@RequestMapping("/login")
public class LoginController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private StringRedisTemplate stringRedisTemplate;



    /**
     * @param user_name
     * @param Pwd
     * @return
     * @throws Exception
     */
    @PostMapping("/loginByPwd")
    public Result loginPCByPwd(@RequestParam String user_name, @RequestParam String Pwd, HttpServletRequest request) {

        // 认证 Subject:主体
        Subject subject = SecurityUtils.getSubject();
        // 根据用户信息,组成用户令牌token
        UsernamePasswordToken Token = new UsernamePasswordToken(user_name, Pwd, false);
        subject.login(Token);
        User user1 = (User) subject.getPrincipal();
        QueryResult queryResult = new QueryResult();
        String token = subject.getSession().getId().toString();
        queryResult.setData(user1)
        queryResult.setToken(token)
        return new Result(CommonCode.LOG_OUT_SUCCESS,queryResult);
    }

    /**
     * @Author mazl
     * @Description:
     * @Return
     */
    @RequestMapping("/logout")
    public Result logout(@RequestParam String user_name){
        SecurityUtils.getSubject().logout();
       
        QueryResult queryResult = new QueryResult();
        return new Result(CommonCode.LOG_OUT_SUCCESS,queryResult);
    }

    @GetMapping("/isPermitted")
    public boolean isPermitted(@RequestParam String requestURI,@RequestParam String token){
        System.out.println("isPermitted");
        logger.info("进入授权,访问路径:{}",requestURI);
//        //方案一,不灵活(对于get请求,不允许在url通过/拼接参数,可以通过?拼接)、不易排查问题
        boolean permitted =SecurityUtils.getSubject().isPermitted(requestURI);
        System.out.println("是否授权:"+permitted);
        return permitted;
        //方案二,不灵活,且无权限时报的异常无法捕获
//        subject.checkPermissions(requestURI);

        //方案一,过于灵活
//      User parse = (User) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
//        List<String> resources = parse.getResources();
//        if("admin".equals(parse.getRole_name())){
//            System.out.println("放行管理员");
//            return true;
//
//        }else
//            if(requestURI.endsWith(".js") || requestURI.endsWith(".css")|| requestURI.endsWith(".gif")){
//            System.out.println("放行资源路径");
//            return true;
//        }else {
//            //开关
//            boolean flag = false;
//            for(String resource:resources){ //判断当前访问的 URI 是否在功能数据中包含
//                if(requestURI.indexOf(resource) !=-1){
//                    flag = true;
//                    System.out.println(requestURI+"--->"+resource);
//                    break;
//                }
//            }
//            return flag;
//        }
    }
}

上面登录登出很一般的代码,最后一个要说一下,结合shiro给网关提供的权限接口,具体有三种判断权限的方式,用哪种可以根据需求自由选取。

2.shirorealm 玩shiro的不用我说了吧

package com.eastcom.oaa.config;


import com.eastcom.oaa.mapper.RmAuResourceMapper;
import com.eastcom.oaa.service.Shiro.ShiroService;
import com.eastcom.oaa.service.Shiro.impl.ShiroServiceImpl;
import common.common.exception.ExceptionCast;
import common.entity.CommonCode;
import common.pojo.User;
import org.apache.shiro.SecurityUtils;
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.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collection;
import java.util.List;


public class ShiroRealm extends AuthorizingRealm{

    private Logger logger = LoggerFactory.getLogger(this.getClass());


    @Autowired
    public  ShiroService shiroService;
    @Autowired
    public  RmAuResourceMapper rmAuResourceMapper;


    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken){
        System.out.println("进入shiroRealm");
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        // 从数据库获取对应用户名密码的用户
        System.out.println(token.toString());
        System.out.println(token.getUsername());
        User user = shiroService.selecUserByAccount(token.getUsername());
        if (user != null) {
            if(user.getUser_state()==2){
                ExceptionCast.shiroCast(CommonCode.FORBIDDEN,"ShiroRealm","doGetAuthenticationInfo");
            }

            user.setResources(shiroService.getResourceUrlByUserName(user.getUser_name()));
            this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());

            //单用户登录
            //处理session
            DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
            SessionManager sessionManager = (SessionManager) securityManager.getSessionManager();
            //获取当前已登录的用户session列表
            Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();
            User temp;
            for(Session session : sessions) {

                Object attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
                if (attribute == null) {
                    continue;
                }

                temp = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
                logger.info("当前已登录的用户user: "+temp.toString());
                //清除该用户以前登录时保存的session,强制退出
                if (token.getUsername().equals(temp.getUser_name())) {
                    sessionManager.getSessionDAO().delete(session);
                }
            }

            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    user, //用户
                    user.getUser_pasw(), //密码
                    getName()  //realm name
            );
            return authenticationInfo;
        }else {
            ExceptionCast.shiroCast(CommonCode.NULL_USER,"ShiroRealm","doGetAuthenticationInfo");
        }
        throw new AuthenticationException();
    }
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
//        // TODO Auto-generated method stub
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //获取登录时查询到的用户对象
        User user = (User)arg0.getPrimaryPrincipal();
        //把用户的所有权限添加到info中
        logger.info("当前正在授权的用户信息: "+user.toString());
        List<String> resources = user.getResources();
        if(resources!=null&&resources.size()>0){
            for(String URL : user.getResources()){
                info.addStringPermission(URL);
            }
        }
       //后台只需要做登录认证即可,权限部分由前端动态遍历授权页面即可

        return info;
    }

    /**
     * 管理员授权
     * @param principals
     * @param permission
     * @return
     */
    @Override
    public  boolean isPermitted(PrincipalCollection principals, String permission){
        User user = (User)principals.getPrimaryPrincipal();
        return isAdmin(user)||super.isPermitted(principals,permission);
    }
    @Override
    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        User user = (User)principals.getPrimaryPrincipal();
        return isAdmin(user)||super.hasRole(principals,roleIdentifier);
    }

    public boolean isAdmin( User user) {
        return "admin".equals(user.getRole_name());
    }
}

3.shiroConfig 同上,玩过单体springboot+shiro的这套应该不难,由于认证和权限放在网关做,所以此处拦截路径完全放开

package com.eastcom.oaa.config;


import org.apache.shiro.mgt.SecurityManager;
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.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.CrossOrigin;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
@CrossOrigin
public class ShiroConfigOaa {

    /**
     * 注入配置文件属性
     */
    @Value("${spring.redis.host}")
    private String host;//地址
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;//过期时间
    @Value("${spring.redis.password}")
    private String password;//地址
    //将自己的验证方式加入容器
    @Bean
    public ShiroRealm ShiroRealm() {

        ShiroRealm myShiroRealm = new ShiroRealm();
        myShiroRealm.setCredentialsMatcher(credentialsMatcher());
        return myShiroRealm;
    }
    @Bean
    public MyCredentialsMatcher credentialsMatcher() {

        return new MyCredentialsMatcher();
    }
    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;

    }



    //权限管理,配置主要是Realm的管理认证
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(ShiroRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix("SPRINGBOOT_CACHE:");   //设置前缀
        return redisCacheManager;
    }

    /**
     * Session User
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public SessionManager sessionManager() {
        SimpleCookie simpleCookie = new SimpleCookie("token");
        simpleCookie.setPath("/");
        simpleCookie.setHttpOnly(false);

        SessionManager sessionManager = new SessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setSessionIdCookieEnabled(false);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionIdCookie(simpleCookie);
        sessionManager.setGlobalSessionTimeout(3600 * 1000);//单位毫秒
        return sessionManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setJedisPool(redisPoolFactory());
        redisManager.setPassword(password);
        redisManager.setTimeout(timeout);
        return redisManager;
    }
    @Bean
    public JedisPool redisPoolFactory() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);

        return jedisPool;
    }


    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setKeyPrefix("SPRINGBOOT_SESSION:");
        redisSessionDAO.setExpire(3600);//设置过期时间,单位s
        return redisSessionDAO;
    }

    /***
     * 授权所用配置
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    //加入注解的使用,不加入这个注解不生效
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * Shiro生命周期处理器
     * 此方法需要用static作为修饰词,否则无法通过@Value()注解的方式获取配置文件的值
     *
     */
    @Bean
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


}

4.sessionManager的实现。

package com.eastcom.oaa.config;

import common.entity.UtilCode;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.apache.shiro.web.util.WebUtils;

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

public class SessionManager  extends DefaultWebSessionManager {
    private static final String AUTHORIZATION = UtilCode.TOKEN;

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public SessionManager() {
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //获取请求头,或者请求参数中的token
        String baseid = StringUtils.isEmpty(WebUtils.toHttp(request).getHeader(AUTHORIZATION))
                ? request.getParameter(AUTHORIZATION) : WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        // 如果请求头中有 Token 则其值为sessionId

        if (!StringUtils.isEmpty (baseid)) {

            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, baseid);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);

            return baseid;
        } else {
            // 否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }

    /**
     * 获取session 优化单次请求需要多次访问redis的问题
     *
     * @param sessionKey
     * @return
     * @throws UnknownSessionException
     */
    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);

        ServletRequest request = null;
        if (sessionKey instanceof WebSessionKey) {
            request = ((WebSessionKey) sessionKey).getServletRequest();
        }

        if (request != null && null != sessionId) {
            Object sessionObj = request.getAttribute(sessionId.toString());
            if (sessionObj != null) {
                return (Session) sessionObj;
            }
        }

        Session session = super.retrieveSession(sessionKey);
        if (request != null && null != sessionId) {
            request.setAttribute(sessionId.toString(), session);
        }
        return session;
    }


}

其实微服务整合shiro核心地方到这已经讲完了,不知道你留意到了没~

网关实现篇

1.实现认证中心的授权接口的feign调用

package com.eastcom.springgateway.FeignService;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name="oaa",fallbackFactory = GatewayFeignFallbackFactory.class)
public interface OaaFeign {
    @RequestMapping(method = RequestMethod.GET, value = "/login/isPermitted")
    boolean isPermitted(@RequestParam String requestURI,@RequestParam String token);
}

2.接下来就没什么难的了,实现GlobalFilter, Ordered来自定义过滤,考虑到代码涉密,不贴了。

效果呈现

登录并获取token
踩坑篇-Nacos+Sping-gateway+shiro实现分布式认证权限框架 (https://mushiming.com/)  第1张
调用oaa服务接口,oaa通过feign调用server服务接口
踩坑篇-Nacos+Sping-gateway+shiro实现分布式认证权限框架 (https://mushiming.com/)  第2张
踩坑篇-Nacos+Sping-gateway+shiro实现分布式认证权限框架 (https://mushiming.com/)  第3张

用错误token进行上一步操作
踩坑篇-Nacos+Sping-gateway+shiro实现分布式认证权限框架 (https://mushiming.com/)  第4张
踩坑篇-Nacos+Sping-gateway+shiro实现分布式认证权限框架 (https://mushiming.com/)  第5张

完工

简单的解释

由于做的是政企项目,这套架构可能会在以后投入使用。政企项目上线前都会安全检查的,包括代码联网审查(github,oschina等,csdn我不清楚有没有),所以保险起见,对上面的登录登出代码有删减,其他地方也没有贴完,望大家见谅。有问题可以评论区留言,谢谢大家

THE END

发表回复