美文网首页
spring boot 整合shiro 、jwt

spring boot 整合shiro 、jwt

作者: 侧耳倾听y | 来源:发表于2021-07-18 22:39 被阅读0次

    参考:
    https://blog.csdn.net/qq_43948583/article/details/104437752
    https://blog.csdn.net/weixin_42375707/article/details/111145907

    计划实现增删改查级别的权限控制

    1. 表结构

    菜单表

    CREATE TABLE `menu` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
      `parent_id` bigint(20) NOT NULL COMMENT '父节点id',
      `url` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '路径',
      `sort_id` int(11) DEFAULT '0' COMMENT '排序ID',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表';
    

    菜单-操作表

    CREATE TABLE `menu_opt` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
      `opt_id` bigint(20) NOT NULL COMMENT '操作ID',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单-操作关联表';
    

    操作表

    CREATE TABLE `opt` (
      `opt_id` bigint(20) NOT NULL AUTO_INCREMENT,
      `opt_name` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作名称',
      PRIMARY KEY (`opt_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作表';
    

    角色表

    CREATE TABLE `role` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `name` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色名称',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
    

    角色-菜单-操作表

    CREATE TABLE `role_menu_opt` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `role_id` bigint(20) NOT NULL COMMENT '角色ID',
      `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
      `opt_id` bigint(20) NOT NULL COMMENT '操作ID',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单-操作关联表表';
    

    用户表

    CREATE TABLE `sys_user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `username` varchar(100) NOT NULL,
      `pwd` varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    

    用户角色表

    CREATE TABLE `user_role` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` bigint(20) NOT NULL COMMENT '用户id',
      `role_id` bigint(20) NOT NULL COMMENT '角色ID',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
    

    2. 使用mybatis plus自动生成一些类

    可以参考:https://www.jianshu.com/p/c984ac0b67e8

    3. 添加shiro、jwt依赖

            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring-boot-starter</artifactId>
                <version>1.5.3</version>
            </dependency>
    
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.8.3</version>
            </dependency>
    

    4. shiro、jwt配置

    • shiro config
    @Configuration
    public class ShiroConfig {
    
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    
            //关联 DefaultWebSecurityManager
            shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    
            //添加shiro内置的过滤器
            /*
             * anon: 无需认证就可以访问
             * auth: 必须认证才能访问
             * user: 必须拥有记住我功能才能用
             * perms: 拥有对某个资源的权限才能访问
             * role: 拥有某个角色权限才能访问
             * */
            // 添加过滤器
            Map<String, Filter> filters = new HashMap<>(4);
            // 自定义的认证授权过滤器
            filters.put("auth", new AuthFilter());
            // 添加自定义的认证授权过滤器
            shiroFilterFactoryBean.setFilters(filters);
    
            //要拦截的路径放在map里面
            Map<String, String> filterMap = new LinkedHashMap<>();
            // 放行login接口
            filterMap.put("/login", "anon");
            // 放行logout接口
            filterMap.put("/logout", "anon");
            // 拦截所有路径, 它自动会跑到 AuthFilter这个自定义的过滤器里面
            filterMap.put("/**", "auth");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
            return shiroFilterFactoryBean;
        }
    
        @Bean
        public DefaultWebSecurityManager defaultWebSecurityManager(AuthRealm authRealm) {
            DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    
            //关联realm
            defaultWebSecurityManager.setRealm(authRealm);
    
            /*
             * 关闭shiro自带的session
             */
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            defaultWebSecurityManager.setSubjectDAO(subjectDAO);
            return defaultWebSecurityManager;
        }
    
        // 将自定义realm注入到 DefaultWebSecurityManager
        @Bean
        public AuthRealm authRealm() {
            return new AuthRealm();
        }
    
        // 通过调用Initializable.init()和Destroyable.destroy()方法,从而去管理shiro bean生命周期
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        // 开启shiro权限注解
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(defaultWebSecurityManager);
            return advisor;
        }
    }
    
    • realm
    public class AuthRealm extends AuthorizingRealm {
    
        @Autowired
        private SysUserMapper sysUserMapper;
    
        @Autowired
        private UserRoleMapper userRoleMapper;
    
        @Autowired
        private RoleMenuOptMapper roleMenuOptMapper;
    
        // 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            // 获取前端传来的token
            String accessToken = (String) token.getPrincipal();
            // 根据token去缓存里查找用户名
            String userId = JwtUtils.getAudience(accessToken);
            if (userId == null) {
                // 查找的用户名为空,即为token失效
                throw new IncorrectCredentialsException("token失效,请重新登录");
            }
            JwtUtils.verifyToken(accessToken, userId);
            SysUser user = sysUserMapper.selectById(Long.valueOf(userId));
            if (user == null) {
                throw new UnknownAccountException("用户不存在!");
            }
            // 此方法需要返回一个AuthenticationInfo类型的数据
            // 因此返回一个它的实现类SimpleAuthenticationInfo,将user以及获取到的token传入它可以实现自动认证
    
            return new SimpleAuthenticationInfo(user, accessToken, "");
        }
    
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            //从认证那里获取到用户对象User
            SysUser user = (SysUser) principals.getPrimaryPrincipal();
            // 此方法需要一个AuthorizationInfo类型的返回值,因此返回一个它的实现类SimpleAuthorizationInfo
            // 通过SimpleAuthorizationInfo里的addStringPermission()设置用户的权限
            QueryWrapper<UserRole> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("user_id", user.getId());
            List<UserRole> userRoles = userRoleMapper.selectList(queryWrapper);
            List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
            QueryWrapper<RoleMenuOpt> optWrapper = new QueryWrapper<>();
            optWrapper.in("role_id", roleIds);
            List<RoleMenuOpt> roleMenuOpts = roleMenuOptMapper.selectList(optWrapper);
            List<String> optList = roleMenuOpts.stream().map(opt -> String.valueOf(opt.getOptId()))
                    .collect(Collectors.toList());
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addStringPermissions(optList);
    
            return simpleAuthorizationInfo;
        }
    
    //    @Override
    //    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
    //        //自定义密码匹配器
    //        MyCredentialsMatcher currentCredentialsMatcher = new MyCredentialsMatcher();
    //        super.setCredentialsMatcher(currentCredentialsMatcher);
    //    }
    }
    
    • shiro filter
    public class AuthFilter extends AuthenticatingFilter {
    
    
        // 生成自定义token
        @Override
        protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            //从header中获取token
            String token = httpServletRequest.getHeader("token");
            return new JwtToken(token);
        }
    
        // 所有请求全部拒绝访问
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            // 允许option请求通过
            return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
        }
    
        // 拒绝访问的请求,onAccessDenied方法先获取 token,再调用executeLogin方法
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            // 获取请求token
            String token = httpServletRequest.getHeader("token");
            // StringUtils.isBlank(String str)  判断str字符串是否为空或者长度是否为0
            if (token == null || "".equals(token)) {
                httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
                httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
                httpServletResponse.setCharacterEncoding("UTF-8");
                Response msg = Response.fail("请先登录");
                httpServletResponse.getWriter().write(JSON.toJSONString(msg));
                return false;
            }
            return executeLogin(request, response);
        }
    
        // token失效时调用
        @Override
        protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpResponse.setCharacterEncoding("UTF-8");
            try {
                //处理登录失败的异常
                Throwable throwable = e.getCause() == null ? e : e.getCause();
                Response msg = Response.fail("登录凭证已失效,请重新登录");
                httpResponse.getWriter().write(JSON.toJSONString(msg));
            } catch (IOException e1) {
    
            }
            return false;
        }
    
    
    }
    
    • jwt util
    public class JwtUtils {
    
        /**
         * 签发对象:这个用户的id
         * 签发时间:现在
         * 有效时间:30分钟
         * 载荷内容:暂时设计为:这个人的名字,这个人的昵称
         * 加密密钥:这个人的id加上一串字符串
         */
        public static String createToken(String userId, String realName, String userName) {
    
            Calendar nowTime = Calendar.getInstance();
            nowTime.add(Calendar.MINUTE, 30);
            Date expiresDate = nowTime.getTime();
            return JWT.create()
                    // 签发对象
                    .withAudience(userId)
                    // 发行时间
                    .withIssuedAt(new Date())
                    // 有效时间
                    .withExpiresAt(expiresDate)
                    // 载荷,随便写几个都可以
                    .withClaim("userName", userName)
                    .withClaim("realName", realName)
                    // 加密
                    .sign(Algorithm.HMAC256(userId + "HelloLehr"));
        }
    
        /**
         * 检验合法性,其中secret参数就应该传入的是用户的id
         *
         * @param token
         */
        public static void verifyToken(String token, String secret) {
            DecodedJWT jwt = null;
            try {
                JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret + "HelloLehr")).build();
                jwt = verifier.verify(token);
            } catch (Exception e) {
                // 效验失败
                // 这里抛出的异常是我自定义的一个异常,你也可以写成别的
                throw new TokenUnavailableException();
            }
        }
    
        /**
         * 获取签发对象
         */
        public static String getAudience(String token) {
            String audience = null;
            try {
                audience = JWT.decode(token).getAudience().get(0);
            } catch (JWTDecodeException j) {
                // 这里是token解析失败
                throw new TokenUnavailableException();
            }
            return audience;
        }
    
        /**
         * 通过载荷名字获取载荷的值
         */
        public static Claim getClaimByName(String token, String name) {
            return JWT.decode(token).getClaim(name);
        }
    
    }
    

    jwt token

    public class JwtToken extends UsernamePasswordToken {
    
        String token;
    
        public JwtToken(String token) {
            this.token = token;
        }
    
        @Override
        public Object getPrincipal() {
            return token;
        }
    
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    

    有了以上类,基本上计算实现了,接下来写登录

    5. 登录

    login controller

    @RestController
    public class LoginController {
    
        @Autowired
        private SysUserMapper sysUserMapper;
    
        @PostMapping("/login")
        public Response login(@RequestParam("username") String username, @RequestParam("password") String password) {
            QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("username", username);
            SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
            if (sysUser == null) {
                return Response.fail("账号或密码错误");
            }
            if (!sysUser.getPwd().equals(password)) {
                return Response.fail("账号或密码错误");
            }
            String token = JwtUtils.createToken(sysUser.getId().toString(), sysUser.getUsername(), sysUser.getUsername());
            return Response.success("登录成功").add("token", token);
        }
    
    }
    

    6. 测试controller

    @RestController
    @RequestMapping("/menu")
    @Slf4j
    public class MenuController {
    
        @Autowired
        private MenuService menuService;
    
        @GetMapping("/view")
        @RequiresPermissions("1")
        public Response view() {
            return Response.success("success").add("menu", menuService.list());
        }
    
        @GetMapping("/add")
        @RequiresPermissions("2")
        public Response add() {
            log.info("menu add.....");
            return Response.success("success");
        }
    
    }
    

    相关文章

      网友评论

          本文标题:spring boot 整合shiro 、jwt

          本文链接:https://www.haomeiwen.com/subject/ftmapltx.html