最近入职了第一份Java后端开发的工作,在正式干活之前,部门老大首先给我派了个小任务,给部门的员工进行信息登记。要求每个员工首先注册自己信息,而员工的任何删改查操作都需要登录,并且只能查询或者修改自己的信息,部门老大需要所有权限。
这个小需求我用Springboot、Mybatis、Thymeleaf、Shiro、MySql做了个小网站,连接池用的Druid,分页插件使用Pagehelper,日志使用的默认日志。
为了记录一下整个项目的过程,并且巩固一下自己的知识,在完成之后,重新建立了一个小demo,重点重现了该Web环境(Mybatis及逆向工程、Shiro权限管理框架集成)的搭建过程。
完成之后,项目的目录结构如下:
这个就没有什么好讲的,我们用IDEA直接创建一个Springboot的Web环境即可。
application.yml文件如下:
server:
port: 80
spring:
datasource:
name: demo
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mysql://localhost:3306/shiroexample?useUnicode=true&charactorEncoding=utf-8&serverTimeZone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath*:/mybatis/mapper/*Mapper.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
root: info
com.hebin.shiroexample.mapper: debug
file: log/log.log
一般Springboot的版本无需使用高版本(本项目使用2.1.14),版本太高会有插件或者工具因为未更新导致未知原因错误。插件选择图中几个,也可以一个不选在最后POM中手动添加依赖坐标,因为本文不展示Thymeleaf的使用,所以无需勾选Thymeleaf。其中Lombok插件用于简化JavaBean的开发,省略Getter/Setter/toString等方法代码,Spring Web是必须使用的,Mybatis和Mysql用于持久层。
POM文件,dependencies如下:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.17version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
关于Shiro的介绍和使用请看王诗林的这篇文章springboot整合shiro(完整版),因为我使用的也不是很熟练,我这里定性的讲一下Shiro权限验证和授权的大概原理。
首先,在一个项目中,会有用户,而用户除了用户名、密码等常规属性之外,还具有各种角色。比如论坛项目,角色有:管理员,版主,普通注册用户,游客等角色,同时应该注意到,一个用户可以同时拥有多个角色,是个一对多的关系。而一个角色会有各种权限,也是一对多的关系。例如管理员有封禁、修改用户角色、发帖、删帖等权限,而游客只有浏览权限。
因此,在Shiro框架下,至少有三个实体类:User,Role,Permission,对应有三张数据库表,同时也应该建立两张关联表,将用户与角色的关系User_Role,角色与权限Role_Permission的关系联系起来。
在数据库中建立五张表
建表语句如下:
create table t_user
(
id int auto_increment comment '用户主键ID' primary key,
username varchar(10) not null comment '用户名,不能重复',
nickname varchar(10) not null comment '昵称',
password varchar(100) not null comment '密码',
description varchar(256) null comment '用户描述',
constraint t_user_username_uindex unique (username)
);
create table t_role
(
id int auto_increment comment '角色主键ID' primary key,
role_name varchar(20) not null comment '角色名称',
description varchar(50) null comment '角色描述',
constraint t_role_role_name_uindex unique (role_name)
);
create table t_permission
(
id int auto_increment comment '权限主键ID' primary key,
permission_name varchar(20) not null comment '权限名称',
description varchar(30) null comment '权限描述',
constraint t_permission_permission_name_uindex unique (permission_name)
);
create table t_user_role
(
id int auto_increment comment '用户和角色关联表主键ID' primary key,
user_id int not null comment '用户ID',
role_id int not null comment '角色ID'
);
create table t_role_permission
(
id int auto_increment comment '角色和权限关联表主键ID' primary key,
role_id int not null comment '角色ID',
permission_id int not null comment '权限ID'
);
利用逆向工程生成User、Role、Permission单表的Bean和Mapper接口以及Mapper映射文件
逆向工程可以查看这篇文章:Springboot项目Web环境引入Mybatis逆向工程MBG。
在User、Role的类中添加字段,最终如下。
@Data
注解为Lombok插件注解,也可不用,但需要将后加属性添加getter/setter方法,以及toString方法。
@Data
public class User {
private Integer id;
private String username;
private String nickname;
private String password;
private String description;
// 用户对应有各种角色,后加。
private Set<Role> roles;
}
@Data
public class Role {
private Integer id;
private String roleName;
private String description;
// 每个角色都有一些权限,后加。
private Set<Permission> permissions;
}
@Data
public class Permission {
private Integer id;
private String permissionName;
private String description;
}
因为Shiro对外接口是Subject,外界与Shiro的交互首先交给Subject,其在内部交给SecurityManager,SecurityManager再根据相关的域(Realm)做出授权或者认证管理。
而这个域,需要进行自己的编写。
package com.hebin.shiroexample.shiro;
import com.hebin.shiroexample.bean.Permission;
import com.hebin.shiroexample.bean.Role;
import com.hebin.shiroexample.bean.User;
import com.hebin.shiroexample.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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 授权的方法,首先从系统中获取系统使用主体,从数据库中查出该主体拥有的角色信息和权限信息,并赋予给AuthorizationInfo,从而完成授权。
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1.根据主体获取PrimaryPrincipal(不一定非得是用户名,也可以是手机号,邮箱地址等能够唯一确定用户的信息)
String username = (String) principalCollection.getPrimaryPrincipal();
// 这一步先判断是否存在被授权用户。
if (principalCollection == null || StringUtils.isEmpty(username)) {
return null;
}
// 2.根据PrimaryPrincipal获取用户。
User user = userService.getUserWithRolesAndPermissionsByUsername(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 3.将用户具有的角色和权限信息分别添加到AuthorizationInfo中
Set<Role> roles = user.getRoles();
for (Role r : roles) {
// 添加角色
simpleAuthorizationInfo.addRole(r.getRoleName());
Set<Permission> permissions = r.getPermissions();
// 添加权限
for (Permission permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission.getPermissionName());
}
}
// 将该主体应该有的角色和权限全部添加到Shiro中返回,Controller接收到返回值后判断是否有授权放行。
return simpleAuthorizationInfo;
}
/**
* 认证的方法
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 从token中获取用户名、密码等信息
String username = (String) authenticationToken.getPrincipal();
// 如果获取不到,认证失败
if (authenticationToken == null || StringUtils.isEmpty(username)) {
return null;
}
// 根据获取到的信息,从数据库查询相关实体
User user = userService.getUserWithRolesAndPermissionsByUsername(username);
// 如果实体不存在,认证失败
if (user == null) {
return null;
}
// 将查询到的实体的相关信息交给Shiro进行判断是否通过认证。
return new SimpleAuthenticationInfo(username,user.getPassword(),getName());
}
}
package com.hebin.shiroexample.service;
import com.hebin.shiroexample.bean.User;
public interface UserService {
User getUserWithRolesAndPermissionsByUsername(String username);
}
package com.hebin.shiroexample.service.impl;
import com.hebin.shiroexample.bean.Role;
import com.hebin.shiroexample.bean.User;
import com.hebin.shiroexample.mapper.RoleExtendMapper;
import com.hebin.shiroexample.mapper.UserExtendMapper;
import com.hebin.shiroexample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserExtendMapper userExtendMapper;
@Autowired
private RoleExtendMapper roleExtendMapper;
@Override
public User getUserWithRolesAndPermissionsByUsername(String username) {
// 根据用户名查出用户,其中用户的Set Roles属性中不包含Role的Set permissions属性。
User user = userExtendMapper.selectUserWithRolesByUsername(username);
// 取出用户的Roles Set,根据每个role查询出对应的Set,将role缺失的该权限属性集合用setter方法赋予。
Set<Role> roles = user.getRoles();
// 1.循环取出role
for (Role r : roles) {
// 2.每个role取出其id
Integer roleId = r.getId();
// 3.根据roleId取出对应的permissions完好的Role
Role role = roleExtendMapper.selectRoleWithPermissionsByRoleId(roleId);
// 4.调用role的setter方法赋值permissions。
r.setPermissions(role.getPermissions());
}
// 返回User对象,该对象是完整对象,内包含全部的role和permission。
return user;
}
}
UserExtendMapper:
package com.hebin.shiroexample.mapper;
import com.hebin.shiroexample.bean.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserExtendMapper extends UserMapper {
User selectUserWithRolesByUsername(String username);
}
RoleExtendMapper:
package com.hebin.shiroexample.mapper;
import com.hebin.shiroexample.bean.Role;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleExtendMapper extends RoleMapper {
Role selectRoleWithPermissionsByRoleId(Integer id);
}
当完成上面的代码后,需要进行Shiro的配置了:
package com.hebin.shiroexample.config;
import com.hebin.shiroexample.shiro.MyRealm;
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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator creator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
// 将自定义的Realm加入到Spring容器
@Bean
public MyRealm myShiroRealm() {
return new MyRealm();
}
// 权限管理,特别需要注意的是,SecurityManager是个接口,一定要使用org.apache.shiro.mgt.SecurityManager;
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(myShiroRealm());
return defaultWebSecurityManager;
}
// filter设置过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
HashMap<String, String> map = new HashMap<>();
map.put("/logout", "logout");
// authc表示需要认证才能访问,即拦截认证;anon表示无需认证也能访问,即不拦截
map.put("/admin/**", "authc");
map.put("/user/**", "authc");
map.put("/leader/**", "authc");
map.put("/visitor/**", "anon");
filterFactoryBean.setLoginUrl("/login");
filterFactoryBean.setUnauthorizedUrl("/error");
filterFactoryBean.setFilterChainDefinitionMap(map);
return filterFactoryBean;
}
// 开启Shiro的注解
@Bean
public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
首先我们建立一个异常拦截类拦截授权或者认证异常
package com.hebin.shiroexample.exceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.naming.AuthenticationException;
@RestControllerAdvice
@Slf4j
public class MyExceptionHandler {
@ExceptionHandler(AuthorizationException.class)
public String authorizationExceptionHandler(AuthorizationException e) {
log.error("没有通过权限验证", e);
return "没有通过权限验证!";
}
@ExceptionHandler(AuthenticationException.class)
public String authenticationExceptionHandler(AuthenticationException e) {
log.error("授权异常", e);
return "授权异常!";
}
}
我们建立一些假数据:
User:
Role:
Permission:
User_Role:
Role_Permission:
建立一个Controller测试权限情况
@RestController
public class IndexController {
@RequestMapping("/index")
public String index() {
return "success!";
}
@RequiresRoles("admin")
@RequestMapping("/admin/list")
public String getUserList() {
Subject subject = SecurityUtils.getSubject();
Object principal = subject.getPrincipal();
System.out.println(principal);
return "userList";
}
@RequiresPermissions("delete")
@RequestMapping("/admin/delete")
public String deleteUser() {
return "userDelete";
}
@GetMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
return "账号和密码错误!";
} catch (AuthorizationException e) {
e.printStackTrace();
return "没有权限!";
}
return "登陆成功!";
}
}
访问:http://localhost/index,无需任何权限,成功!
返回Success!
测试登录:
浏览器访问:http://localhost/login?username=aa&password=123
控制台提示认证错误。
org.apache.shiro.authc.AuthenticationException: Authentication failed for token submission [org.apache.shiro.authc.UsernamePasswordToken - aa, rememberMe=false].
浏览器访问:http://localhost/login?username=a&password=123
a用户为管理员,包含所有权限,访问:http://localhost/admin/list,成功