shiro基于角色URL进行鉴权

前言

shiro基于URL进行鉴权,网上有很多,但是多数都是copy不排版,眼睛都看花了,还不如自己看看源码。
2021年1月14日21:23:49最新的shiro是1.7,使用时发现了首次访问的一个bug,然后我使用1.5.3
环境springboot 2.3.7.RELEASE + thymeleaf + shiro 1.5.3

	<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring-boot-starter</artifactId>
      <version>1.5.3</version>
    </dependency>

自定义过滤
自定义过滤

一、登录

我并没有用到继承AuthorizingRealm来实现登录,自己进行手动验证、加密

  //我们不使用shiro的密码认证,在此验证
  //自己认证, 账号密码认证 采用 MD5 + 随机盐salt
  //密码加密方式:密码第二位插入盐 再md5加密,例如密码是123,盐是Po*-,那么加盐后是1Po*-23 再md5
  @ApiOperation("登录")
  @PostMapping("login")
  @ResponseBody
  public ResponseResult login(String username, String password) {
    Assert.notBlank(username, "账号不能为空!");
    Assert.notBlank(username, "密码不能为空!");
    try {
      //查询数据库
      SUser user = userService.getUserByUsername(username);
      if (user == null) {
        throw new UnknownAccountException("账号不存在");
      }
      if (user.getStatus() != 1) {
        return new ResponseResult(HttpCode.FAIL, "当前账号无效或被停止使用!");
      }
      if (!CommonUtils.passwordToMD5(password, user.getSalt()).equals(user.getPassword())) {
        throw new IncorrectCredentialsException("密码错误!");
      }

      //通过安全工具类获取主体,初始化时自定注入了
      Subject subject = SecurityUtils.getSubject();
      //创建token
      UsernamePasswordToken token = new UsernamePasswordToken(username, password);
      subject.login(token);
      //信息存会话中
      User info = new User();
      info.setId(user.getId());
      info.setUsername(user.getUsername());
      info.setNickname(user.getNickname());
      request.getSession().setAttribute("user", info);
    } catch (UnknownAccountException e) {
      log.error("账号不存在!");
      return new ResponseResult(HttpCode.FAIL, e.getMessage());
    } catch (IncorrectCredentialsException e) {
      log.error("无效的凭据,密码错误!");
      return new ResponseResult(HttpCode.FAIL, "无效的凭据,密码错误!");
    } catch (LockedAccountException e) {
      log.error("当前账号被锁定,无法登录");
      return new ResponseResult(HttpCode.FAIL, "当前账号被锁定,无法登录");
    } catch (AuthenticationException e) {
      log.error("授权过程中的异常:{}", e.getMessage());
      return new ResponseResult(HttpCode.FAIL, "登录异常" + e.getMessage());
    }

    //登录后,返回原访问页面
    SavedRequest savedRequest = WebUtils.getSavedRequest(request);
    if (savedRequest != null) {
      // 登录前url
      return new ResponseResult(HttpCode.OK, "登录成功!", savedRequest.getRequestUrl());
    }
    String referer = request.getParameter("referer");
    if (StrUtil.isBlank(referer)) {
      referer = "/";
    } else if (referer.contains("/register")) {
      referer = "/";
    }
    return new ResponseResult(HttpCode.OK, "登录成功!", referer);
  }

二、自定义过滤

查询当前URL需要哪些角色,没有就走到401返回JSON

import com.example.shirodemo.service.PermissionService;
import java.util.List;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * @author 绫小路
 * @date 2021/1/10 19:54
 * @description
 */
public class PermissionFilter extends AccessControlFilter {

  private static final String UN_RESPONSE_MESSAGE = "{\"timestamp\":%d,\"code\":1,\"message\":\"未授权资源\"}";

  @Autowired
  private PermissionService permissionService;

