第10章 权限控制、图形报表
1. 在项目中应用Spring Security
前面我们已经学习了Spring Security框架的使用方法,本章节我们就需要将Spring Security框架应用到
后台系统中进行权限控制,其本质就是认证和授权。
要进行认证和授权需要前面课程中提到的权限模型涉及的7张表支撑,因为用户信息、权限信息、菜单
信息、角色信息、关联信息等都保存在这7张表中,也就是这些表中的数据是我们进行认证和授权的依
据。所以在真正进行认证和授权之前需要对这些数据进行管理,即我们需要开发如下一些功能:
1、权限数据管理(增删改查)
2、菜单数据管理(增删改查)
3、角色数据管理(增删改查、角色关联权限、角色关联菜单)
4、用户数据管理(增删改查、用户关联角色)
鉴于时间关系,我们不再实现这些数据管理的代码开发。我们可以直接将数据导入到数据库中即可。
1.1 导入和配置
第一步:在health_parent父工程的pom.xml中导入Spring Security的maven坐标
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
第二步:在health_backend工程的web.xml文件中配置用于整合Spring Security框架的过滤器
DelegatingFilterProxy
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<security:http security="none" pattern="/login.html"></security:http>
<security:http security="none" pattern="/css/**"></security:http>
<security:http security="none" pattern="/img/**"></security:http>
<security:http security="none" pattern="/js/**"></security:http>
<security:http security="none" pattern="/plugins/**"></security:http>
<!--
auto-config:自动配置,如果设置为true,表示自动应用一些默认配置,比如框架会提供一个默认的登录页面
user-expressions:是否使用spring security提供的表达式来描述权限
-->
<security:http auto-config="true" use-expressions="true">
<security:access-denied-handler ref="myExceptionAdvice"/>
<security:headers>
<!-- 设置在页面可以通过iframe访问受保护的页面,默认为不允许访问,页面嵌套的要告诉具体策略-->
<security:frame-options policy="SAMEORIGIN"></security:frame-options>
</security:headers>
<!-- 配置拦截规则,/**表示拦截所有请求-->
<!--只要认证通过就可以访问-->
<security:intercept-url pattern="/pages/**" access="isAuthenticated()" />
<!--
pattern:描述拦截规则
asscess:指定所需的访问角色或者访问权限
-->
<!-- 如果我们要使用自己指定的页面作为登录页面,必须配置登录表单-->
<!-- form-login指定登录页面访问URL-->
<security:form-login
login-page="/login.html"
username-parameter="username"
password-parameter="password"
login-processing-url="/login.do"
default-target-url="/index.html"
authentication-failure-url="/login.html"></security:form-login>
<!-- csrf:对应CsrfFilter过滤器
disabled:是否启用CsrfFilter过滤器,如果使用自定义登录页面需要关闭此项,否则登录操作会被禁用(403)
-->
<security:csrf disabled="true"></security:csrf>
<!-- logout:退出登录
logout-url:退出登录操作采采芣苡的请求路径
logout-success-url:退出登录后的跳转页面
基于过滤器实现
-->
<security:logout logout-url="/logout.do"
logout-success-url="/login.html"
invalidate-session="true"/>
</security:http>
<!-- 配置认证管理器-->
<security:authentication-manager>
<!-- 配置认证提供者-->
<security:authentication-provider user-service-ref="springSecurityUserService">
<!-- 指定对密码加密的对象-->
<security:password-encoder ref="passwordEncoder"></security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<!-- 配置密码加密对象-->
<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="passwordEncoder"></bean>
<!-- 开启注解方式权限控制-->
<security:global-method-security pre-post-annotations="enabled"/>
<bean class="com.ybb.ex.MyExceptionAdvice" id="myExceptionAdvice"/>
</beans>
再写个类实现UserDetailsService接口
package com.ybb.service;
import com.alibaba.dubbo.config.annotation.Reference;
import com.ybb.pojo.Permission;
import com.ybb.pojo.Role;
import com.ybb.pojo.User;
import com.ybb.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Component
public class SpringSecurityUserService implements UserDetailsService {
//使用dubbo通过网络远程调用服务提供方获取数据库中的用户信息
@Reference
private UserService userService;
//根据用户名查询数据库获取用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUserName(username);
if(user == null){
//用户名不存在
return null;
}
List<GrantedAuthority> list = new ArrayList<>();
//动态为当前用户授权
Set<Role> roles = user.getRoles();
for (Role role : roles) {
//遍历角色集合,为用户授予角色
list.add(new SimpleGrantedAuthority(role.getKeyword()));
Set<Permission> permissions = role.getPermissions();
for (Permission permission : permissions) {
//遍历权限集合,为用户授权
list.add(new SimpleGrantedAuthority(permission.getKeyword()));
}
}
org.springframework.security.core.userdetails.User securityUser = new org.springframework.security.core.userdetails.User(username,user.getPassword(),list);
return securityUser;
}
}
这里踩了个坑,配置了成功的访问路径,但一直出问题,后面发现在web.xml中配置了个spring-redis和spring-security,出现了问题,不能这么配置,不规范
后面改动了工程,在springmvc中引入了这两个xml文件,就好了。注意配置规范
1.2controller,service,dao
这里因为涉及到动态的加载权限,要访问数据库,也涉及到多表联查,这里用的方式是用java代码简化xml配置的思路,根据id查询中间表,再移动到role,这是一块,后面再根据role的id查询中间表到permssion,又刚好pojo实体类中,user中有一个属性包含role的集合,role中又包含permission的集合,所以就简单了
userSerivce
public interface UserService {
User findByUserName(String username);
}
userSericeImpl
@Service(interfaceClass = UserService.class)
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private PermissionDao permissionDao;
//根据用户名查询书库获取用户信息和关联的角色信息,同时需要查询角色关联的权限信息
@Autowired
private MenuDao menuDao;
@Override
public User findByUserName(String username) {
User user = userDao.findByUserName(username);
if (user==null){
return null;
}
Integer userId = user.getId();
//根据用户ID查询对应的角色
Set<Role> roles = roleDao.findByUserId(userId);
for (Role role : roles) {
//角色ID,去查询关联权限
Integer roleId = role.getId();
Set<Permission> permissions = permissionDao.findByRoleId(roleId);
role.setPermissions(permissions);
}
user.setRoles(roles);
return user;
}
userDao
public interface UserDao {
User findByUserName(String username);
}
<?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.ybb.dao.UserDao">
-->
<select id="findByUserName" resultType="com.ybb.pojo.User" parameterType="string">
SELECT * FROM t_user where username=#{username}
</select>
</mapper>
RoleDao
public interface RoleDao {
Set<Role>findByUserId(Integer userId);
}
<?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.ybb.dao.RoleDao">
<select id="findByUserId" resultType="com.ybb.pojo.Role" parameterType="int">
SELECT r.* FROM t_user_role ur,t_role r where
ur.role_id=r.id and
ur.user_id=#{userId}
</select>
</mapper>
包扫描记得动一下
<!--批量扫描--> <dubbo:annotation package="com.全局" />
1.3权限
这里用注解的方式实现权限的给予
@PreAuthorize("hasAuthority('CHECKITEM_DELETE')")//权限校验
@RequestMapping("/delete")
public Result delete(Integer id){
try {
checkItemService.deleteById(id);
}catch (Exception e){
return new Result(false,MessageConstant.DELETE_CHECKGROUP_FAIL);
}
return new Result(true, MessageConstant.DELETE_CHECKGROUP_SUCCESS);
}
权限和角色是有层级关系的,但对于spring来说都只是个字符串而已,当要涉及到角色或者权限验证的时候,就去这个集合中找有没有这个字符串,没有就403.
List<GrantedAuthority> list = new ArrayList<>();
1.4 403时如何处理给用户看
spring这有个接口AccessDeniedHandler,专门处理403情况的,不过也算个异常,自己可以写个异常处理器来实现。这里博主比较懒,基于spring框架来实现的。
package com.ybb.ex;
import com.alibaba.fastjson.JSON;
import com.ybb.entity.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Description :spring security内部封装了权限出问题的异常处理器,如果出异常就会跑这来
* Version :1.0
*/
public class MyExceptionAdvice implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = new Result(false,"出问题啦");
String data = JSON.toJSON(result).toString();
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(data);
}
}
然后配置进spring-security中,注意插入位置
1.5显示用户名
前面我们已经完成了认证和授权操作,如果用户认证成功后需要在页面展示当前用户的用户名。Spring
Security在认证成功后会将用户信息保存到框架提供的上下文对象中,所以此处我们就可以调用Spring
Security框架提供的API获取当前用户的username并展示到页面上。
实现步骤:
第一步:在main.html页面中修改,定义username模型数据基于VUE的数据绑定展示用户名,发送ajax
请求获取username
new Vue({
el: '#app',
data: {
username: null,
},
created(){
axios.get("/user/getUsernameAndMenu.do").then((res)=>{
if (res.data.flag){
this.username=res.data.data.username
}
})
},
package com.ybb.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.ybb.constant.MessageConstant;
import com.ybb.entity.Result;
import com.ybb.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.swing.*;
/**
* Description :
* Version :1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Reference
private UserService userService;
//获得当前登录用户的用户名
@RequestMapping("/getUsernameAndMenu")
public Result getUsername(){
//当spring security完成认证后,会将当前用户信息保存到框架提供的上下文对象(基于session)
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(user);
if (user!=null){
return new Result(true, MessageConstant.GET_MENU_SUCCESS,user.getUsername);
}
return new Result(false,MessageConstant.GET_USERNAME_FAIL);
}
}
1.6 用户退出
a标签加security配置,结束
2. 图形报表ECharts
2.1 ECharts简介
ECharts缩写来自Enterprise Charts,商业级数据图表,是百度的一个开源的使用JavaScript实现的数据
可视化工具,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,
Chrome,Firefox,Safari等),底层依赖轻量级的矢量图形库 ZRender,提供直观、交互丰富、可高
度个性化定制的数据可视化图表
https://echarts.apache.org/examples/zh/index.html
下载什么的就不说了。只需要引入echarts.js文件就能用了
2.2会员数量折线图
2.21 需求分析
会员信息是体检机构的核心数据,其会员数量和增长数量可以反映出机构的部分运营情况。通过折线图
可以直观的反映出会员数量的增长趋势。本章节我们需要展示过去一年时间内每个月的会员总数据量。
展示效果如下图:
2.22 完善页面
记得引入echats.js文件
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>传智健康</title>
<meta name="description" content="传智健康">
<meta name="keywords" content="传智健康">
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
<!-- 引入样式 -->
<link rel="stylesheet" href="../css/style.css">
<script src="../plugins/echarts/echarts.js"></script>
</head>
<body class="hold-transition">
<div id="app">
<div class="content-header">
<h1>统计分析<small>会员数量</small></h1>
<el-breadcrumb separator-class="el-icon-arrow-right" class="breadcrumb">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>统计分析</el-breadcrumb-item>
<el-breadcrumb-item>会员数量</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="app-container">
<div class="box">
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="chart1" style="height:600px;"></div>
</div>
</div>
</div>
</body>
<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../js/axios-0.18.0.js"></script>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart1 = echarts.init(document.getElementById('chart1'));
// 使用刚指定的配置项和数据显示图表。
//myChart.setOption(option);
axios.get("/report/getMemberReport.do").then((res)=>{
myChart1.setOption(
{
title: {
text: '会员数量'
},
tooltip: {},
legend: {
data:['会员数量']
},
xAxis: {
data: res.data.data.months
},
yAxis: {
type:'value'
},
series: [{
name: '会员数量',
type: 'line',
data: res.data.data.memberCount
}]
});
});
</script>
</html>
2.23 后台代码
这里实现的业务逻辑是比如第一个月1个,第二个月加了2个,展示的3,累加的效果。
package com.ybb.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.ybb.constant.MessageConstant;
import com.ybb.entity.Result;
import com.ybb.service.MemberService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Description :
* Version :1.0
*/
@RestController
@RequestMapping("/report")
public class report {
@Reference
private MemberService memberService;
@RequestMapping("/getMemberReport")
public Result getMemberReport() {
//需要往前端传两个数组对象,用map封装
HashMap<String, Object> map = new HashMap<>();
List<String> months = new ArrayList<>();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM");
//先要计算过去一年的12个月
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MONTH, -12);
for (int i = 0; i < 12; i++) {
calendar.add(Calendar.MONTH, 1);
months.add(format.format(calendar.getTime()));
}
map.put("months",months);
List<Integer> memberCountByMonths = memberService.findMemberCountByMonths(months);
map.put("memberCount",memberCountByMonths);
return new Result(true, MessageConstant.GET_MEMBER_NUMBER_REPORT_SUCCESS,map);
}
}
MemberService
List<Integer>findMemberCountByMonths(List<String>months);
MemberServiceImpl
package com.ybb.service.Impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.ybb.dao.MemberDao;
import com.ybb.pojo.Member;
import com.ybb.service.MemberService;
import com.ybb.utils.MD5Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
* Description :
* Version :1.0
*/
@Service(interfaceClass = MemberService.class)
@Transactional
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberDao memberDao;
//根据月份查询会员数量
@Override
public List<Integer> findMemberCountByMonths(List<String> months) {
List<Integer>memberCount=new ArrayList<>();
for (String month : months) {
String date = month + ".31";
Integer memberCountBeforeDate = memberDao.findMemberCountBeforeDate(date);
memberCount.add(memberCountBeforeDate);
}
return memberCount;
}
}
dao
public Integer findMemberCountBeforeDate(String date);
<!--根据日期统计会员数,统计指定日期之前的会员数-->
<select id="findMemberCountBeforeDate" parameterType="string" resultType="int">
select count(id) from t_member where regTime <= #{value}
</select>