一、数据库模板设计
在本文中,我们使用RBAC(Role-Based Access Control,基于角色的访问控制)模型设计用户,角色和权限间的关系。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系
一个用户对应一个或者多个角色。
一个角色对应一个或者多个权限。
一个权限对应能够访问对应的API或url资源。
1 . RBAC基本实体关系,Permission类(权限资源) :
/**
* Created by EalenXie on 2019/3/25 11:15.
* <p>
* 权限许可(Permission) 操作 及其能访问url 权限对应一个url地址
*/
@Entity
@Table(name = "system_shiro_permission")
public class Permission extends BaseEntity {
@Column(unique = true)
private String name; //权限名 唯一
@Column(unique = true)
private String url; //访问地址信息 唯一
private String description; //描述信息
//省略getter/setter
}
2 . Role类(用户角色),一个角色拥有一个或者多个权限 :
/**
* Created by EalenXie on 2019/3/25 11:18.
* <p>
* 角色(Role) 角色下面对应多个权限
*/
@Entity
@Table(name = "system_shiro_role")
public class Role extends BaseEntity {
@Column(unique = true)
private String name; //角色名 唯一
private String description; //描述信息
@ManyToMany(fetch= FetchType.EAGER)
private List<Permission> permissions; //一个用户角色对应多个权限
//省略getter/setter
}
3 . User类(用户),一个用户拥有一个或者多个角色 :
/**
* Created by EalenXie on 2019/3/25 11:01.
* <p>
* 用户表(User) 用户下面对应多个角色
*/
@Entity
@Table(name = "system_shiro_user")
public class User extends BaseEntity {
@Column(unique = true)
private String username;//用户名 唯一
private String password;//用户密码
private String passwordSalt;//用户密码加密盐值
@ManyToMany(fetch = FetchType.EAGER)
private List<Role> roles;//用户角色 一个用户可能有一个角色,也可能有 多个角色
//省略getter/setter
}
以上是对应关系的实体,数据库字段根据上述实体创建,这里就不写出来了。设计到的查询根据实际情况自己写,这里主要是讲shiro
二:Shiro整合实现思路
我们先来屡一下思路,实现认证权限功能主要可以归纳为3点:
1.定义一个ShiroConfig配置类,配置 SecurityManager Bean , SecurityManager为Shiro的安全管理器,管理着所有Subject;
2.在ShiroConfig中配置 ShiroFilterFactoryBean ,它是Shiro过滤器工厂类,依赖SecurityManager ;
3.自定义Realm实现类,包含
doGetAuthorizationInfo()
和doGetAuthenticationInfo()
方法
下面我们就来实现这些步骤:
1、定义ShiroConfig配置类
/**
* Shiro配置
*/
@Configuration
public class ShiroConfig {
@Resource
private OAuth3Realm userAuthRealm;
/**
* 配置安全管理器
* @param oAuth3Realm UserRealm
* @return DefaultWebSecurityManager
*/
@Bean
public SecurityManager securityManager(OAuth3Realm oAuth3Realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth3Realm);
return securityManager;
}
/**
* 配置Shiro过滤器工厂
* 配置 资源访问策略 . web应用程序 shiro核心过滤器配置
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, UrlConfig urlConfig) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 注册安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 定义资源访问规则
Map<String, String> filterMap = new LinkedHashMap<String, String>(16);
/*
* 过滤器说明
* anon:不需要认证就可以访问的资源
* authc:需要登录认证才能访问的资源
* perms:需要指定权限才能访问的资源
*/
// 需要登录认证才能访问的资源
//oauth过滤,anon:不需要认证就可以访问的资源
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("oauth3", new OAuth3Filter());
// TODO 增加新的B端用户拦截器
shiroFilterFactoryBean.setFilters(filters);
//排除配置
if (urlConfig.getAnonUrl() != null && !urlConfig.getAnonUrl().isEmpty()) {
for (String anonUrl : urlConfig.getAnonUrl()) {
filterMap.put(anonUrl, "anon");
}
}
//对所有用户认证 对B端访问路由进行拦截
filterMap.put("/**", "oauth3");
//配置 拦截过滤器链
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启shiro 注解支持. 使以下注解能够生效 :
* 需要认证 {@link org.apache.shiro.authz.annotation.RequiresAuthentication RequiresAuthentication}
* 需要用户 {@link org.apache.shiro.authz.annotation.RequiresUser RequiresUser}
* 需要访客 {@link org.apache.shiro.authz.annotation.RequiresGuest RequiresGuest}
* 需要角色 {@link org.apache.shiro.authz.annotation.RequiresRoles RequiresRoles}
* 需要权限 {@link org.apache.shiro.authz.annotation.RequiresPermissions RequiresPermissions}
* 启用shiro注解
*加入注解的使用,不加入这个注解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置shiro中缓存管理器对象 (对象的名字不能写cacheManager,因为spring容器中已经存在一个名字为cacheManager的对象了)
*
* 作用:缓存授权信息,当第一次访问授权时,会调用自定义Realm的获取授权数据的方法,从数据库中查询授权数据,并将其以登录者的Principal为键,存储在缓存中。
* 以后的每次访问授权,就直接从缓存中获取,而不再从数据库中获取。
*/
@Bean
public CacheManager shiroCacheManager() {
return new MemoryConstrainedCacheManager();
}
/**
* 配置记住我管理对象:底层同cookie对象将用户信息写到客户端
*/
@Bean
public RememberMeManager rememberMeManager() {
CookieRememberMeManager cManager = new CookieRememberMeManager();
//配置cookie
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置cookie执行时长(7天)
cookie.setMaxAge(1 * 24 * 60 * 60);
cManager.setCookie(cookie);
return cManager;
}
// @Bean
// public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
// DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// advisorAutoProxyCreator.setProxyTargetClass(true);
// return advisorAutoProxyCreator;
// }
}
注意:(当时笔者遇到的一个小问题,贴出来给大家涨姿势)
注解无效,登录时不会执行验证角色和权限的方法,只会执行登录验证方法,遂查询资料,得知shiro在subject.login(token)方法时不会执行doGetAuthorizationInfo方法,只有在访问到有权限验证的接口时会调用查看权限,于是猜想注解无效,发现shiro的权限注解需要开启才能有用,添加在配置文件中加入
advisorAutoProxyCreator
和getAuthorizationAttributeSourceAdvisor
两个bean开启shiro注解,解决问题。
shiroFilterFactoryBean 的Url配置
可以配置在数据库,也可以配置在配置文件,作者这里配置在配置文件中的
@Data
@Configuration
@ConfigurationProperties(prefix = "amsh")
public class UrlConfig {
/**
* 不需要拦截的url
*/
private List<String> anonUrl;
/**
* openapi分页大小
*/
private Integer openPageSize;
}
通过这种方式排除配置
注意:
1.这里要用LinkedHashMap 保证有序
2.filterChain基于短路机制,即最先匹配原则,
3.像anon、authc等都是Shiro为我们实现的过滤器
附录:
1.Shiro拦截机制表
Filter Name Class Description anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 /static/**=anon
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单的拦截器;如 /**=authc
,如果没有登录会跳到相应的登录页面登录authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP身份验证拦截器 logout org.apache.shiro.web.filter.authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/),示例 /logout=logout
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter 不创建会话拦截器,调用 subject.getSession(false)
不会有什么问题,但是如果subject.getSession(true)
将抛出DisabledSessionException
异常perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例 /user/**=perms["user:create"]
port org.apache.shiro.web.filter.authz.PortFilter 端口拦截器,主要属性 port(80)
:可以通过的端口;示例/test= port[80]
,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter rest风格拦截器,自动根据请求方法构建权限字符串;示例 /users=rest[user]
,会自动拼出user:read,user:create,user:update,user:delete权限字符串进行权限匹配(所有都得匹配,isPermittedAll)roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;示例 /admin/**=roles[admin]
ssl org.apache.shiro.web.filter.authz.SslFilter SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口443;其他和port拦截器一样; user org.apache.shiro.web.filter.authc.UserFilter 用户拦截器,用户已经身份验证/记住我登录的都可;示例 /**=user
oauth2过滤器
-上面配置文件中加入的类- filters.put("oauth3", new OAuth3Filter());
/**
* oauth2过滤器
*/
@Slf4j
public class OAuth3Filter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isEmpty(token)) {
//清理线程变量
ThreadLocalUserInfo.remove();
return null;
}
return new OAuth3Token(token);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = getRequestToken(httpRequest);
if (StringUtils.isEmpty(token)) {
log.info("OAuth3Filter过滤器,onAccessDenied获取token:{}", token);
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=UTF-8");
UnifiedResult result = UnifiedResult.fail(HttpStatus.UNAUTHORIZED.value(), "无效的token");
String json = JSON.toJSONString(result);
httpResponse.getWriter().print(json);
return false;
}
return executeLogin(request, response);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=UTF-8");
try {
//清理线程变量
ThreadLocalUserInfo.remove();
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
UnifiedResult result = UnifiedResult.fail(HttpStatus.UNAUTHORIZED.value(), throwable.getMessage());
String json = JSON.toJSONString(result);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
e1.printStackTrace();
}
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader("auth");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isEmpty(token)) {
token = httpRequest.getParameter("auth");
}
return token;
}
}
2、实现自定义Realm类
自定义Realm类需要继承 AuthorizingRealm 类,实现 doGetAuthorizationInfo()和doGetAuthenticationInfo()方法即可 ,
doGetAuthorizationInfo() 方法是进行授权的方法,获取角色的权限信息
doGetAuthenticationInfo()方法是进行用户认证的方法,验证用户名和密码
/**
* @ClassName MyShiroRealm
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 3:08
* @Version 1.0
*/
@Service
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RolePermissionMapper rolePermissionMapper;
/**
* 获取用户角色和权限
* @param principal
* 授权(验证权限时调用)
* 获取授权信息(把数据库中shiroID加入到shiro中管理,然后如果ctronler层方法加上 @RequiresPermissions注解才会调用这个方法和数组进行比对)
* 只有当需要检测用户权限的时候才会调用此方法
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
if(principal == null){
throw new AuthorizationException("principals should not be null");
}
User userInfo= (User) SecurityUtils.getSubject().getPrincipal();
System.out.println("用户-->"+userInfo.getUsername()+"获取权限中");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//用户获取角色集
List<Role> roleList=userRoleMapper.findByUserName(userInfo.getUsername());
Set<String> roleSet=new HashSet<>();
for (Role r:roleList){
Integer roleId=r.getId();//获取角色id
simpleAuthorizationInfo.addRole(r.getName());//添加角色名字
List<Permission> permissionList=rolePermissionMapper.findByRoleId(roleId);
for (Permission p:permissionList){
//添加权限
simpleAuthorizationInfo.addStringPermission(p.getName());
}
}
System.out.println("角色为-> " + simpleAuthorizationInfo.getRoles());
System.out.println("权限为-> " + simpleAuthorizationInfo.getStringPermissions());
return simpleAuthorizationInfo;
}
/**
* 登录认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户输入的用户名密码
String username= (String) token.getPrincipal();
String password=new String((char[])token.getCredentials());
System.out.println("用户输入--->username:"+username+"-->password:"+password);
//在数据库中查询
User userInfo=userMapper.selectByName(username);
if (userInfo == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
if (!password.equals(userInfo.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, // 用户名
userInfo.getPassword(), // 密码
getName() // realm name
);
return authenticationInfo;
}
}
其中
UnknownAccountException
等异常为Shiro自带异常,Shiro具有丰富的运行时AuthenticationException
层次结构,可以准确指出尝试失败的原因
创建一个UserController.class类
用于处理User类的访问请求,并使用Shiro权限注解控制权限:
/**
* @ClassName UserController
* @Description TODO
* @Author fqCoder
* @Date 2020/3/3 15:14
* @Version 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequiresPermissions("user:queryAll")
@GetMapping("/queryAll")
public String queryAll(){
//只演示框架...功能不实现
return "查询列表";
}
@RequiresPermissions("user:add")
@GetMapping("/add")
public String userAdd(){
return "添加用户";
}
@RequiresPermissions("user:delete")
@GetMapping("/delete")
public String userDelete(){
return "删除用户";
}
}
这只是前后端分离时的shiro验证,不涉及页面的代码
以上就是shiro 整合的操作步骤
三、下面是拓展场景——在项目启动的时候进行全局扫描自动生成权限资源
假如最开始框架设计时,并没有引入权限框架,中途是安测扫描出越权问题,进行补漏。
这时已经实现了很多接口,设计到加权限问题,如果是注解的方式,一个一个加不太现实,那得累死,这时就可以考虑在项目启动的时候进行全局扫描自动生成权限资源
步骤:
1、定义需要进行权限控制的接口-自动扫描生成的资源注解
/**
* 权限注解,用于标识需要权限处理的接口
*
* @author
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Auth {
/**
* 权限id,模块id + 方法id需要唯一
*/
long id();
/**
* 权限名称
*/
String name();
int type() default 1;
/**
* 权限菜单路径
*/
String perssionMenus() default "";
}
定义组件,项目启动时扫描
/**
* @author
*/
@Slf4j
@Component
public class ApplicationStartup implements ApplicationRunner {
@Resource
private IBackendRabcResourceService resourceService;
@Override
public void run(ApplicationArguments args) {
log.info("--------------->【ApplicationStartup接口初始化】-------> 开始初始化扫描所有需要权限的接口资源");
resourceService.initRoleResource();
log.info("--------------->【ApplicationStartup接口初始化完成】------->");
}
}
service层
@Override
public void initRoleResource() {
List<RabcResource> listAll = new LinkedList<>();
List<RabcResource> moduleResourceslist = getAuthModuleResources();
// 扫描并获取所有需要权限处理的接口资源(该方法逻辑写在下面)
List<RabcResource> methodResourceslist = getAuthMethodResources();
// 如果权限资源为空,就不用走后续数据插入步骤
if (CollUtil.isEmpty(moduleResourceslist) && CollUtil.isEmpty(methodResourceslist)) {
return;
}
// 先删除所有操作权限类型的权限资源,待会再新增资源,以实现全量更新(注意哦,数据库中不要设置外键,否则会删除失败)
if (CollUtil.isNotEmpty(moduleResourceslist)) {
log.info("--------------->开始删除已存在的模块资源,进行全量更新");
this.deleteResourceByType(0);
}
if (CollUtil.isNotEmpty(methodResourceslist)) {
log.info("--------------->开始删除已存在的接口资源,进行全量更新");
this.deleteResourceByType(1);
}
listAll.addAll(moduleResourceslist);
listAll.addAll(methodResourceslist);
// 将资源数据批量添加到数据库
this.insertResources(listAll);
List<RabcRoleResource> roleResources = this.retrieveItemChild();
if (CollUtil.isNotEmpty(roleResources)) {
log.info("--------------->开始删除已存在的角色资源,进行全量更新");
this.deleteRoleResourceByType(0);
}
this.insertRoleResource(roleResources);
log.info("--------------->【ApplicationStartup接口初始化完成】------->");
}
/**
* 扫描并返回所有需要权限处理的模块资源
*/
private List<RabcResource> getAuthModuleResources() {
// 接下来要添加到数据库的资源
List<RabcResource> list = new LinkedList<>();
// 拿到所有接口信息,并开始遍历
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingInfoHandlerMapping.getHandlerMethods();
handlerMethods.forEach((info, handlerMethod) -> {
// 拿到类(模块)上的权限注解
Auth moduleAuth = handlerMethod.getBeanType().getAnnotation(Auth.class);
if (moduleAuth == null) {
return;
}
if (0 == moduleAuth.type()) {
String path = info.getPatternsCondition().getPatterns().toArray()[0].toString();
String moduleAuthName = "/" + path.split("/")[1];
RabcResource resource = list.stream().filter(item -> moduleAuthName.equals(item.getPath()))
.findFirst()
.orElse(null);
if (ObjectUtil.isEmpty(resource)) {
// 将权限名、资源路径、资源类型组装成资源对象,并添加集合中
RabcResource resourceEntity = new RabcResource();
resourceEntity.setType(0)
.setPath(moduleAuthName)
.setId(moduleAuth.id())
.setPerssionmenus(moduleAuth.perssionMenus())
.setName(moduleAuth.name());
list.add(resourceEntity);
}
}
});
log.info("【getAuthModuleResources方法】-------> 获取到所有需要权限控制的模块资源 模块数量:{}", list.size());
return list;
}
/**
* 扫描并返回所有需要权限处理的接口资源
*/
private List<RabcResource> getAuthMethodResources() {
// 接下来要添加到数据库的资源
List<RabcResource> list = new LinkedList<>();
// 拿到所有接口信息,并开始遍历
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingInfoHandlerMapping.getHandlerMethods();
handlerMethods.forEach((info, handlerMethod) -> {
// 拿到类(模块)上的权限注解
Auth moduleAuth = handlerMethod.getBeanType().getAnnotation(Auth.class);
// 拿到接口方法上的权限注解
Auth methodAuth = handlerMethod.getMethod().getAnnotation(Auth.class);
// 模块注解和方法注解缺一个都代表不进行权限处理
if (moduleAuth == null || methodAuth == null) {
return;
}
// 拿到该接口方法的请求方式(GET、POST等)
Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
// 如果一个接口方法标记了多个请求方式,权限id是无法识别的,不进行处理
if (methods.size() != 1) {
return;
}
// 将请求方式和路径用`:`拼接起来,以区分接口。比如:GET:/user/{id}、POST:/user/{id}
String path = methods.toArray()[0] + ":" + info.getPatternsCondition().getPatterns().toArray()[0];
String methodPath = info.getPatternsCondition().getPatterns().toArray()[0].toString();
// 将权限名、资源路径、资源类型组装成资源对象,并添加集合中
RabcResource resourceEntity = new RabcResource();
resourceEntity.setType(1)
.setPath(path)
.setPerssionmenus(moduleAuth.perssionMenus())
.setName(methodAuth.name())
.setId(moduleAuth.id() + methodAuth.id())
.setMethod(methodPath);
list.add(resourceEntity);
});
log.info("【getAuthMethodResources方法】-------> 获取到所有需要权限控制的模块下的具体方法接口资源 模块下方法数量{}", list.size());
return list;
}
@Auth(id=1000,name = "附件文件上传下载接口", type = 0)
public class AnnexFileController {
@Autowired
private AnnexFileService annexFileService;
@PostMapping("/upload")
@ApiOperation("文件上传")
@Auth(id=1,name = "附件文件上传")
public Result<File> upload(@RequestPart("file") MultipartFile file) throws Exception {
Member user = LoginUtils.getCurrentMember();
AmshFile upload = annexFileService.upload(file, user.getUsername());
return Result.ok(upload);
}
@GetMapping("/download")
@ApiOperation(value = "下载文件")
@Auth(id=2,name = "附件文件下载")
public Result<?> download(@ApiParam(value = "文件ID", required = true) String fileId, HttpServletResponse response) {
annexFileService.downloadFile(fileId, response);
return Result.ok();
}
注意只有添加了该注解@Auth(id=2,name = "附件文件下载") ,类型才会进行接口资源扫描,才会加入到权限资源的数据库中,加入到资源数据库中,需要自己修改逻辑。只有同时实现了下面两个注解才会实现控制访问,一个是生成接口权限资源的,一个是接口访问的时候进行权限验证的
以上是开发过程中遇到问题并解决之后的总结