  @Override
  protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    Subject subject = SecurityUtils.getSubject();
    //判断是否登录
    if (!subject.isAuthenticated()) {
      // 通过阅读源码,AccessControlFilter的protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response)
      // 才是跳转到登录的正确姿势,网上大多数姿势采用重定向,采用的重定向无法保存访问记录,登录后无法回到原访问页面
      saveRequestAndRedirectToLogin(request, response);
    }
    HttpServletRequest httpRequest = WebUtils.toHttp(request);
    String uri = httpRequest.getRequestURI();
    //获取访问此URL的角色
    List<String> roles = permissionService.getPermissionRole(uri);
    if (!roles.isEmpty()) {
      if (subject.hasRoles(roles).length > 0) {//有此角色,放行
        return true;
      }
    }

    //401 返回json数据,此处可灵活使用 HttpServletRequest HttpServletResponse 转发到你想要的页面
    HttpServletResponse httpResponse = WebUtils.toHttp(response);
    httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    httpResponse.setHeader("Content-type", "application/json;charset=utf-8");
    httpResponse.setCharacterEncoding("UTF-8");//防止中文乱码
    httpResponse.getWriter().write(String.format(UN_RESPONSE_MESSAGE, System.currentTimeMillis()));
    return false;
  }

  @Override
  protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    return false;
  }
}

三、配置

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.example.shirodemo.config.custom.PermissionFilter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 绫小路
 * @date 2021/1/10 16:55
 * @description
 */
@Configuration
public class ShiroConfig {


  //1 SecurityManager 流程控制
  @Bean
  public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("customAuthorizingRealm")CustomAuthorizingRealm customAuthorizingRealm) {
    DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    defaultWebSecurityManager.setRealm(customAuthorizingRealm);
    //do something...
    return defaultWebSecurityManager;
  }

  //2 ShiroFilterFactoryBean 请求过滤器
  @Bean
  public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    //添加Shiro内置过滤器
    /**
     * Shiro内置过滤器,可以实现权限相关的拦截器
     *    常用的过滤器:
     *       anon: 无需认证(登录)可以访问
     *       authc: 必须认证才可以访问
     *       user: 如果使用rememberMe的功能可以直接访问
     *       perms: 该资源必须得到资源权限才可以访问
     *       role: 该资源必须得到角色权限才可以访问
     */
    Map<String, String> filterMap = new HashMap<String, String>();
    //授权过滤器
    //注意:当前授权拦截后,shiro会自动跳转到未授权页面
    //perms括号中的内容是权限的值
//    filterMap.put("/add", "perms[user:add]");
    filterMap.put("/admin/**", "authc");

    filterMap.put("/swagger-ui/**", "authc");
    filterMap.put("/v2/**", "authc");

    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

    //登录页面
    shiroFilterFactoryBean.setLoginUrl("/login");

    //自定义过滤
    Map<String, Filter> filter = new HashMap<>();
    //添加自定义过滤器
    filter.put("authc", permissionFilter());
    shiroFilterFactoryBean.setFilters(filter);
    return shiroFilterFactoryBean;
  }

  /**
   * 注入自定义过滤器
   */
  @Bean
  public PermissionFilter permissionFilter() {
    return new PermissionFilter();
  }
}

CustomAuthorizingRealm.java

import com.example.shirodemo.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author 绫小路
 * @date 2021/1/10 16:58
 * @description
 */
@Component //交给spring托管
public class CustomAuthorizingRealm extends AuthorizingRealm {

  @Autowired
  private UserService userService;

  //授权,每次访问都会授权一次,不缓存会对数据库造成压力
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    System.out.println(11111);
    return null;
  }

  //认证, 账号密码认证 采用 MD5 + 随机盐salt
  //密码加密方式:密码第二位插入盐 再md5加密,例如密码是123,盐是Po*-,那么加盐后是1Po*-23 再md5
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //因为在controller中做了认证,这里直接返回即可
    return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), this.getName());
  }
}

效果图

正常访问:
在这里插入图片描述
登录后未授权访问
在这里插入图片描述

posted @ 2022-09-16 00:09  凌康  阅读(201)  评论(0编辑  收藏  举报