springboot整合shiro的一次进阶与补充

说明

本文主要基于springboot,对shiro的一次进阶和补充,如具备有shiro和基础开发思想,观看本文效果更佳

本文仅为记录学习轨迹,如有侵权,联系删除

一、sql

本文基于springboot整合了shiro,实现了基于角色的权限控制系统的权限管理(RBAC),RBAC(role-based access control),基于角色的权限控制系统,是指对于不同角色的用户,拥有不同的权限 。用户绑定角色,角色绑定菜单权限和资源权限,形成用户-角色-权限的关系,
在这里插入图片描述
具体的sql如下:

/*
 Navicat Premium Data Transfer

 Source Server         : test1
 Source Server Type    : MySQL
 Source Server Version : 80015
 Source Host           : localhost:3306
 Source Schema         : shiro01

 Target Server Type    : MySQL
 Target Server Version : 80015
 File Encoding         : 65001

 Date: 15/05/2022 17:20:42
*/

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

项目所需依赖如下

       <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>

        <!--Druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>

        <!--log4j-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.10</version>
        </dependency>

        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.8.0</version>
        </dependency>

        <!--shiro的ehcache缓存-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.6.0</version>
        </dependency>


    </dependencies>

(2)基础的业务搭建

主要是对用户、角色、权限这三张表进行基础业务的编写,整合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<Role> 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<User> {
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zqcn.springboot_shiro.mapper.UserMapper">

</mapper>

RoleMapper

@Mapper
@Repository
public interface RoleMapper extends BaseMapper<Role> {

    /**
     * 根据名字用户名字查询用户角色
     * @param name
     * @return
     */
    List<Role> selectRoleByUserName(String name);
}

RoleMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zqcn.springboot_shiro.mapper.RoleMapper">
    <resultMap id="roleMap" type="com.zqcn.springboot_shiro.entity.Role">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
    </resultMap>


    <select id="selectRoleByUserName" resultMap="roleMap">
        SELECT
            r.*
        FROM
            t_user u
            LEFT JOIN t_user_role ur ON u.id = ur.user_id
            LEFT JOIN t_role r ON r.id = ur.role_id
        WHERE
            u.`name` = #{name};
    </select>
</mapper>

Perms

@Repository
@Mapper
public interface PermsMapper extends BaseMapper<Perms> {

    /**
     * 通过用户名查询用户权限
     * @param id
     * @return
     */
    List<Perms> selectPermsByRoleId(Long id);

}

Perms.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zqcn.springboot_shiro.mapper.PermsMapper">
    <resultMap id="permsMap" type="com.zqcn.springboot_shiro.entity.Perms">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="url" property="url"></result>
    </resultMap>



    <select id="selectPermsByRoleId" resultMap="permsMap">
        SELECT
            p.*
        FROM
            t_perms p
            LEFT JOIN t_role_perms rp ON p.id = rp.perms_id
            LEFT JOIN t_role r ON r.id = rp.role_id
        WHERE
            r.id = #{id}
    </select>
</mapper>

service

service具体业务,结构如下:
在这里插入图片描述
UserServiceImpl

@Service
public class UserServerImpl extends ServiceImpl<UserMapper, User> {


    /**
     * 根据用户名查询用户列表
     * @param username
     * @return
     */
    public List<User> findUserByName(String username) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getName,username);
        List<User> userList = this.baseMapper.selectList(wrapper);
        return userList;
    }

    /**
     * 登录
     * @param user
     * @return
     */
    public Boolean login(User user) {
        //封装用户的登录数据
        UsernamePasswordToken token = new UsernamePasswordToken(user.getName(),user.getPassword());
        Subject subject = SecurityUtils.getSubject();

        try {
            /**
             * //            DisabledAccountException(禁用的帐号)
             * //            LockedAccountException(锁定的帐号)
             * //            UnknownAccountException(错误的帐号)
             * //            ExcessiveAttemptsException(登录失败次数过多)
             * //            IncorrectCredentialsException (错误的凭证)
             * //            ExpiredCredentialsException(过期的凭证)等
             */

            // 执行登录方法,如果没有异常即成功了
            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;
    }

    /**
     * 注册
     * @param user
     * @return
     */
    public Boolean register(User user) {
        //验证用户名是否存在
        List<User> 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<RoleMapper, Role> {


    /**
     * 通过用户名查询对应的所有角色
     * @param name
     * @return
     */
    public List<Role> findRoleByUserName(String name) {
        return this.baseMapper.selectRoleByUserName(name);
    }
}

PermsServerImpl

@Service
public class PermsServerImpl extends ServiceImpl<PermsMapper, Perms> {

    /**
     * 通过角色id查询对应角色的权限
     * @param id
     * @return
     */
    public List<Perms> findPermsByRoleId(Long id) {
        return this.baseMapper.selectPermsByRoleId(id);
    }
}

common

这个用于封装通用返回实体类,以及一些通用的类
在这里插入图片描述
ResponseResult

@Slf4j
@Data
public class ResponseResult<T> {

    /**
     * 状态相应码
     */
    private Integer code;

    /**
     * 对响应码的描述
     */
    private String msg;

    /**
     * 返回的数据
     */
    private T data;



    /**
     * 成功,设置code和msg
     * @param code
     * @param msg
     * @return
     */
    public static ResponseResult success(Integer code, String msg){
        ResponseResult result = new ResponseResult();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }


    /**
     * 成功,设置code和msg,data
     * @param code
     * @param msg
     * @return
     */
    public static<T> ResponseResult success(Integer code, String msg,T data){
        ResponseResult result = new ResponseResult();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    /**
     * 失败,设置code和msg
     * @param code
     * @param msg
     * @return
     */
    public static ResponseResult failure(Integer code, String msg){
        ResponseResult result = new ResponseResult();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }



}

(3)shiro核心配置

只要有一点shiro基础的人都应该知道,其实shiro就两个核心的配置,一个是自定义的realm,只要继承AuthorizingRealm这个类即可,里面实现认证和授权的逻辑,还有一个是shiro的拦截和其他的核心配置,这个是真正意义上的核心配置,配置哪些路径需要拦截,哪些不拦截,未登录时应该跳往哪个路径,还有默认的注销方法配置等。
在这里插入图片描述

realm

@Slf4j
public class CustomerRealm extends AuthorizingRealm {

    @Autowired
    private UserServerImpl userServer;

    @Autowired
    private RoleServerImpl roleServer;

    @Autowired
    private PermsServerImpl permsServer;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        /**
         * 这里主要是查询该用户下的所有权限,步骤如下:
         * (1)获取用户名
         * (2)根据用户名获取角色
         * (3)根据对应角色获取权限
         * (4)所有角色的权限填充到”SimpleAuthorizationInfo“类中
         */


        // 获取用户名
        String username = (String) principalCollection.getPrimaryPrincipal();

        //添加用户权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //根据用户名称查询对应角色
        List<Role> roles = roleServer.findRoleByUserName(username);

        //依次添加权限
        for (Role role:roles){
            //角色信息添加
            simpleAuthorizationInfo.addRole(role.getName());
            List<Perms> perms = permsServer.findPermsByRoleId(role.getId());

            //权限信息添加
            for(Perms perm:perms){
                simpleAuthorizationInfo.addStringPermission(perm.getName());
            }
        }

        return simpleAuthorizationInfo;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        /**
         * 这里主要是用户登录时进行登录认证,主要步骤如下:
         * (1)获取登录用户的信息(姓名和密码)
         * (2)先根据用户名判断该用户存不存在,不存在则抛出异常,然后在调用登录方法的地方进行异常捕捉即可
         * (3)最后将查询出来的用户,交给”SimpleAuthenticationInfo“类进行登录认证即可
         */


        // 获取用户的信息:UsernamePasswordToken,里面封装了username和password
        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
        // 根据用户名查询该用户
        List<User> 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 {

    /**
     * 创建自定义realm类,自定义UserRealm
     *
     * @return
     */
    @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;
    }

    /**
     * DefaultWebSecurityManager
     *
     * @param customerRealm
     * @return
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("customerRealm") CustomerRealm customerRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //关联UserRealm
        securityManager.setRealm(customerRealm);
        return securityManager;
    }


    /**
     * ShiroFilterFactoryBean
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();

        //关联安全管理器(DefaultWebSecurityManager)
        bean.setSecurityManager(securityManager);


        /**
         * 添加shiro的内置过滤器
         * anon:无需认证就可以访问
         * anthc:必须认证了才能访问
         * user:必须拥有记住我功能才能用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有对某个角色权限才能访问
         */
        Map<String, String> 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("/**", "authc");

        //添加过滤器
        bean.setFilterChainDefinitionMap(filterMap);
        //未登录时的跳转路径
        bean.setLoginUrl("/login/page");

        return bean;
    }


    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     *
     * @return
     */
    @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

/**
 * @ClassName : RegisterController
 * @Description : 注册
 * @Author : CJH
 * @Date: 2022-05-15 16:14
 */
@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里面的接口,就会发现无权限访问,直接报错
在这里插入图片描述

如果觉得直接返回500错误不优雅,可以在代码里面加个全局异常捕捉,进行返回友好提示

(5)CustomerRealm说明

对于CustomerRealm这个类,功能就是简单的授权和认证,那么,什么时候会触发里面的方法呢?

doGetAuthorizationInfo

首先是里面的doGetAuthorizationInfo方法,这个方法是授权的时候用的,上面在授权的时候,用到两个注解@RequiresRoles和@RequiresPermissions,只要用到这两个注解的时候就会触发doGetAuthorizationInfo方法,例如:用户1登录后,访问了用户1授权的接口就会触发这个方法
在这里插入图片描述

doGetAuthenticationInfo

这个方法是用于认证,触发的时机就只有用户在登录的时候触发,更准确来说,用户在执行下图的subject.login(token);方法时就会触发这个认证方法
在这里插入图片描述

(6)补充

像这种前后端不分离的模式,一般用的session进行认证,上面的代码可以整合thymeleaf,做几个页面完善一下就可以直接用了。

二、前后端分离的模式

前后端不分离的模式,前后端的认证包括授权都是基于token实现的,下面利用shiro进行项目搭建,sql结构还是上面的sql结构。

(1)pom和yml文件

    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--        mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

        <!--Druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>

        <!--log4j-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.10</version>
        </dependency>


        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.2</version>
        </dependency>

        <!--shiro的ehcache缓存-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.6.0</version>
        </dependency>



        <!-- java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>

    </dependencies>
#参考链接:https://www.cnblogs.com/hellokuangshen/p/12497041.html
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro01?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    password: 123
    username: root

    #切换为druid数据源
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    #需要导入log4j依赖
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500


# 密码加密方法
md5:
  # 哈希迭代次数
  hashIterations: 1024



#jwt:
#  #jwt密钥
#  secret: aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrs1232134214
#  #jwt过期时间
#  expire-time-in-second: 60
#  #携带jwt的请求头字段名
#  auth-header: token



  #出现错误时, 直接抛出异常
  mvc:
    throw-exception-if-no-handler-found: true


mybatis-plus:
  mapper-locations: classpath:/mapper/**.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

(2)基础业务的搭建

entity

主要有3个实体类,对应数据库的结构,分别是用户user、角色role和权限perms
在这里插入图片描述
User

@Data
@TableName("t_user")
public class User {

    /**
     * id
     */
    private Long id;
    /**
     * 用户名
     */
    private String name;
    /**
     * 密码
     */
    private String password;
    /**
     * 盐值
     */
    private String salt;
}

Role

@TableName("t_role")
@Data
public class Role {
    private Long id;
    private String name;
}

Perms

@TableName("t_perms")
@Data
public class Perms {
    private Long id;
    private String name;
    private String url;
}

Mapper

UserMapper

public interface UserMapper extends BaseMapper<User> {
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zqcn.mapper.UserMapper">

</mapper>

RoleMapper

public interface RoleMapper extends BaseMapper<Role> {

    List<Role> selectByUsername(String username);
}

RoleMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zqcn.mapper.RoleMapper">

    <select id="selectByUsername" resultType="com.zqcn.entity.Role">
        SELECT
            r.id as id,
            r.name as name
        FROM
            t_user_role ur
            LEFT JOIN t_user u ON u.id = ur.user_id
            LEFT JOIN t_role r ON ur.role_id = r.id
        where u.name = #{username}
    </select>
</mapper>

PermsMapper

public interface PermsMapper extends BaseMapper<Perms> {

    /**
     * 根据用户名查询权限
     * @param username
     * @return
     */
    List<Perms> selectByUsername(String username);
}

PermsMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zqcn.mapper.PermsMapper">

    <select id="selectByUsername" resultType="com.zqcn.entity.Perms">
        SELECT
            p.id as id,
            p.name as name,
            p.url as url
        FROM
            t_role_perms rp
            LEFT JOIN t_role r ON rp.role_id = r.id
            left join t_perms p on rp.perms_id = p.id
            LEFT JOIN t_user_role ur on ur.role_id = r.id
            LEFT JOIN t_user u on ur.user_id = u.id
        where u.name = #{username}


    </select>
</mapper>

service

UserServiceImpl

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {

    @Resource
    private UserMapper userMapper;

    /**
     * 密码md5加密,哈希迭代次数
     */
    @Value("${md5.hashIterations}")
    private Integer hashIterations;



    public User getByName(String username){
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getName,username);
        List<User> users = userMapper.selectList(wrapper);
        return users.size() > 0? users.get(0):null;
    }


    public LoginDto login(UserLoginForm form) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getName,form.getUsername());
        List<User> users = userMapper.selectList(wrapper);
        if(CollectionUtils.isEmpty(users)){
            throw new BusinessException("用户不存在");
        }


        User user = users.get(0);
        String hex = new Md5Hash(form.getPassword(), user.getSalt(), hashIterations).toHex();
        if(!hex.equals(user.getPassword())){
            throw new BusinessException("密码错误");
        }

        LoginDto loginDto = new LoginDto();
        loginDto.setUsername(user.getName());
        loginDto.setToken(JwtUtil.createToken(user.getName()));
        return loginDto;
    }

}

