Springboot+Shiro实现动态权限验证

本文将带你从0开始搭建一个Springboot+shiro动态权限控制


首先你得知道什么是Shiro?

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解API,你可以快速、轻松地获取任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
官网:[点我进入]

开发环境:

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

<!-- springboot -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/>
</parent>

 

1.编写LoginController

    @PostMapping("/login")
    @ApiOperation("登录系统")
    @Log(module = "登录系统", description = "登陆系统")
    public ResultResponse login(@RequestBody LoginInoutDTO inoutDTO) {
        if (StringUtils.isBlank(inoutDTO.getUsername()) || StringUtils.isBlank(inoutDTO.getPassword())) {
            return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "参数不全", null);
        }

        //当前登录的用户
        Subject currentUser = SecurityUtils.getSubject();
        // 如果这个用户没有登录,进行登录功能
        if (!currentUser.isAuthenticated()) {
            try {
                // 验证身份和登陆
                UsernamePasswordToken token = new UsernamePasswordToken(inoutDTO.getUsername(), inoutDTO.getPassword());
                currentUser.login(token);
                permissionsConfig.updatePermission();
            } catch (UnknownAccountException e) {
                return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "此账号不存在!", null);
            } catch (IncorrectCredentialsException e) {
                return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "用户名或者密码错误,请重试!", null);
            } catch (LockedAccountException e) {
                return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "该账号已被锁定,请联系管理员!", null);
            } catch (AuthenticationException e) {
                return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "未知错误,请联系管理员!", null);
            }
        }
        return new ResultResponse(true, ErrorDetail.RC_0000000.getIndex(), "登录成功", userInfo);
        }
    }

 

2.自定义Realm

/**
 * Created by Eagga_Lo on 2019/11/26 14:19
 */
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    @Autowired
    RoleService roleService;

    @Autowired
    MenuService menuService;

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("--------------开始授权操作--------------");

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principalCollection.getPrimaryPrincipal();
        log.info("user:{}", user.toString());

        Set<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();

        try {
            List<Role> roleList = roleService.selectRoleByUserId(user.getById());
            for (Role role : roleList) {
                rolesSet.add(role.getRoleId());
                List<XtZzjgMenu> menuList = menuService.selectMenuByRoleId(role.getRoleId());
                for (XtZzjgMenu menu : menuList) {
                    permsSet.add(menu.getResource());
                }
            }

            log.info("--------------当前用户的角色:{}--------------", rolesSet);
            log.info("--------------当前角色的权限:{}--------------", permsSet);

            //将查到的权限和角色分别传入authorizationInfo中
            authorizationInfo.setStringPermissions(permsSet);
            authorizationInfo.setRoles(rolesSet);
            log.info("--------------赋予角色和权限成功---------------");
        } catch (Exception e) {
            e.printStackTrace();
            log.info("--------------赋予角色和权限失败{}--------------", e.getMessage());
        }
        return authorizationInfo;

    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("---------------开始认证操作---------------");

        UsernamePasswordToken tokenInfo = (UsernamePasswordToken) authenticationToken;
        // 获取用户输入的账号
        String username = tokenInfo.getUsername();
        // 获取用户输入的密码
        String password = String.valueOf(tokenInfo.getPassword());

        //数据库中的user
        User user = userService.selectByPrimaryKey(username);
    
        if (user == null) {
            return null;
        }

        // 判断是否为冻结状态
        if ("Y".equals(user.getSfsc())) {
            throw new LockedAccountException();
        }

        /*
         * 进行验证 -> 注:shiro会自动验证密码
         * 参数1:principal -> 放对象就可以在页面任意地方拿到该对象里面的值
         * 参数2:hashedCredentials -> 密码
         * 参数3:credentialsSalt -> 设置盐值
         * 参数4:realmName -> 自定义的Realm
         */
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
        log.info("--------------- 认证完毕! ---------------");
        return authenticationInfo;
    }


}

 

3.前后端分离需要过滤OPTIONS请求,否则可能出现302错误。

