Spring Boot+Mybatis-Plus+Shiro+JWT开发一个权限管理系统

源码地址

后端

https://github.com/jonssonyan...

前端

https://github.com/jonssonyan...

系统界面

Spring Boot+Mybatis-Plus+Shiro+JWT开发一个权限管理系统_第1张图片
Spring Boot+Mybatis-Plus+Shiro+JWT开发一个权限管理系统_第2张图片

项目结构

后端

Spring Boot+Mybatis-Plus+Shiro+JWT开发一个权限管理系统_第3张图片

前端

Spring Boot+Mybatis-Plus+Shiro+JWT开发一个权限管理系统_第4张图片

数据库

Spring Boot+Mybatis-Plus+Shiro+JWT开发一个权限管理系统_第5张图片

核心代码解释

ShiroConfig

Shiro核心配置类,里面有很多对象,值得好好看看,这里不一一举例,以下是该类中将用户密码使用加密存储的配置

 /*
     * 凭证匹配器 由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");// 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1024);// 散列的次数,比如散列两次,相当于MD5(MD5(""));
        return hashedCredentialsMatcher;
    }

UserRealm

用户登陆时的认证和授权

@Slf4j
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RolePermissionService rolePermissionService;
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private MenuListService menuListService;
    @Autowired
    private RoleMenuListService roleMenuListService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 执行授权
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 设置角色
        List roles = roleService.selectRoles(SecurityUtil.getCurrentUser().getRoleId(), true);
        authorizationInfo.addRoles(roles.stream().map(Role::getName).collect(Collectors.toList()));
        List rolePermissions = rolePermissionService.lambdaQuery().eq(RolePermission::getRoleId, SecurityUtil.getCurrentUser().getRoleId()).list();
        if (CollectionUtil.isNotEmpty(rolePermissions)) {
            Set set = new HashSet<>();
            for (RolePermission rolePermission : rolePermissions) {
                List permissions = permissionService.lambdaQuery().eq(Permission::getId, rolePermission.getPermissionId()).list();
                set.addAll(permissions);
            }
            // 设置权限
            authorizationInfo.addStringPermissions(set.stream().map(Permission::getName).collect(Collectors.toList()));
        }
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        if (authenticationToken.getPrincipal() == null) {
            return null;
        }
        // 执行认证
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        User user = userService.selectByUsername(usernamePasswordToken.getUsername());
        // 判断用户
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        if (user.getState() == 0) {
            throw new DisabledAccountException("账号已被禁用!");
        }

        // 认证成功之后设置角色关联的菜单
        List roleMenuLists = roleMenuListService.lambdaQuery().in(RoleMenuList::getRoleId, user.getRoleId()).list();
        if (CollectionUtil.isNotEmpty(roleMenuLists)) {
            List collect = roleMenuLists.stream().map(RoleMenuList::getMenuListId).collect(Collectors.toList());
            List menuLists = menuListService.lambdaQuery().in(CollectionUtil.isNotEmpty(collect), MenuList::getId, collect).list();
            // 认证成功之后设置角色关联的菜单
            user.setMenuLists(CollectionUtil.isNotEmpty(collect) ? menuLists : null);
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(SystemConstants.JWT_SECRET_KEY), getName());
    }
}

JWTRealm

请求接口时需要携带token,该类用于对token的认证和授权

@Slf4j
public class JWTRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RolePermissionService rolePermissionService;
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private RoleService roleService;

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

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 执行授权
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 设置角色
        List roles = roleService.selectRoles(SecurityUtil.getCurrentUser().getRoleId(), true);
        authorizationInfo.addRoles(roles.stream().map(Role::getName).collect(Collectors.toList()));
        List rolePermissions = rolePermissionService.lambdaQuery().eq(RolePermission::getRoleId, SecurityUtil.getCurrentUser().getRoleId()).list();
        Set set = new HashSet<>();
        for (RolePermission rolePermission : rolePermissions) {
            List permissions = permissionService.lambdaQuery().eq(Permission::getId, rolePermission.getPermissionId()).list();
            set.addAll(permissions);
        }
        // 设置权限
        authorizationInfo.addStringPermissions(set.stream().map(Permission::getName).collect(Collectors.toList()));
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JwtUtils.getUsernameByToken(token);
        if (StrUtil.isBlank(username)) {
            throw new AuthenticationException("token认证失败!");
        }
        User user = userService.selectByUsername(username);
        // 判断用户
        if (user == null) {
            throw new AuthenticationException("用户不存在!");
        }
        if (user.getState() == 0) {
            throw new AuthenticationException("账号已被禁用!");
        }
        return new SimpleAuthenticationInfo(user, token, getName());
    }
}

NoSessionFilter

该类用于过滤请求,获取请求中的token,并对其认证

@Slf4j
public class NoSessionFilter extends BasicHttpAuthenticationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        // 1.从Cookie获取token
        String token = getTokenFromCookie(servletRequest);
        if (StrUtil.isBlank(token)) {
            // 2.从headers中获取
            token = servletRequest.getHeader(SystemConstants.TOKEN_HEADER);
        }
        if (StrUtil.isBlank(token)) {
            // 3.从请求参数获取
            token = request.getParameter(SystemConstants.TOKEN_HEADER);
        }
        if (StrUtil.isBlank(token)) {
            return false;
        }
        // 验证token
        token = token.replace(SystemConstants.TOKEN_PREFIX, "");
        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        // todo https://www.cnblogs.com/red-star/p/12121941.html https://blog.csdn.net/qq_43721032/article/details/110188342
        try {
            SecurityUtils.getSubject().login(jwtToken);
        } catch (Exception e) {
            return false;
        }
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        PrintWriter printWriter = response.getWriter();
        response.setCharacterEncoding("utf-8");
        printWriter.write("403");
        printWriter.flush();
        printWriter.close();
        return false;
    }

    private String getTokenFromCookie(HttpServletRequest request) {
        String token = null;
        Cookie[] cookies = request.getCookies();
        int len = null == cookies ? 0 : cookies.length;
        if (len > 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(SystemConstants.TOKEN_HEADER)) {
                    token = cookie.getValue();
                    break;
                }
            }
        }
        return token;
    }

    /**
     * 对跨域提供支持
     */
    @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);
    }
}