RoleServiceImpl

@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> {

    public List<Role> getListByUsername(String username){
        return this.baseMapper.selectByUsername(username);
    }

}

PermsServiceImpl

@Service
public class PermsServiceImpl extends ServiceImpl<PermsMapper, Perms> {


    public List<Perms> getListByUsername(String username){
        return this.baseMapper.selectByUsername(username);
    }
}

commons

commons主要是封装一些通用性的东西,例如通用返回实体类,通用分页实体类等
通用返回实体类ResponseResult

@Slf4j
@Data
public class ResponseResult<T> {

    /**
     * 状态相应码
     */
    private Integer code;

    /**
     * 对响应码的描述
     */
    private String msg;

    /**
     * 返回的数据
     */
    private T data;

    /**
     * 成功,但没有返回数据
     * @return
     */
    public static ResponseResult success(){
        ResponseResult result = new ResponseResult();
        result.setCode(200);
        result.setMsg("success");
        return result;
    }

    /**
     * 成功,有返回数据
     * @param data
     * @return
     */
    public static <T>ResponseResult success(T data){
        ResponseResult result = new ResponseResult();
        result.setCode(200);
        result.setMsg("success");
        result.setData(data);
        return result;
    }

    /**
     * 失败,设置code和msg
     * @return
     */
    public static ResponseResult failure(){
        ResponseResult result = new ResponseResult();
        result.setCode(500);
        result.setMsg("failure");
        return result;
    }

