1、依赖导入
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
除了shiro-spring这种依赖,其实在springboot也有集成了的依赖:shiro-spring-boot-web-starter,差别不大,我们这里使用shiro-spring
2、数据库表的建立及初始化
Shiro是不会替我们定义角色和权限的,所以我们需要设计相应的表
下面是一些相关表的设计,大家可以参考
用户表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(50) NOT NULL,
`salt` varchar(128) DEFAULT NULL COMMENT '加密盐值',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(50) DEFAULT NULL COMMENT '联系方式',
`sex` int(255) DEFAULT NULL COMMENT '年龄:1男2女',
`age` int(3) DEFAULT NULL COMMENT '年龄',
`status` int(1) NOT NULL COMMENT '用户状态:1有效; 2删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
PRIMARY KEY (`id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
角色表
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色id',,
`name` varchar(50) NOT NULL COMMENT '角色名称',
`description` varchar(255) DEFAULT NULL COMMENT '角色描述',
`status` int(1) NOT NULL COMMENT '状态:1有效;2删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
权限表(菜单和按钮)
CREATE TABLE `permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '权限id',
`permission_code` varchar(20) NOT NULL COMMENT '权限编码',
`name` varchar(100) NOT NULL COMMENT '权限名称',
`description` varchar(255) DEFAULT NULL COMMENT '权限描述',
`url` varchar(255) DEFAULT NULL COMMENT '权限访问路径',
`parent_id` int(11) DEFAULT NULL COMMENT '父级权限id',
`type` int(1) DEFAULT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
`order_num` int(3) DEFAULT '0' COMMENT '排序',
`icon` varchar(50) DEFAULT NULL COMMENT '图标',
`status` int(1) NOT NULL COMMENT '状态:1有效;2删除',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
说明:parent_id用于组成一个树状的菜单结构,比如系统设置下有用户管理和角色管理两个子模块,而角色管理下有角色添加、角色修改等子模块
用户角色关系表
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(20) NOT NULL COMMENT '用户id',
`role_id` varchar(20) NOT NULL COMMENT '角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
角色权限关系表
CREATE TABLE `role_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` varchar(20) NOT NULL COMMENT '角色id',
`permission_id` varchar(20) NOT NULL COMMENT '权限id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
注:修改关系表,我们只需要将原来的数据删除,然后插入新的数据即可。
权限设计
当我们需要将权限控制到某个菜单下的时候,可以类似如下
message表示一个消息模块,我们有一个权限是控制用户是否能访问这个模块,我们在权限表设计时,访问消息模块的权限url都为message:xxx,所以当用户拥有message:权限的时候都可以访问这个模块
例:
map.put(“/message/**”,perms[message:*])
消息模块下的某个接口如下,使用@RequiresPermissions注解进行控制
//设置权限
@RequiresPermissions("message:getMessage")
@PostMapping("/getMessage")
public String getMessage(){
return "";
}
注:一般一个模块对应一个控制器。
shiro常用的权限控制注解,可以在控制器类上使用
注解 功能
@RequiresGuest 只有游客可以访问
@RequiresAuthentication 需要登录才能访问
@RequiresUser 已登录的用户或“记住我”的用户能访问
@RequiresRoles 已登录的用户需具有指定的角色才能访问
@RequiresPermissions 已登录的用户需具有指定的权限才能访问
想要使用shiro的注解,我们需要在配置类添加以下配置
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
3、自定义Realm
在设计好角色和权限之后,我们就可以自定义Realm。Realm的作用是什么呢?我们再复习下身份认证的流程
1:应用程序代码调用 Subject.login(token) 方法后,传入代表最终用户身份的 AuthenticationToken 实例 Token。
2:将 Subject 实例委托给应用程序的 SecurityManager(Shiro 的安全管理)并开始实际的认证工作。
3:SecurityManager 根据具体的 Realm 进行安全认证。
自定义Realm只需要继承 AuthorizingRealm 类,然后实现下面两个方法即可
doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。也就是先先根据用户名从数据库中获取其对应的角色和权限,将其封装到 authorizationInfo 并返回给 Shiro。
doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息,doGetAuthenticationInfo主要作用是获取用户输入的用户名、密码等信息并从数据库中取出保存的密码交给shiro,由shiro的密码匹配器进行匹配。
下面是一个自定义的realm,实际项目根据自己的需求进行定义
/**
* 自定义realm
*/
public class MyRealm extends AuthorizingRealm {
@Resource
private EmployeeMapper employeeMapper;
/**
* 该方法主要用于获取授权信息,PrincipalCollection是一个用于标识属性的安全术语,如用户名或用户id或号码
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//创建一个SimpleAuthorizationInfo对象用于存储授权信息
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
//获取用户凭证,就是用户名、帐号
String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();
//获取用户对应的角色
List<String> userRoles = employeeMapper.getUserRoles(primaryPrincipal);
//将用户角色信息设置进simpleAuthorizationInfo
simpleAuthorizationInfo.setRoles(new HashSet<>(userRoles));
//获取用户对应的权限
List<String> userPermissions = employeeMapper.getUserPermissions(primaryPrincipal);
simpleAuthorizationInfo.setStringPermissions(new HashSet<>(userPermissions));
return simpleAuthorizationInfo;
}
/**
* 该方法用于认证当前用户,根据用户输入的用户名,在数据库中查找到用户记录,并用查到的用户对象、数据库中存储的密码、密码盐(没有则不传)和Realm对象名字构建一个认证信息对象(SimpleAuthenticationInfo)交给系统进行密码验证。
* @param authenticationToken AuthenticationToken包含了用户在身份验证时提交的帐户和凭证
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名/帐号
String username = (String) authenticationToken.getPrincipal();
// 根据用户名从数据库中查询该用户
Employee employee = employeeMapper.selectEmployeeByName(username);
if (employee!=null &&employee.getId()!=null){
// 把当前用户存到 Session 中
SecurityUtils.getSubject().getSession().setAttribute("employee", employee);
//传入用户名和密码进行身份认证,并返回认证信息。第一个参数是用户名,第二个是密码,第三个是realm
SimpleAuthenticationInfo myRealm = new SimpleAuthenticationInfo(employee.getName(), employee.getPassword(), "myRealm");
return myRealm;
}else {
//如果用户不存在,则抛出UnknownAccountException异常
throw new UnknownAccountException("账号不存在");
}
}
}
4、Shiro 配置
Shiro需要配置后才可以生效,Shiro配置主要包括三个东西:自定义 Realm、安全管理器 SecurityManager 和 Shiro 过滤器。
自定义Realm我们已经定义定义好了,只需要注入即可
SecurityManager是Shiro核心,主要协调Shiro内部的各种安全组件
ShiroFilterFactoryBean :用于在基于spring的web应用中定义Shiro过滤器。
ShiroFilterFactoryBean 中的一些配置
-
loginUrl:没有登录的用户请求需要登录的页面时自动跳转到登录页面。
-
unauthorizedUrl:没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面。
-
successUrl:登录成功默认跳转页面,不配置则跳转至”/”,可以不配置,直接通过代码进行处理。
-
securityManager:这个属性是必须的,配置为我们定义的securityManager就好了。
-
filterChainDefinitions或者filterChainDefinitionMap:配置过滤规则,从上到下的顺序匹配。
下面再介绍下shiro过滤器,方便我们配置过滤规则
配置缩写 对应的过滤器 功能
anon AnonymousFilter 指定url可以匿名访问
authc FormAuthenticationFilter 指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到 loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返 回的信息都可以定制嘛。
authcBasic BasicHttpAuthenticationFilter 指定url需要basic登录
logout LogoutFilter 登出过滤器,配置指定url(即ShiroFilterFactoryBean.setLoginUrl设置的url)就可以实现退出功能,非常方便
noSessionCreation NoSessionCreationFilter 禁止创建会话
perms PermissionsAuthorizationFilter 需要指定权限才能访问
port PortFilter 需要指定端口才能访问
rest HttpMethodPermissionFilter 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
roles RolesAuthorizationFilter 需要指定角色才能访问
ssl SslFilter 需要https请求才能访问
user UserFilter 需要已登录或“记住我”的用户才能访问
下面是一个简单的shiro配置类,大家可以参考
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
* 注入自定义的realm
*/
@Bean
public MyRealm myRealm(){
return new MyRealm();
}
/**
* 配置SecurityManager并设置Reaml
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(myRealm());
return defaultSecurityManager;
}
/**
* 配置过滤器
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//没有登录的用户请求需要登录的页面时自动跳转到登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
//没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
//配置过滤规则,使用LinkedHashMap保持规则的顺序,因为过滤规则是从上到下匹配的
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/login", "anon");
//配置logout过滤器
filterChainDefinitionMap.put("/logout", "logout");
//这行代码必须放在所有权限设置的最后,不然会导致所有url都被拦截,都需要认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
5、定义登录接口
在后台登录接口中,接收用户名密码,据此创建一个usernamePasswordToken令牌,交由Shiro并调用login()方法进行登录,然后就会调用自定义 Realm的doGetAuthenticationInfo 方法,如果不抛出任何异常表明登录成功,如果抛出异常,我们可以根据异常种类返回提示出错信息给用户。
下面是一个简单的登录接口例子
@RestController
public class LoginController {
@PostMapping("/login")
public String login(@RequestParam("username")String username,@RequestParam("password")String password){
//从SecurityUtils中创建一个subject
Subject subject = SecurityUtils.getSubject();
//创建用于认证的token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
try {
// 执行认证登陆
subject.login(usernamePasswordToken);
} catch (UnknownAccountException uae) {
return "未知账户";
} catch (IncorrectCredentialsException ice) {
return "密码不正确";
} catch (LockedAccountException lae) {
return "账户已锁定";
} catch (ExcessiveAttemptsException eae) {
return "用户名或密码错误次数过多";
} catch (AuthenticationException ae) {
return "用户名或密码不正确!";
}catch (Exception e){
return "登录失败!";
}
if (subject.isAuthenticated()){
return "登录成功!";
}else {
return "登录失败!";
}
}
}
注:参数使用表单传输
6、权限控制
shiro可以使用url配置控制权限,也可以在控制器类上使用注解控制权限。一般我们是同时使用两种配置方式,用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制。url控制权限是在shiro配置类里进行配置,我们上面已经简单的配置了,下面我们使用shiro注解进行接口的权限控制
@RequiresPermissions注解标识接口需要用户拥有哪些权限才可以访问
例:
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private EmployeeMapper employeeMapper;
@PostMapping("list")
@RequiresPermissions({"employee:update"})
public List<Employee>list(){
return employeeMapper.selectEmployeeList();
}
}
shiro注解中多个权限和角色之间默认是“与”关系,如果想实现的是“或”关系,那么将logical配置成Logical.OR即可,如下
@PostMapping("list")
@RequiresPermissions(value = {"employee:update","employee:list"},logical = Logical.OR)
public List<Employee>list(){
return employeeMapper.selectEmployeeList();
}
当我们进行接口访问的时候,shiro就会校验登录的用户是否拥有对应的角色或者权限,如果没有那么就会进入某个页面或者抛出异常,跳转的页面是由setUnauthorizedUrl决定的
7、Shiro常见异常
1、AuthencationException: AuthenticationException 异常是Shiro在登录认证过程中,认证失败需要抛出的异常。
AuthenticationException包含以下子类:
CredentitalsException :凭证异常
IncorrectCredentialsException 不正确的凭证
ExpiredCredentialsException 凭证过期
AccountException: 账号异常
ConcurrentAccessException 并发访问异常(多个用户同时登录时抛出)
UnknownAccountException 未知的账号
ExcessiveAttemptsException 认证次数超过限制
DisabledAccountException 禁用的账号
LockedAccountException 账号被锁定
UnsupportedTokenException: 使用了不支持的Token
2、AuthorizationException:授权相关的异常
子类:
UnauthorizedException:抛出以指示请求的操作或对请求的资源的访问是不允许的。
UnanthenticatedException:当尚未完成成功认证时,尝试执行授权操作时引发异常。