前段时间公司需求,写了一个RABC模式的权限管理,因为项目是前后端分离的,shiro默认是不支持前后端分离的写法的,它是跳转到配置的页面,看了很多人的写法都是比较复杂的,我特地的简化了,写了一个比较好用的前后端分离的写法。
项目:springboot 项目 + mybatis-plus + redis
shrio相关的jar:
pom文件:
<!-- https://mvnrepository.com/artifact/org.crazycake/shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.0</version>
</dependency>
import com.xxx.shiro.MySessionManager;
import com.xxx.shiro.MyShiroRealm;
import com.xxx.shiro.RedisSessionDao;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
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;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
private long expireTime = 3600;
/**
* 配置核心安全管理器
*
* @return
*/
@Bean
public SecurityManager securityManager(MyShiroRealm customizeRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customizeRealm);
// 取消Cookie中的RememberMe参数
securityManager.setRememberMeManager(null);
// 配置自定义Session管理器
securityManager.setSessionManager(mySessionManager());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录,无权限是跳转的路径
// shiroFilterFactoryBean.setLoginUrl("/");
// 登录成功后跳转的路径
// shiroFilterFactoryBean.setSuccessUrl("/");
// 错误页面,认证不通过跳转
// shiroFilterFactoryBean.setUnauthorizedUrl("/");
// 配置拦截规则
Map<String, String> filterChainMap = new LinkedHashMap<>();
// 登录页面和登录请求路径需要放行
filterChainMap.put("/admin/login", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
//注入权限管理
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 自定义Session管理器
*/
@Bean
public MySessionManager mySessionManager() {
MySessionManager mySessionManager = new MySessionManager();
// 配置自定义SessionDao
mySessionManager.setSessionDAO(redisSessionDao());
mySessionManager.setGlobalSessionTimeout(expireTime * 1000);
return mySessionManager;
}
@Bean
public RedisSessionDao redisSessionDao() {
return new RedisSessionDao(expireTime);
}
}
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.hx.xxx.dto.MenuDTO;
import com.hx.xxx.dto.RoleMenuDTO;
import com.hx.xxx.dto.UserDTO;
import com.hx.xxx.dto.UserRoleDTO;
import com.xxx.service.*;
import com.xxx.utils.Misc;
import org.apache.shiro.SecurityUtils;
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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Component
public class MyShiroRealm extends AuthorizingRealm {
private final UserService userService;
private final RoleService roleService;
private final UserRoleService userRoleService;
private final RoleMenuService roleMenuService;
private final MenuService menuService;
@Autowired
public MyShiroRealm(UserService userService, RoleService roleService, UserRoleService userRoleService, RoleMenuService roleMenuService, MenuService menuService) {
this.userService = userService;
this.roleService = roleService;
this.userRoleService = userRoleService;
this.roleMenuService = roleMenuService;
this.menuService = menuService;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
Set<String> permsSet = new HashSet<>();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
UserDTO userDTO = userService.get(Wrappers.<UserDTO>lambdaQuery().eq(UserDTO::getUsername, name));
if (Misc.isNotNull(userDTO)) {
//查询用户对应的角色
List<UserRoleDTO> userRoleList = userRoleService.list(Wrappers.<UserRoleDTO>lambdaQuery()
.eq(UserRoleDTO::getUserId, userDTO.getId()));
for (UserRoleDTO userRoleDTO : userRoleList) {
if (Misc.isNotNull(userRoleDTO)) {
//查询角色下对应的菜单
List<RoleMenuDTO> roleMenuDTOList = roleMenuService.list(Wrappers.<RoleMenuDTO>lambdaQuery().eq(RoleMenuDTO::getRoleId, userRoleDTO.getRoleId()));
Set<Long> menuIdSet = new HashSet<>();
menuIdSet = roleMenuDTOList.stream().map(RoleMenuDTO::getMenuId).collect(Collectors.toSet());
//获取菜单对应的操作权限
if (Misc.isNotNull(menuIdSet)) {
List<MenuDTO> menuDTOList = menuService.list(menuIdSet);
permsSet = menuDTOList.stream().map(MenuDTO::getPerms).collect(Collectors.toSet());
}
}
}
}
info.setStringPermissions(permsSet);
// 可以直接放入角色
// info.setRoles();
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号.
// 1.从主体传过来的认证信息中,获得用户名
String username = (String) token.getPrincipal();
// 2.通过用户名到数据库中获取凭证
UserDTO userDTO = userService.get(Wrappers.<UserDTO>lambdaQuery().eq(UserDTO::getUsername, username));
userDTO.setSessionId(SecurityUtils.getSubject().getSession().getId().toString());
// 设置最后登录时间
userDTO.setUpdatedTime(new Date());
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username, userDTO.getPassword(), getName()
);
return authenticationInfo;
}
}
package xxx.shiro;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
/**
* RedisSessionDao,以Redis持久化方式做Session共享,无需配置即可支持集群
*/
public class RedisSessionDao extends AbstractSessionDAO {
/**
* Session超时时间(秒)
*/
private long expireTime;
public RedisSessionDao(long expireTime) {
this.expireTime = expireTime;
}
@Autowired
private RedisTemplate redisTemplate;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return sessionId == null ? null : (Session) redisTemplate.opsForValue().get(sessionId);
}
@Override
public void update(Session session) throws UnknownSessionException {
if (session != null && session.getId() != null) {
session.setTimeout(expireTime * 1000);
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
}
}
@Override
public void delete(Session session) {
if (session != null && session.getId() != null) {
redisTemplate.opsForValue().getOperations().delete(session.getId());
}
}
@Override
public Collection<Session> getActiveSessions() {
return redisTemplate.keys("*");
}
}
package xxx.shiro;
import com.xxx.utils.IPUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
/**
* 自定义Session管理器,以Token做会话保持,同时兼容Cookie方式
* 默认从请求头中获取Token,获取不到会继续读取Cookie
*/
public class MySessionManager extends DefaultWebSessionManager {
private Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 前端ajax请求headers中须传入Authorization的值,也能兼容Cookie方式
*/
private static final String AUTHORIZATION = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
String ipAddr = IPUtils.getIpAddr(httpServletRequest);
String requestUri = httpServletRequest.getRequestURI();
log.debug(">>>>>>>>>>>>>>>>>>>>> MySessionManager.getSessionId(), IP: {}, URI: {}", ipAddr, requestUri);
// 先从请求头中获取 Authorization
Serializable sessionId = httpServletRequest.getHeader(AUTHORIZATION);
// 如果请求头中有 Authorization 则其值为sessionId
if (sessionId != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
log.debug(">>>>>>>>>> MySessionManager.getSessionId(), 从Header中获取sessionId: " + sessionId);
return sessionId;
}
// 否则按默认规则从 cookie 取sessionId
else {
sessionId = super.getSessionId(request, response);
log.debug(">>>>>>>>>> MySessionManager.getSessionId(), 使用默认模式从cookie获取sessionID为: " + sessionId);
return sessionId;
}
}
}
/**
* 菜单列表
* @return
*/
@RequiresPermissions("menu:list")
@GetMapping("/list")
public Result<Page<MenuDTO>> menuList(PageBO pageBO) {
return Result.ok();
}
@RestControllerAdvice
public class GlobalDefaultExceptionHandler {
/**
* AuthorizationException 类捕获
*/
@ExceptionHandler(value = AuthorizationException.class)
public Result handlerAuthorizationException() {
return Result.failed(ErrMsgConsts.USER_UNPOWER);
}
}
直接捕获 AuthorizationException异常,这个异常就表示用户无权限,你可以在这里直接返回json格式的异常就ok了。
总结: 我找了很多人写的教程里面,大部分人都是写了一个baseController基础类,然后全部的controller 去继承,我是觉得很繁琐,所以直接全局的异常类去直接捕获,然后在抛出。
上面的一些写法,我也是看了好几个gitlab上面的项目去实现的,很多东西都是雷同的,我写的比较简单,感兴趣的可以去尝试一下shiro的自定义过滤器去实现返回json格式。