本章节是在上一节的基础上继续完成,如有不明白,请看上一篇文章【Shiro】SpringBoot集成Shiro权限认证《上》。
SQL语句
这里我们需要先准备好SQL语句,如下所示:
/*
Navicat MySQL Data Transfer
Source Server : local
Source Server Version : 50525
Source Host : localhost:3306
Source Database : new-shiro
Target Server Type : MYSQL
Target Server Version : 50525
File Encoding : 65001
Date: 2023-09-27 12:39:49
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_permissions
-- ----------------------------
DROP TABLE IF EXISTS `sys_permissions`;
CREATE TABLE `sys_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(20) NOT NULL COMMENT '权限名称',
`name` varchar(20) NOT NULL COMMENT '权限标识',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_permissions
-- ----------------------------
INSERT INTO `sys_permissions` VALUES ('5', 'user:update', '用户修改');
INSERT INTO `sys_permissions` VALUES ('6', 'user:delete', '用户删除');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(50) DEFAULT NULL COMMENT '角色编码',
`name` varchar(50) NOT NULL COMMENT '角色名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'admin', '管理员');
INSERT INTO `sys_role` VALUES ('20', 'user', '普通用户');
-- ----------------------------
-- Table structure for sys_roles_permissions
-- ----------------------------
DROP TABLE IF EXISTS `sys_roles_permissions`;
CREATE TABLE `sys_roles_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) NOT NULL COMMENT '角色编号',
`permission_id` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`id`) USING BTREE,
KEY `role_id` (`role_id`) USING BTREE,
KEY `permission_id` (`permission_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='角色权限表';
-- ----------------------------
-- Records of sys_roles_permissions
-- ----------------------------
INSERT INTO `sys_roles_permissions` VALUES ('2', '20', '5');
INSERT INTO `sys_roles_permissions` VALUES ('5', '1', '5');
INSERT INTO `sys_roles_permissions` VALUES ('6', '1', '6');
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(30) NOT NULL COMMENT '登录名',
`password` varchar(255) NOT NULL COMMENT '用户密码',
`name` varchar(30) DEFAULT NULL COMMENT '昵称',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `un_username_easyuser` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户表';
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', '小王');
INSERT INTO `sys_user` VALUES ('18', 'halo', '123456', 'Halo');
-- ----------------------------
-- Table structure for sys_users_roles
-- ----------------------------
DROP TABLE IF EXISTS `sys_users_roles`;
CREATE TABLE `sys_users_roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户编号',
`role_id` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`id`) USING BTREE,
KEY `user_id` (`user_id`) USING BTREE,
KEY `role_id` (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户角色表';
-- ----------------------------
-- Records of sys_users_roles
-- ----------------------------
INSERT INTO `sys_users_roles` VALUES ('1', '1', '1');
INSERT INTO `sys_users_roles` VALUES ('12', '18', '20');
依赖引入
<properties>
<java.version>8</java.version>
<mybatis-plus.version>3.1.1</mybatis-plus.version>
<mysql.version>5.1.47</mysql.version>
<alibaba.durid.version>1.0.9</alibaba.durid.version>
</properties>
<!--Mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--阿里巴巴连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${alibaba.durid.version}</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
代码生成
这里我们可以使用IDEA插件《EasyCode》生成Mybatis-Plus模板的后端代码。
生成的结果如下所示:
统一返回结果
这块是为了返回统一的结果,下面会用到。
Result.java
新建文件夹 common
@Data
public class Result<T> implements Serializable {
private String code;
private String message;
private T data;
public Result(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public Result(ResultCodeEnum resultCodeEnum, T data) {
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
this.data = data;
}
public static <T> Result<T> success(T data) {
return new Result<T>(ResultCodeEnum.SUCCESS, data);
}
public static Result fail(ResultCodeEnum resultCodeEnum) {
return new Result(resultCodeEnum, "");
}
}
ResultCodeEnum.java
新建文件夹 enums
public enum ResultCodeEnum {
SUCCESS("0000", "操作成功"),
SUCCESS_QUERY("0001", "查询成功"),
SUCCESS_ADD("0002", "添加成功"),
SUCCESS_UPDATE("0003", "更新成功"),
SUCCESS_DELETE("0004", "删除成功"),
TOKEN_ERROR("1000", "token错误"),
TOKEN_NULL("1001", "token为空"),
TOKEN_EXPIRED("1002", "token过期"),
TOKEN_INVALID("1003", "token无效"),
USER_ERROR("2000", "用户名密码错误"),
USER_NOT_EXISTS("2001", "用户不存在"),
USER_INVALID("2002", "用户无效"),
USER_EXPIRED("2003", "用户过期"),
USER_BLOCKED("2004", "用户封禁"),
USER_PASSWORD_ERROR("2005", "密码错误"),
PARAM_ERROR("3000", "参数错误"),
PARAM_NULL("3001", "参数为空"),
PARAM_FORMAT_ERROR("3002", "参数格式不正确"),
PARAM_VALUE_INCORRECT("3003", "参数值不正确"),
PARAM_DUPLICATE("3004", "参数重复"),
PARAM_CONVERT_ERROR("3005", "参数转化错误"),
AUTHORITY_ERROR("4000", "权限错误"),
AUTHORITY_UNAUTHORIZED("4001", "无权限"),
SERVER_ERROR("5000", "服务器内部错误"),
SERVER_UNAVAILABLE("5001", "服务器不可用"),
;
ResultCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
private String code;
private String message;
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
认证逻辑及授权
数据库操作逻辑
在SysUserService 新增 查询用户信息方法及查询角色、权限信息方法。
如下所示:
SysUserService.java
public interface SysUserService extends IService<SysUser> {
/**
* 根据用户名查询用户信息
* @param userName
* @return
*/
SysUser queryUserInfoByUserName(String userName);
/**
* 根据用户ID查询用户角色、权限相关信息
* @param userName
* @return
*/
SysUser queryUserInfoByUserInfo(String userName);
}
SysUserServiceImpl.java
@Service("sysUserService")
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Resource
SysRoleMapper sysRoleMapper;
@Resource
SysPermissionsMapper sysPermissionsMapper;
@Override
public SysUser queryUserInfoByUserName(String userName) {
return this.baseMapper.selectOne(new QueryWrapper<SysUser>().eq("username",userName));
}
@Override
public SysUser queryUserInfoByUserInfo(String userName) {
SysUser sysUser = this.baseMapper.selectOne(new QueryWrapper<SysUser>().eq("username",userName));
List<SysRole> roleList = sysRoleMapper.querySysRoleByUserId(sysUser.getId());
Set<SysPermissions> sysPermissionsSet = new HashSet<>();
roleList.forEach(role->{
sysPermissionsSet.addAll(this.sysPermissionsMapper.queryPermissionByRoleId(role.getId()));
});
sysUser.setSysRoleList(roleList);
sysUser.setPermissionsSet(sysPermissionsSet);
return sysUser;
}
}
SysRoleMapper.java
public interface SysRoleMapper extends BaseMapper<SysRole> {
/**
* 根据用户ID查询角色信息
* @param id
* @return
*/
@Select(" select t1.* from sys_role t1 inner join sys_users_roles t2 on t1.id = t2.role_id where t2.user_id = #{id} ")
List<SysRole> querySysRoleByUserId(Integer id);
}
SysPermissionsMapper.java
/**
* (SysPermissions)表数据库访问层
*
* @author halo-king
* @since 2023-09-27 12:42:43
*/
public interface SysPermissionsMapper extends BaseMapper<SysPermissions> {
/**
* 根据角色ID查询角色信息
* @param roleId
* @return
*/
@Select(" select t1.* from sys_permissions t1 inner join sys_roles_permissions t2 on t1.id = t2.permission_id where t2.role_id =#{roleId}")
List<SysPermissions> queryPermissionByRoleId(Integer roleId);
}
在完成了数据库相关的操作逻辑后,我们需要修改 CustomerRealm
中的认证和授权逻辑。
认证
/**
* 认证逻辑
* @param authenticationToken
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
System.out.println("执行了=>认证逻辑AuthenticationToken");
if(authenticationToken.getPrincipal()==null){
return null;
}
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
SysUser sysUser = this.sysUserService.queryUserInfoByUserName(token.getUsername());
if(sysUser == null){
return null;
}
return new SimpleAuthenticationInfo(sysUser.getUsername(), sysUser.getPassword(), getName());
}
授权
/**
* 授权逻辑
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
//获取登录用户名
String userName = (String) principalCollection.getPrimaryPrincipal();
SysUser sysUser = this.sysUserService.queryUserInfoByUserInfo(userName);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
for (SysRole role : sysUser.getSysRoleList()) {
//添加角色
authorizationInfo.addRole(role.getCode());
//添加权限
for (SysPermissions permissions : sysUser.getPermissionsSet()) {
authorizationInfo.addStringPermission(permissions.getCode());
}
}
return authorizationInfo;
}
统一接口返回
原来的登录接口
@GetMapping("/login")
public String login(String userName,String passWord) {
if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(passWord)) {
return "用户名密码不能为空";
}
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName, passWord);
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return "用户不存在";
} catch (AuthenticationException e) {
return "用户名密码错误";
} catch (AuthorizationException e) {
return "无权限登录";
}
return "登录成功";
}
修改完后的登录接口
@GetMapping("/login")
public Result login(String userName, String passWord) {
if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(passWord)) {
return Result.fail(ResultCodeEnum.USER_OR_PWD_NOT_EMPTY);
}
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName, passWord);
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return Result.fail(ResultCodeEnum.USER_NOT_EXISTS);
} catch (AuthenticationException e) {
return Result.fail(ResultCodeEnum.USER_ERROR);
} catch (AuthorizationException e) {
return Result.fail(ResultCodeEnum.AUTHORITY_UNAUTHORIZED);
}
return Result.success("登录成功");
}
测试
登录测试
到这一步,我们就已经完成了登录和授权的操作,现在启动项目,测试。
不输入用户名和密码
访问地址:
http://localhost:8082/login
返回结果:
{"code":"2006","message":"用户名或密码不能为空","data":""}
输入不存在的用户名和密码
访问地址:
http://localhost:8082/login?userName=test&passWord=123456
返回结果:
{"code":"2001","message":"用户不存在","data":""}
输入错误的用户名和密码
访问地址:
http://localhost:8082/login?userName=admin&passWord=12333
返回结果:
{"code":"2000","message":"用户名密码错误","data":""}
输入正确的用户名和密码
访问地址:
http://localhost:8082/login?userName=admin&passWord=123456
返回结果:
{"code":"0000","message":"操作成功","data":"登录成功"}
权限测试
注意:权限测试前提,需要先登录才行!!!
权限测试,我们分为方法授权和注解授权。
这里会列举一些小栗子,供大家参考。
通过上面的SQL,我们查询出用户admin 他具有角色为 admin 以及权限 user:update、user:delete.
方法授权
下面的代码,你可以方法放在LoginController 或者自己新建一个TestController 里面进行测试,都可。
@GetMapping("delete")
public Result delete() {
Subject subject = SecurityUtils.getSubject();
if(subject.isPermitted("user:delete")){
return Result.success("delete success");
}else{
return Result.fail(ResultCodeEnum.AUTHORITY_UNAUTHORIZED);
}
}
访问地址:
http://localhost:8082/delete
返回结果:
{"code":"0000","message":"操作成功","data":"delete success"}
注解授权
当前用户拥有update,delete 但是没有 add 权限。
常规用的注解:
- @RequiresAuthentication 需要完成用户登录
- @RequiresGuest 未登录用户可以访问,登录用户不能访问
- @RequiresPermissions 需要有对应资源权限
- @RequiresRoles 需要有对应角色才能访问
- @RequiresUser 需要用户完成登录并且实现了记住我功能
@RequiresPermissions("user:update")
@GetMapping("update")
public Result<String> update() {
return Result.success("update success ");
}
@RequiresPermissions("user:add")
@GetMapping("add")
public Result<String> add() {
return Result.success("add success ");
}
有权限
访问地址:
http://localhost:8082/update
返回结果:
{"code":"0000","message":"操作成功","data":"update success"}
无权限
访问地址:
http://localhost:8082/add
返回结果:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Sep 27 15:38:37 CST 2023
There was an unexpected error (type=Internal Server Error, status=500).
Subject does not have permission [user:add]
全局异常捕获
在上面的页面上,我们会发现,当出现没权有权限的时候,这样提示给用不是非常的不友好,所以,这里使用全局的异常捕获,给用户一个比较友好的体验。
新建一个exception文件夹,然后再文件加下新建 GlobalException.java
@Slf4j
@ControllerAdvice
public class GlobalException {
@ExceptionHandler(AuthorizationException.class)
@ResponseBody
@ResponseStatus(HttpStatus.FORBIDDEN)
public Result ExceptionHandler(AuthorizationException e) {
// 打印堆栈,以供调试
e.printStackTrace();
return Result.fail(ResultCodeEnum.AUTHORITY_UNAUTHORIZED);
}
@ExceptionHandler(Exception.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result ExceptionHandler(Exception e) {
// 打印堆栈,以供调试
e.printStackTrace();
return Result.fail(ResultCodeEnum.SERVER_ERROR);
}
}
当我们再次测试上面的没有权限的接口时,返回的结果就是如下所示:
{"code":"4001","message":"无权限","data":""}
密码加密
密码加密
使用以下的代码,在新增用户的时候,生成密码
System.out.println(new Md5Hash("123456", "YX"));
修改ShiroConfig
/**
* Shiro自带密码管理器
*
* @return HashedCredentialsMatcher
*/
public HashedCredentialsMatcher hashedCredentialsMatcher() {
//Shiro自带加密
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//散列算法使用md5
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数,2表示md5加密两次
credentialsMatcher.setHashIterations(1);
return credentialsMatcher;
}
修改自定义Realm,即:密码验证那块逻辑。
/**
* 认证逻辑
* @param authenticationToken
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
System.out.println("执行了=>认证逻辑AuthenticationToken");
if(authenticationToken.getPrincipal()==null){
return null;
}
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
SysUser sysUser = this.sysUserService.queryUserInfoByUserName(token.getUsername());
if(sysUser == null){
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(sysUser.getUsername(), sysUser.getPassword(), getName());
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("YX")); //加盐
return simpleAuthenticationInfo;
}