    /**
     * 失败,设置code和msg
     * @return
     */
    public static ResponseResult failure(Integer code,String msg){
        ResponseResult result = new ResponseResult();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }

    /**
     * 失败,设置code和msg
     * @return
     */
    public static ResponseResult failure(String msg){
        ResponseResult result = new ResponseResult();
        result.setCode(500);
        result.setMsg(msg);
        return result;
    }
}

config

项目配置类,这里主要是mybatis-plus的相关配置
MyBatisPlusConfig

@MapperScan("com.zqcn.*")
@EnableTransactionManagement
@Configuration
public class MyBatisPlusConfig {

    /**
     * 注册分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 添加分页插件
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
        // 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
        pageInterceptor.setOverflow(false);
        // 单页分页条数限制,默认无限制
        pageInterceptor.setMaxLimit(500L);
        // 设置数据库类型
        pageInterceptor.setDbType(DbType.MYSQL);

        interceptor.addInnerInterceptor(pageInterceptor);
        return interceptor;
    }

}

bean

bean是个人根据项目需要,自己创建的文件夹,里面主要存放有dto和form两个文件夹,dto主要存放接口返回的实体类,form是前端传进来的参数,都用form来进行接收
在这里插入图片描述

form

UserLoginForm

@Data
public class UserLoginForm {
    private String username;
    private String password;
}
dto

LoginDto

@Data
public class LoginDto {
    public String username;
    private String token;
}

exception

exception是自定义异常,系统根据业务有时候会抛出自定义异常,用于全局的异常捕捉,返回特定的提示消息
BusinessException

@Data
public class BusinessException extends RuntimeException {
    protected  Integer code;
    protected  String message;

    public BusinessException(Integer code, String message){
        this.code = code;
        this.message = message;
    }

    public BusinessException(String message){
        this.code = 500;
        this.message = message;
    }


}

handler

这个是全局异常捕捉,用于捕捉特定的异常,并且返回特定的提示消息

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


    /**
     * shiro权限不足异常
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseResult unauthorizedException(Throwable e, HttpServletRequest request) {
        log.warn("URL:{}.系统异常:", request.getRequestURI(), e);
        ResponseResult failure = ResponseResult.failure(401,"权限不足");
        return failure;
    }

    /**
     * 运行时异常处理(500错误)
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public ResponseResult handle500(Throwable e, HttpServletRequest request) {
        ResponseResult failure = ResponseResult.failure(e.getMessage());
        log.warn("URL:{}.系统异常:", request.getRequestURI(), e);
        return failure;
    }

    /**
     * 运行时异常处理(404异常错误)
     */
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseResult handle404(Exception e, HttpServletRequest request) {
        ResponseResult failure = ResponseResult.failure(e.getMessage());
        log.warn("URL:{}.404异常:", request.getRequestURI(), e);
        return failure;
    }