@Slf4j
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {

    public MyFormAuthenticationFilter() {
        super();
    }

    /**
     * 过滤OPTIONS请求  不配置可能会报302错误
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //过滤OPTIONS请求
        String method = ((HttpServletRequest) request).getMethod().toUpperCase();
        if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
            return true;
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                return executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
                return true;
            }
        } else {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
            if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
                resp.setStatus(HttpStatus.OK.value());
                return true;
            }

            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
                        "Authentication url [" + getLoginUrl() + "]");
            }
            //前端Ajax请求时requestHeader里面带一些参数,用于判断是否是前端的请求
            String test = req.getHeader("test");
            if (test != null || req.getHeader("wkcheck") != null) {
                //前端Ajax请求,则不会重定向
                resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
                resp.setHeader("Access-Control-Allow-Credentials", "true");
                resp.setContentType("application/json; charset=utf-8");
                resp.setCharacterEncoding("UTF-8");
                PrintWriter out = resp.getWriter();
                JSONObject result = new JSONObject();
                result.put("message", "登录失效");
                result.put("resultCode", 1000);
                out.println(result);
                out.flush();
                out.close();
            } else {
                saveRequestAndRedirectToLogin(request, response);
            }
            return false;
        }
    }
}

 

4.自定义过滤器继承PermissionsAuthorizationFilter解决由于权限不足直接报302错误。

/**
 * 自定义url过滤权限
 */
@Slf4j
public class MyPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {

    /**
     * 解决权限不足302问题
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestUrl = httpRequest.getServletPath();
        log.info("请求的url:  " + requestUrl);

        // 检查是否拥有访问权限
        Subject subject = this.getSubject(request, response);
        if (subject.getPrincipal() == null) {
            this.saveRequestAndRedirectToLogin(request, response);
        } else {
            // 转换成http的请求和响应
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;

            // 获取请求头的值
            String header = req.getHeader("X-Requested-With");
            // ajax 的请求头里有X-Requested-With: XMLHttpRequest 正常请求没有
            if ("XMLHttpRequest".equals(header)) {
                resp.setContentType("text/json,charset=UTF-8");
                ResultResponse resultResponse = new ResultResponse(false, "302", "权限不足", null);
                log.info("----------权限不足---------");
                resp.getWriter().print(resultResponse);
            } else {
                //正常请求
                String unauthorizedUrl = this.getUnauthorizedUrl();
                if (StringUtils.hasText(unauthorizedUrl)) {
                    WebUtils.issueRedirect(request, response, unauthorizedUrl);
                } else {
                    WebUtils.toHttp(response).sendError(401);
                }
            }
        }
        return false;
    }
}

 

5.自定义过滤器继承AuthorizationFilter,原生过滤器必须满足所有角色都有才允许访问。

/**
 * 原生的角色过滤器RolesAuthorizationFilter 默认是必须同时满足roles[admin,guest]才有权限
 */
@Slf4j
public class MyRolesAuthorizationFilter extends AuthorizationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) throws Exception {
        Subject subject = getSubject(req, resp);
        String[] rolesArray = (String[]) mappedValue;
        // 没有角色限制,有权限访问
        if (rolesArray == null || rolesArray.length == 0) {
            return true;
        }
        for (String s : rolesArray) {
            //若当前用户是rolesArray中的任何一个,则有权限访问
            if (subject.hasRole(s)) {
                return true;
            }
        }
        return false;
    }

}

 

6.自定义Session管理器 继承DefaultWebSessionManager

注意:此处没有采用Redis作为session缓存,让前端加请求头进行session传递。请求头必须与“AUTHORIZATION ”的值一致。

/**
 * Created by Eagga_Lo on 2019/11/26 14:16
 */

@Slf4j
public class MySessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "Authorization";

    /**
     * 采用自定义SessionManager的方式自己来管理sessionid的获取
     * 这样前端需要做的就是每次请求,要把后端传给它的sessionid即token
     * 放到请求头里key为Authorization,value为后台传过来的token
     * 然后用自定义的SessionManager获取就可以了
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //前端ajax的headers中必须传入Authorization的值
        String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中有 Authorization 则其值为sessionId
        if (!StringUtils.isEmpty(sessionId)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}

7.自定义ShiroConfig配置类

/**
 * Created by Eagga_Lo on 2019/11/26 14:14
 */

@Configuration
public class ShiroConfig {

    @Autowired
    MenuMapper menuMapper;

    @Autowired
    RoleMapper roleMapper;

    @Autowired
    PermissionsConfig permissionsConfig;