UserController

使用Shiro的注解对接口进行角色和权限的控制

/**
     * 分页查询用户
     * 需要管理员权限
     *
     * @param userVO
     * @return
     */
    @PostMapping("/selectPage")
    @RequiresRoles({"admin"})
    @RequiresPermissions({"user:select"})
    public Result selectPage(@RequestBody UserVO userVO) {
        return Result.success(userService.selectPage(userVO));
    } 
 

DefaultController

登陆时创建token返回给用户,同时将用户名和加密后的密码与数据库中比对

/**
     * 登录
     *
     * @param user 登录的用户对象
     * @return
     */
    @PostMapping("/login")
    public Result findByUsernameAndPassword(@RequestBody User user) {
        if (!SecurityUtils.getSubject().isAuthenticated()) {
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUsername(), user.getPassword(), true);
            try {
                // shiro验证用户名密码
                SecurityUtils.getSubject().login(usernamePasswordToken);
                // 生成token
                String token = JwtUtils.createToken(user.getUsername(), false);
                // 将用户户名和token返回
                HashMap map = new HashMap<>();
                map.put("username", user.getUsername());
                map.put("Authorization", token);
                map.put("role_id", SecurityUtil.getCurrentUser().getRoleId().toString());
                return Result.success(map);
            } catch (IncorrectCredentialsException e) {
                return Result.fail("登录密码错误");
            } catch (ExcessiveAttemptsException e) {
                return Result.fail("登录失败次数过多");
            } catch (LockedAccountException e) {
                return Result.fail("帐号已被锁定");
            } catch (DisabledAccountException e) {
                return Result.fail("帐号已被禁用");
            } catch (ExpiredCredentialsException e) {
                return Result.fail("帐号已过期");
            } catch (UnknownAccountException e) {
                return Result.fail("帐号不存在");
            } catch (UnauthorizedException e) {
                return Result.fail("您没有得到相应的授权");
            } catch (Exception e) {
                return Result.fail("出错了!!!");
            }
        }
        return Result.fail("你已经登录了");
    } 
 

总结

该系统使用Shrio框架对接口实现角色和权限的控制,角色权限和授权可通过用户交互界面动态实现,用户登录使用jwt单点登录,并将用户密码使用盐加密存储,从而保证了接口和用户密码的安全性。注意,不可将admin账户设置成禁用状态否则没办法登录(除了改数据库),应为状态一旦禁用只可以管理员进行解禁。每一个大型系统都会有权限的管理,权限管理在实际工作中经常遇到。

你可能感兴趣的:(java程序员安全)