【Shiro】SpringBoot集成Shiro权限认证《下》

本章节是在上一节的基础上继续完成,如有不明白,请看上一篇文章【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>8java.version>
        <mybatis-plus.version>3.1.1mybatis-plus.version>
        <mysql.version>5.1.47mysql.version>
        <alibaba.durid.version>1.0.9alibaba.durid.version>
      properties>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plusartifactId>
            <version>${mybatis-plus.version}version>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>${mybatis-plus.version}version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>${alibaba.durid.version}version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>${mysql.version}version>
        dependency>

代码生成

这里我们可以使用IDEA插件《EasyCode》生成Mybatis-Plus模板的后端代码。

生成的结果如下所示:

【Shiro】SpringBoot集成Shiro权限认证《下》_第1张图片

统一返回结果

这块是为了返回统一的结果,下面会用到。

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;
}

你可能感兴趣的:(SpringBoot,spring,boot,后端,java)