    /**
     * 空指针异常处理
     *
     * @param e       空指针异常
     * @param request
     * @return
     */
    @ExceptionHandler(NullPointerException.class)
    public ResponseResult handleNullPointException(NullPointerException e, HttpServletRequest request) {
        ResponseResult failure = ResponseResult.failure(e.getMessage());
        log.warn("URL:{}.系统异常:", request.getRequestURI(), e);
        return failure;
    }

    /**
     * 自定义异常处理
     *
     * @param e       自定义异常
     * @param request
     * @return
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseResult handleBusinessException(BusinessException e, HttpServletRequest request) {
        ResponseResult result = new ResponseResult();
        result.setCode(e.getCode());
        result.setMsg(e.getMessage());

        log.warn("URL:{},业务异常{}", request.getRequestURI(), e);
        return result;
    }
}

(3)shrio的核心配置

这一部分是重点,先简单说一下思路,主要分为以下两种:
(1)请求登录接口,返回token
(2)请求中带上登录返回的token,系统会拦截,然后执行shrio里面的认证方法(doGetAuthenticationInfo)和授权方法(doGetAuthorizationInfo),认证方法直接解析token,得到用户名,再去比对数据库的用户,存在该用户就进入授权方法,根据token解析的用户名去获取权限,进行权限的填充,如果不存在该用户就直接抛出自定义异常
(3)如果请求中不带token,直接抛出自定义异常

注:任何异常,都有全局异常捕捉器进行捕捉并且返回特定的提示

jwt

这里主要是jwt的相关代码
JwtToken

public class JwtToken implements AuthenticationToken {
    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}

JwtUtil

@Slf4j
public class JwtUtil {
    // 过期时间 5min
    private static final Long EXPIRE_TIME = 5 * 60 * 1000L;

    // 密钥
    private static final String SECRET = "SHIRO+JWT";

    // 生成token,5min后过期
    public static String createToken(String username) {
        String token = null;
        try {
            // 过期时间
            Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            // 加密算法
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            token = JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(expireDate)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            log.error("Failed to create token. {}", e.getMessage());
        }
        return token;
    }


    // 验证token
    public static boolean verify(String token, String username) {
        boolean isSuccess = false;
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            // 验证token
            verifier.verify(token);
            isSuccess = true;
        } catch (UnsupportedEncodingException e) {
            log.error("Token is invalid. {}", e.getMessage());
        }
        return isSuccess;
    }

    // 获取token中的信息,无需secret解密也能获得
    public static String getUsernameFromToken(String token) {
        try {
            DecodedJWT decode = JWT.decode(token);
            String username = decode.getClaim("username").asString();
            return username;
        } catch (JWTDecodeException e) {
            log.error("Failed to Decode jwt. {}", e.getMessage());
            return null;
        }
    }

}

filter

filter主要是拦截token,进行相应的业务处理,JwtFilter需要继承BasicHttpAuthenticationFilter类,再实现对应的逻辑

@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
    /**
     * token合法性校验
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Token");

        // 判断请求头是否带有 token
        if (Strings.isNotBlank(token)) {
            // 如果存在 token ,则进入executeLogin()方法执行登入,并检测 token 的正确性
            try {
                return executeLogin(request, response);
            } catch (Exception e) {
                log.error("Error! {}", e.getMessage());
                this.response401(request,response,e.getMessage());
            }
        }else {
            // 请求头获取不到token
            HttpServletResponse resp = (HttpServletResponse) response;
            String message = "权限认证失败 => 获取不到token";
            log.error(message);
            this.response401(request,response,message);
            return false;
        }
        return false;
    }


    /**
     * 执行登入操作
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Token");
        JwtToken jwtToken = new JwtToken(token);
        // 提交给 realm 进行登入,如果错误,会抛出异常并捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常,则代表登入成功,返回 true
        return true;
    }


    /**
     * 非法请求将跳转到 "/unauthorized/**"
     * @param request
     * @param response
     * @param msg
     * @throws ServletException
     * @throws IOException
     */
    private void response401(ServletRequest request, ServletResponse response
            , String msg) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        try {
            // //请求转发401controller
            httpServletRequest.getRequestDispatcher("/auth/unauthorized/" + msg).forward(request, response);
        } catch (ServletException | IOException e) {
            e.printStackTrace();
        }
    }



    /**
     * 对跨域提供支持
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

}

realm

自定义realm主要实现认证和授权的业务逻辑

CustomRealm

@Slf4j
public class CustomRealm extends AuthorizingRealm {
    @Resource
    private UserServiceImpl userService;
    @Resource
    private RoleServiceImpl roleService;
    @Resource
    private PermsServiceImpl permsService;


    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授权认证
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("授权");
        String token = (String) principals.getPrimaryPrincipal();
        // 解密
        String username = JwtUtil.getUsernameFromToken(token);
        //添加用户权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 添加用户角色
        List<Role> roleList = roleService.getListByUsername(username);
        for (Role role : roleList) {
            simpleAuthorizationInfo.addRole(role.getName());
        }

        //添加用户权限
        List<Perms> permsList = permsService.getListByUsername(username);
        for (Perms perms : permsList) {
            simpleAuthorizationInfo.addStringPermission(perms.getName());
        }

        return simpleAuthorizationInfo;
    }

    /**
     * 登录认证
     * @param auth
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.info("登录认证");

        // 这里的 token是从 JWTFilter 的 executeLogin() 方法传递过来的
        String token = (String) auth.getCredentials();
        // 解密
        String username = JwtUtil.getUsernameFromToken(token);
        User user = userService.getByName(username);
        if(null == user){
            throw new UnknownAccountException("用户名或密码错误");
        }
        return new SimpleAuthenticationInfo(token, token, getName());

    }
}

config

shiro的相关配置,重点是要在拦截器配置上,加上上面创建的拦截器JwtFilter进行拦截

ShiroConfig


@Configuration
public class ShiroConfig {

    /**
     * 创建自定义realm类,自定义UserRealm
     *
     * @return
     */
    @Bean(name = "customRealm")
    public CustomRealm customerRealm() {
        CustomRealm customerRealm = new CustomRealm();
        return customerRealm;
    }


    /**
     * DefaultWebSecurityManager
     *
     * @param customerRealm
     * @return
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("customRealm") CustomRealm customerRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //关联UserRealm
        securityManager.setRealm(customerRealm);
        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }


    /**
     * ShiroFilterFactoryBean
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();

        //关联安全管理器(DefaultWebSecurityManager)
        bean.setSecurityManager(securityManager);

        // 添加自己的过滤器并且取名为jwtFilter
        Map<String, Filter> customFilterMap = new HashMap<String, Filter>(1);
        customFilterMap.put("jwtFilter", new JwtFilter());
        bean.setFilters(customFilterMap);


        /**
         * 添加shiro的内置过滤器
         * anon:无需认证就可以访问
         * anthc:必须认证了才能访问
         * user:必须拥有记住我功能才能用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有对某个角色权限才能访问
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/login", "anon");
        filterMap.put("/auth/**", "anon");


        //拦截所有的这一行必须放在最后
        filterMap.put("/**", "jwtFilter");
        bean.setFilterChainDefinitionMap(filterMap);

        return bean;
    }



    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     *
     * @return
     */
    @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;
    }


    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


}

