
本文主要基于springboot,对shiro的一次进阶和补充,如具备有shiro和基础开发思想,观看本文效果更佳
本文仅为记录学习轨迹,如有侵权,联系删除
一、sql本文基于springboot整合了shiro,实现了基于角色的权限控制系统的权限管理(RBAC),RBAC(role-based access control),基于角色的权限控制系统,是指对于不同角色的用户,拥有不同的权限 。用户绑定角色,角色绑定菜单权限和资源权限,形成用户-角色-权限的关系,
具体的sql如下:
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_perms -- ---------------------------- DROp TABLE IF EXISTS `t_perms`; CREATE TABLE `t_perms` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_perms -- ---------------------------- INSERT INTO `t_perms` VALUES (1, 'user1:*:*', NULL); INSERT INTO `t_perms` VALUES (3, 'user1:add', NULL); INSERT INTO `t_perms` VALUES (4, 'user1:detail', NULL); INSERT INTO `t_perms` VALUES (5, 'user1:edit', NULL); INSERT INTO `t_perms` VALUES (6, 'user1:del', NULL); INSERT INTO `t_perms` VALUES (7, 'user2:*.*', NULL); INSERT INTO `t_perms` VALUES (8, 'user2:add', NULL); INSERT INTO `t_perms` VALUES (9, 'user2:detail', NULL); INSERT INTO `t_perms` VALUES (10, 'user2:edit', NULL); INSERT INTO `t_perms` VALUES (11, 'user2:del', NULL); -- ---------------------------- -- Table structure for t_role -- ---------------------------- DROP TABLE IF EXISTS `t_role`; CREATE TABLE `t_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_role -- ---------------------------- INSERT INTO `t_role` VALUES (1, 'admin'); INSERT INTO `t_role` VALUES (2, 'user1'); INSERT INTO `t_role` VALUES (3, 'user2'); -- ---------------------------- -- Table structure for t_role_perms -- ---------------------------- DROP TABLE IF EXISTS `t_role_perms`; CREATE TABLE `t_role_perms` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) NULL DEFAULT NULL, `perms_id` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_role_perms -- ---------------------------- INSERT INTO `t_role_perms` VALUES (1, 1, 1); INSERT INTO `t_role_perms` VALUES (2, 1, 7); INSERT INTO `t_role_perms` VALUES (3, 2, 1); INSERT INTO `t_role_perms` VALUES (4, 2, 3); INSERT INTO `t_role_perms` VALUES (5, 2, 4); INSERT INTO `t_role_perms` VALUES (6, 2, 5); INSERT INTO `t_role_perms` VALUES (7, 2, 6); INSERT INTO `t_role_perms` VALUES (8, 3, 7); INSERT INTO `t_role_perms` VALUES (9, 3, 8); INSERT INTO `t_role_perms` VALUES (10, 3, 9); INSERT INTO `t_role_perms` VALUES (11, 3, 10); INSERT INTO `t_role_perms` VALUES (12, 3, 11); -- ---------------------------- -- Table structure for t_user -- ---------------------------- DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_user -- ---------------------------- INSERT INTO `t_user` VALUES (1, 'admin', '1a5a87c78c15ccb7dce2c66da8ad02de', '0jgji'); INSERT INTO `t_user` VALUES (2, '用户1', '9280294433e60ffab79c9fa76bb13877', '0q1ry'); INSERT INTO `t_user` VALUES (3, '用户2', '8c032f06d637221c055798f04f88e906', 'wa4oj'); -- ---------------------------- -- Table structure for t_user_role -- ---------------------------- DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NULL DEFAULT NULL, `role_id` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_user_role -- ---------------------------- INSERT INTO `t_user_role` VALUES (1, 1, 1); INSERT INTO `t_user_role` VALUES (2, 2, 2); INSERT INTO `t_user_role` VALUES (3, 3, 3); SET FOREIGN_KEY_CHECKS = 1;
二、前后端不分离模式 (1)pom注:这里仅仅做了最基础的实现
项目所需依赖如下
(2)基础的业务搭建org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine com.baomidou mybatis-plus-boot-starter 3.3.1 com.alibaba druid 1.1.21 log4j log4j 1.2.17 cn.hutool hutool-all 5.3.10 org.apache.shiro shiro-spring 1.8.0 org.apache.shiro shiro-ehcache 1.6.0
主要是对用户、角色、权限这三张表进行基础业务的编写,整合mybatis,实现基础的增删改查,用于后续shiro的配置
entity主要有三个实体,User(用户)、Role(角色)、Perms(权限)
User实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class User implements Serializable {
private static final long serialVersionUID = 2052750694898007196L;
@TableId(type = IdType.AUTO)
private Long id;
@TableField("name")
private String name;
@TableField("password")
private String password;
@TableField("salt")
private String salt;
@TableField(exist = false)//忽略该字段映射
private List roles;
}
Role实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_role")
public class Role implements Serializable {
private static final long serialVersionUID = 2148634916936785098L;
@TableId(type = IdType.AUTO)
private Long id;
@TableField("name")
private String name;
}
Perms实体
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_perms")
public class Perms implements Serializable {
private static final long serialVersionUID = -2097887484456744985L;
@TableId(type = IdType.AUTO)
private Long id;
@TableField("name")
private String name;
@TableField("url")
private String url;
}
mapper
mapper层负责数据库的增删改查操作,具体结构如下
UserMapper
@Repository @Mapper public interface UserMapper extends BaseMapper{ }
UserMapper.xml
RoleMapper
@Mapper @Repository public interface RoleMapper extends BaseMapper{ List selectRoleByUserName(String name); }
RoleMapper.xml
Perms
@Repository @Mapper public interface PermsMapper extends BaseMapper{ List selectPermsByRoleId(Long id); }
Perms.xml
service
service具体业务,结构如下:
UserServiceImpl
@Service public class UserServerImpl extends ServiceImpl{ public List findUserByName(String username) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getName,username); List userList = this.baseMapper.selectList(wrapper); return userList; } public Boolean login(User user) { //封装用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(user.getName(),user.getPassword()); Subject subject = SecurityUtils.getSubject(); try { // 执行登录方法,如果没有异常即成功了 subject.login(token); return true; } catch (DisabledAccountException e){ e.printStackTrace(); log.error("禁用的帐号"); } catch (UnknownAccountException e) { e.printStackTrace(); log.error("错误的帐号"); } catch (ExcessiveAttemptsException e){ e.printStackTrace(); e.printStackTrace(); log.error("登录失败次数过多"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); log.error("错误的凭证(密码)"); }catch (UnauthorizedException e){ e.printStackTrace(); log.error("权限不足"); }catch (AuthenticationException e) { e.printStackTrace(); log.error("未知的错误"); } return false; } public Boolean register(User user) { //验证用户名是否存在 List users = this.findUserByName(user.getName()); if (users.isEmpty()) { //密码加密,随机盐 String salt = RandomUtil.randomString(5); Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024); user.setPassword(md5Hash.toHex()); user.setSalt(salt); return this.baseMapper.insert(user) == 1? true:false; } else { //用户名已经存在,注册失败 return false; } } }
RoleServerImpl
@Service public class RoleServerImpl extends ServiceImpl{ public List findRoleByUserName(String name) { return this.baseMapper.selectRoleByUserName(name); } }
PermsServerImpl
@Service public class PermsServerImpl extends ServiceImplcommon{ public List findPermsByRoleId(Long id) { return this.baseMapper.selectPermsByRoleId(id); } }
这个用于封装通用返回实体类,以及一些通用的类
ResponseResult
@Slf4j @Data public class ResponseResult(3)shiro核心配置{ private Integer code; private String msg; private T data; public static ResponseResult success(Integer code, String msg){ ResponseResult result = new ResponseResult(); result.setCode(code); result.setMsg(msg); return result; } public static ResponseResult success(Integer code, String msg,T data){ ResponseResult result = new ResponseResult(); result.setCode(code); result.setMsg(msg); result.setData(data); return result; } public static ResponseResult failure(Integer code, String msg){ ResponseResult result = new ResponseResult(); result.setCode(code); result.setMsg(msg); return result; } }
只要有一点shiro基础的人都应该知道,其实shiro就两个核心的配置,一个是自定义的realm,只要继承AuthorizingRealm这个类即可,里面实现认证和授权的逻辑,还有一个是shiro的拦截和其他的核心配置,这个是真正意义上的核心配置,配置哪些路径需要拦截,哪些不拦截,未登录时应该跳往哪个路径,还有默认的注销方法配置等。
@Slf4j
public class CustomerRealm extends AuthorizingRealm {
@Autowired
private UserServerImpl userServer;
@Autowired
private RoleServerImpl roleServer;
@Autowired
private PermsServerImpl permsServer;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户名
String username = (String) principalCollection.getPrimaryPrincipal();
//添加用户权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//根据用户名称查询对应角色
List roles = roleServer.findRoleByUserName(username);
//依次添加权限
for (Role role:roles){
//角色信息添加
simpleAuthorizationInfo.addRole(role.getName());
List perms = permsServer.findPermsByRoleId(role.getId());
//权限信息添加
for(Perms perm:perms){
simpleAuthorizationInfo.addStringPermission(perm.getName());
}
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户的信息:UsernamePasswordToken,里面封装了username和password
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
// 根据用户名查询该用户
List users = userServer.findUserByName(userToken.getUsername());
// 如果用户不存在就抛出异常
if(CollectionUtils.isEmpty(users)){
throw new RuntimeException("用户不存在");
}
//密码认证,shiro自动处理
return new SimpleAuthenticationInfo(
users.get(0).getName(),
users.get(0).getPassword(),
ByteSource.Util.bytes(users.get(0).getSalt()),
this.getName());
}
}
config
ShiroConfig
@Configuration
public class ShiroConfig {
@Bean(name = "customerRealm")
public CustomerRealm customerRealm() {
//修改凭证校验匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//md5算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//散列次数
hashedCredentialsMatcher.setHashIterations(1024);
//应用凭证校验匹配器
CustomerRealm customerRealm = new CustomerRealm();
customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
//开启缓存
customerRealm.setCacheManager(new EhCacheManager());
customerRealm.setCachingEnabled(true);
//开启认证缓存
customerRealm.setAuthenticationCachingEnabled(true);
//给认证缓存起个名字
customerRealm.setAuthenticationCacheName("authenticationCache");
//开启授权缓存
customerRealm.setAuthorizationCachingEnabled(true);
//给授权缓存起个名字
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("customerRealm") CustomerRealm customerRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(customerRealm);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//关联安全管理器(DefaultWebSecurityManager)
bean.setSecurityManager(securityManager);
Map filterMap = new LinkedHashMap<>();
// 登录
filterMap.put("/login/page", "anon");
filterMap.put("/login", "anon");
// 注册
filterMap.put("/register/page", "anon");
filterMap.put("/register", "anon");
// 注销
filterMap.put("/logout", "logout");
//拦截所有的这一行必须放在最后
filterMap.put("
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
到这一步,基本完成了shiro的搭建,下面只需要写几个controller去测一下功能即可
(4)使用 登录认证首先编写登录和注册的接口
LoginController
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
private UserServerImpl userServer;
@PostMapping
public ResponseResult login(User user){
return userServer.login(user)?ResponseResult.success(200, "登录成功") : ResponseResult.failure(500, "登录失败");
}
@GetMapping("/page")
public ResponseResult loginPage(){
return ResponseResult.failure(500,"友情提示,请先登录");
}
}
RegisterController
@RestController
@RequestMapping("/register")
public class RegisterController {
@Resource
private UserServerImpl userServer;
@PostMapping
public ResponseResult register(User user){
return userServer.register(user)?ResponseResult.success(200, "注册成功") : ResponseResult.failure(500, "注册失败");
}
}
启动项目进行测试,如果处于未登录状态的话,访问该系统的任意接口(哪怕这个接口不存在),都会先被拦截到配置好的登录接口,理论上这个接口应该跳转到一个页面(例如用thymeleaf跳转到html页面),在页面进行登录,这里为了演示就直接返回一个字符串提示
拦截生效了,再进行登录
授权有种方式,同样是对于接口进行授权
| 注解 | 说明 | 案例 |
|---|---|---|
| @RequiresRoles | 基于角色进行授权,可以绑定多个角色 | @RequiresRoles(value = {“user1”,“admin”},logical = Logical.OR)// 具备有user1或者admin角色即可访问 |
| @RequiresPermissions | 基于某个详细的权限进行授权,可以绑定多个 | @RequiresPermissions(“user2:detail”)//只有具备user2:detail权限的用户才可以访问 |
下面进行接口的编写
User1Controller
@RestController
@RequestMapping("/user1")
@RequiresRoles(value = {"user1","admin"},logical = Logical.OR)// 具备有user1或者admin角色即可访问
public class User1Controller {
@GetMapping("/detail")
public ResponseResult detailAll(){
return ResponseResult.success(200,"用户1查看");
}
@PostMapping("/edit")
public ResponseResult edit() {
return ResponseResult.failure(200,"用户1编辑");
}
@PostMapping("/add")
public ResponseResult add(User user){
return ResponseResult.failure(200,"用户1新增");
}
@PostMapping("/del")
public ResponseResult del(){
return ResponseResult.failure(200,"用户1删除");
}
}
User2Controller
@RestController
@RequestMapping("/user2")
@RequiresRoles("admin")
public class User2Controller {
@RequiresPermissions("user2:detail")
@GetMapping("/detail")
public ResponseResult detailAll(){
return ResponseResult.success(200,"用户2查看");
}
@RequiresPermissions("user2:edit")
@PostMapping("/edit")
public ResponseResult edit() {
return ResponseResult.failure(200,"用户2编辑");
}
@RequiresPermissions("user2:add")
@PostMapping("/add")
public ResponseResult add(User user){
return ResponseResult.failure(200,"用户2新增");
}
@RequiresPermissions("user2:del")
@PostMapping("/del")
public ResponseResult del(){
return ResponseResult.failure(200,"用户2删除");
}
}
开始测试,先登录用户1(具备有User1Controller里面的所有接口权限)
登录成功后访问User1Controller里面的接口,一切正常
如果访问User2Controller里面的接口,就会发现无权限访问,直接报错
(5)CustomerRealm说明如果觉得直接返回500错误不优雅,可以在代码里面加个全局异常捕捉,进行返回友好提示
对于CustomerRealm这个类,功能就是简单的授权和认证,那么,什么时候会触发里面的方法呢?
doGetAuthorizationInfo首先是里面的doGetAuthorizationInfo方法,这个方法是授权的时候用的,上面在授权的时候,用到两个注解@RequiresRoles和@RequiresPermissions,只要用到这两个注解的时候就会触发doGetAuthorizationInfo方法,例如:用户1登录后,访问了用户1授权的接口就会触发这个方法
这个方法是用于认证,触发的时机就只有用户在登录的时候触发,更准确来说,用户在执行下图的subject.login(token);方法时就会触发这个认证方法
像这种前后端不分离的模式,一般用的session进行认证,上面的代码可以整合thymeleaf,做几个页面完善一下就可以直接用了。