    /**
     * 请求拦截
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 自定义过滤器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("MyAuthc", new MyFormAuthenticationFilter());
        filterMap.put("MyPerms", new MyPermissionsAuthorizationFilter());
        filterMap.put("MyRoles", new MyRolesAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        // 登录的路径: 如果你没有登录则会跳到这个页面中 - 如果没有设置值则会默认跳转到工程根目录下的"/login.jsp"页面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/unLogin");
        // 设置没有权限时跳转的url
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");

        // 加载权限
        shiroFilterFactoryBean.setFilterChainDefinitionMap(permissionsConfig.loadFilterChainDefinitions());
        return shiroFilterFactoryBean;
    }

    /**
     * @Description: SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(ShiroRealm());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    /**
     * 自定义认证
     *
     * @return MyShiroRealm
     * @Title: myShiroRealm
     * @Description: ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,负责用户的认证和权限的处理
     */
    @Bean
    public ShiroRealm ShiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

    /**
     * 密码凭证匹配器,作为自定义认证的基础 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 )
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //加密算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        //加密的次数
        hashedCredentialsMatcher.setHashIterations(1024);
        //此处的设置,true加密用的hex编码,false用的base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * 自定义sessionManager,用户的唯一标识,即Token或Authorization的认证
     */
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setGlobalSessionTimeout(-1L);
        return mySessionManager;
    }

}

 

8.自定义实现动态加载权限

最后一步也是最重要的一步,动态加载FilterChainDefinitionMap,也就是shiroFilterFactoryBean.setFilterChainDefinitionMap(permissionsConfig.loadFilterChainDefinitions())这一步。

之所以需要动态刷新权限,是因为当我们新增了用户,新增角色,然后重新分配权限,用这个账户直接登录时,这个用户和角色并没有被初始化到FilterChainDefinitionMap里面,大家debug这一步的时候想必也知道只有在开启服务的时候会调用一次,所以这时候就需要动态加载FilterChainDefinitionMap,在增删改查涉及到角色,权限等地方调用进行刷新权限。刷新之后FilterChainDefinitionMap里就会加载出我们动态查询出的所有权限了。

/**
 * Created by Eagga_Lo on 2019/12/23 16:41
 */
@Service
@Slf4j
public class PermissionsConfig {

    @Autowired
    MenuMapper menuMapper;

    @Autowired
    RoleMapper roleMapper;

    public Map<String, String> loadFilterChainDefinitions() {
        // 权限控制
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置不会被拦截的链接 顺序判断
        //authc 必须认证通过才可以访问;
        //anon 可以匿名访问
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        // 登录
        filterChainDefinitionMap.put("/login", "anon");
        // 退出登录
        filterChainDefinitionMap.put("/logout", "anon");
        // 未登录
        filterChainDefinitionMap.put("/unLogin", "anon");
        // 未授权
        filterChainDefinitionMap.put("/unauth", "anon");

        // 自定义权限
        List<Menu> permissionList = menuMapper.selectAll();
        if (!CollectionUtils.isEmpty(permissionList)) {
            permissionList.forEach(e -> {
                if (StringUtils.isNotBlank(e.getUrl())) {
                    // 根据url查询相关联的角色名,拼接自定义的角色权限
                    List<Role> roleList = roleMapper.selectRoleByMenuId(e.getMenuId());
                    StringJoiner MyRoles = new StringJoiner(",", "MyRoles[", "]");
                    if (!CollectionUtils.isEmpty(roleList)) {
                        roleList.forEach(f -> {
                            MyRoles.add(f.getRoleDes());
                        });
                    }
                    filterChainDefinitionMap.put(e.getUrl(), "MyAuthc," + MyRoles.toString() + ",MyPerms[" + e.getResource() + "]");
                }
            });
        }
        filterChainDefinitionMap.put("/**", "MyAuthc");
        return filterChainDefinitionMap;
    }

    /**
     * 更新权限,解决需要重启tomcat才能生效权限的问题
     */
    public void updatePermission() {
        log.info("-----------------正在更新权限...--------------");
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            ServletContext servletContext = request.getSession().getServletContext();
                //注意bean的名字不要写错了 是自己的shiroFilter名称
            AbstractShiroFilter shiroFilter = (AbstractShiroFilter) WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext).getBean("shiroFilter");
            synchronized (shiroFilter) {
                // 获取过滤管理器
                PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
                DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
                // 清空初始权限配置
                manager.getFilterChains().clear();
                // 重新获取资源
                Map<String, String> chains = loadFilterChainDefinitions();
                for (Map.Entry<String, String> entry : chains.entrySet()) {
                    String url = entry.getKey();
                    String chainDefinition = entry.getValue().trim().replace(" ", "");
                    manager.createChain(url, chainDefinition);
                }
                log.info("-----------------更新权限成功!!--------------");
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

 

以上就是全部过程,有不对或者不足的地方欢迎指出。有不明白的地方可以在评论区回复或者私信。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值