到这里基本完成配置,下面可以开始测试了

(4)使用

这里简单写了一些接口
AuthenticateController

@RestController
@RequestMapping("/auth")
public class AuthenticateController {

    @GetMapping("/unauthorized/{message}")
    public ResponseResult<String> unauthorized(@PathVariable String message) {
        return ResponseResult.failure(401,message);
    }

}

LoginController

@RestController
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private UserServiceImpl userService;

    @GetMapping
    public ResponseResult login(UserLoginForm form){
        LoginDto loginDto = userService.login(form);
        return ResponseResult.success(loginDto);
    }


}

UserController

@RestController
@RequestMapping("/user")
@Slf4j
@RequiresRoles(value = {"user1"},logical = Logical.OR)
public class UserController {

    @RequestMapping("/add")
    public String add(){
        return "this is test_add";
    }

    @RequestMapping("/update")
    public String update(){
        return "this is test_update";
    }

    @RequestMapping("/delete")
    public String delete(){
        return "this is test_delete";
    }

    @RequestMapping("/query")
    public String query(){
        return "this is test_query";
    }
}

如果不登录直接访问user接口
在这里插入图片描述
登录用户1,登录成功后返回token
在这里插入图片描述
带上用户1的token,重新访问user接,访问成功
在这里插入图片描述
登录用户2的账号,并且用用户2的token去访问user接口(该接口只有用户1才有权限访问)
在这里插入图片描述
在这里插入图片描述
认证与授权都是正常的,到此为止基本完成这样的一个小demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值