1

路由

开源框架若依系统中,其前端菜单列表,即我们技术上通常所说的路由信息,是通过接口来返回给前端动态生成的。我们通过抓取接口发现,其接口路径为:getRouters。今天我们来通过接口代码,逐渐深入来研究若依框架中其菜单表设计结构。

getRouters

该接口位于ruoyi-admin模块controller包下的SysLoginController中,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}

代码第一行,是后台程序通过过滤器等来逐步获取用户的userId,这一步我们不多做阐述。

第二行,通过userId获取菜单树结构,我们来深入看一下其中的执行逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据用户ID查询菜单
*
* @param userId 用户名称
* @return 菜单列表
*/
@Override
public List<SysMenu> selectMenuTreeByUserId(Long userId)
{
List<SysMenu> menus = null;
if (SecurityUtils.isAdmin(userId))
{
menus = menuMapper.selectMenuTreeAll();
}
else
{
menus = menuMapper.selectMenuTreeByUserId(userId);
}
return getChildPerms(menus, 0);
}

其执行逻辑为:首先判断当前用户是否是超级管理员admin,如果不是,则通过方法:selectMenuTreeByUserId来查询用户的菜单。我们暂且放一下最后一句话,先看这一句:

1
menus = menuMapper.selectMenuTreeByUserId(userId);

其对应的执行mysql语句为:

1
2
3
4
5
6
7
8
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
from sys_menu m
left join sys_role_menu rm on m.menu_id = rm.menu_id
left join sys_user_role ur on rm.role_id = ur.role_id
left join sys_role ro on ur.role_id = ro.role_id
left join sys_user u on ur.user_id = u.user_id
where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0
order by m.parent_id, m.order_num

跟权限类似,其查询逻辑为:用户 -> 用户角色对应表->用户角色->角色菜单对应表->菜单。除一般的有效查询条件外(即state=1或者status=0,当然,如果要说的话,就是很奇怪为什么要把0设为正常,把1设为失效或停用),我们来看下面这一查询条件:

1
m.menu_type in ('M', 'C') 

我们通过数据库,查看其数据表设计,其字段内容menu_type,如下:

image-20220404150900001

即查询菜单的时候,只查询目录以及菜单,而不查询按钮类别。

如此,我们便把指定用户可以查看的所有目录,菜单都罗列了出来,反过来,我们再去看那一行代码:

1
getChildPerms(menus, 0);

深入方法内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 根据父节点的ID获取所有子节点
*
* @param list 分类表
* @param parentId 传入的父节点ID
* @return String
*/
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId)
{
List<SysMenu> returnList = new ArrayList<SysMenu>();
for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();)
{
SysMenu t = (SysMenu) iterator.next();
// 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
if (t.getParentId() == parentId)
{
recursionFn(list, t);
returnList.add(t);
}
}
return returnList;
}

不难看出,其实现主要是将菜单的链表list转换为一个“目录-菜单”的树状结构。

到这里结束了吗?不我们还有Controller中的最后一句没有看:

1
return AjaxResult.success(menuService.buildMenus(menus));

前面的AjaxResult.success(xxx)我们不阐述,其表示成功返回数据,我们展开方法buildMenus(menus)看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 构建前端路由所需要的菜单
*
* @param menus 菜单列表
* @return 路由列表
*/
@Override
public List<RouterVo> buildMenus(List<SysMenu> menus)
{
List<RouterVo> routers = new LinkedList<RouterVo>();
for (SysMenu menu : menus)
{
RouterVo router = new RouterVo();
router.setHidden("1".equals(menu.getVisible()));
router.setName(getRouteName(menu));
router.setPath(getRouterPath(menu));
router.setComponent(getComponent(menu));
router.setQuery(menu.getQuery());
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
List<SysMenu> cMenus = menu.getChildren();
if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()))
{
router.setAlwaysShow(true);
router.setRedirect("noRedirect");
router.setChildren(buildMenus(cMenus));
}
else if (isMenuFrame(menu))
{
router.setMeta(null);
List<RouterVo> childrenList = new ArrayList<RouterVo>();
RouterVo children = new RouterVo();
children.setPath(menu.getPath());
children.setComponent(menu.getComponent());
children.setName(StringUtils.capitalize(menu.getPath()));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
children.setQuery(menu.getQuery());
childrenList.add(children);
router.setChildren(childrenList);
}
else if (menu.getParentId().intValue() == 0 && isInnerLink(menu))
{
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
router.setPath("/inner");
List<RouterVo> childrenList = new ArrayList<RouterVo>();
RouterVo children = new RouterVo();
String routerPath = StringUtils.replaceEach(menu.getPath(), new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" });
children.setPath(routerPath);
children.setComponent(UserConstants.INNER_LINK);
children.setName(StringUtils.capitalize(routerPath));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
childrenList.add(children);
router.setChildren(childrenList);
}
routers.add(router);
}
return routers;
}

这里对菜单的判断主要有以下几种:

  1. 子菜单非空的“目录”
  2. 外链
  3. 路由地址为http开头的内链(即isInnerLink执行的判断)

如此,便成功的将存在数据库中的目录菜单,转换为了vue前端需要的路由地址映射结构,具体的,我们来看一下,前端的路由结构样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
path: '',
component: Layout,
redirect: 'index',
children: [
{
path: 'index',
component: (resolve) => require(['@/views/index'], resolve),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
}

1

接口入手

我们发现,若依的getInfo接口获取了用户的roles与permissions以及用户的基本信息,我们来研究一下其获取逻辑与sql,从而尝试从接口与sql研究其权限表设计结构。

getInfo接口

在Controller中,其代码实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return ajax;
}

我们展开看一下其中的角色集合与权限集合方法。

1
2
3
4
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);

getRolePermission

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取角色数据权限
*
* @param user 用户信息
* @return 角色权限信息
*/
public Set<String> getRolePermission(SysUser user)
{
Set<String> roles = new HashSet<String>();
// 管理员拥有所有权限
if (user.isAdmin())
{
roles.add("admin");
}
else
{
roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
}
return roles;
}

对于管理员,仅需要添加admin即可,否则则需要按照用户id去查询用户权限,我们再次深入,展开selectRolePermissionByUserId方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据用户ID查询权限
*
* @param userId 用户ID
* @return 权限列表
*/
@Override
public Set<String> selectRolePermissionByUserId(Long userId)
{
List<SysRole> perms = roleMapper.selectRolePermissionByUserId(userId);
Set<String> permsSet = new HashSet<>();
for (SysRole perm : perms)
{
if (StringUtils.isNotNull(perm))
{
permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));
}
}
return permsSet;
}

方法第一句调用mapper中的方法,实际是执行sql查询,我们看一下其执行的sql:

1
2
3
4
5
6
7
select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly,
r.status, r.del_flag, r.create_time, r.remark
from sys_role r
left join sys_user_role ur on ur.role_id = r.role_id
left join sys_user u on u.user_id = ur.user_id
left join sys_dept d on u.dept_id = d.dept_id
WHERE r.del_flag = '0' and ur.user_id = #{userId}

由此,得到其角色表主表为sys_role,角色与用户关联表为sys_user_role。从表命名上我们也能看出一二来。

getMenuPermission

同样的,我们展开权限方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取菜单数据权限
*
* @param user 用户信息
* @return 菜单权限信息
*/
public Set<String> getMenuPermission(SysUser user)
{
Set<String> perms = new HashSet<String>();
// 管理员拥有所有权限
if (user.isAdmin())
{
perms.add("*:*:*");
}
else
{
perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
}
return perms;
}

若是admin,则只添加*:*:*,否则执行方法:selectMenuPermsByUserId,我们再次深入展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据用户ID查询权限
*
* @param userId 用户ID
* @return 权限列表
*/
@Override
public Set<String> selectMenuPermsByUserId(Long userId)
{
List<String> perms = menuMapper.selectMenuPermsByUserId(userId);
Set<String> permsSet = new HashSet<>();
for (String perm : perms)
{
if (StringUtils.isNotEmpty(perm))
{
permsSet.addAll(Arrays.asList(perm.trim().split(",")));
}
}
return permsSet;
}

同样的,关键语句在于sql查询selectMenuPermsByUserId:

1
2
3
4
5
6
select distinct m.perms
from sys_menu m
left join sys_role_menu rm on m.menu_id = rm.menu_id
left join sys_user_role ur on rm.role_id = ur.role_id
left join sys_role r on r.role_id = ur.role_id
where m.status = '0' and r.status = '0' and ur.user_id = #{userId}

我们发现,若依系统中,获取的权限实际上都属于“菜单”的权限,注意,这里的菜单包括实际意义的菜单以及菜单中的按钮,即包括下图两个东西:

image-20220403230656975

获取权限,从其执行的sql来看,其实际逻辑为:

用户->用户的角色->角色关联的菜单->返回菜单的权限字符。即:

sys_user -> sys_user_role -> sys_role_menu -> sys_menu

这样避免了空权限(我所指的空权限是有此权限而没有实际含义),即每个权限都有对应的菜单或按钮。

从菜单的前端到后端

在上一篇文章《开源框架若依中的权限控制逻辑-菜单》中,我们介绍了若依系统中的菜单管理的相关操作,最后遗留了一个小问题,为什么菜单的“路由参数”设置为非空时,菜单不会在左侧出现。今天我们就来深入的看看若依系统的菜单返回逻辑与后台数据设计。

接口捕获

我们打开若依系统,刷新页面,通过浏览器调试窗口看看调用了哪些网络请求:

在若依首页,我们F12打开浏览器调试窗口,并切换到“网络”页面,勾选过滤条件“Fetch/XHR”,然后刷新页面。捕获的请求如下图所示:

image-20220403185629457

我们依次查看各个请求分别获取了什么信息。

getInfo

getInfo的全部请求信息如下:

请求网址: http://localhost/dev-api/getInfo, 请求方法: GET。

请求标头包括Authorization,Cookie等信息。

返回信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{
"msg": "操作成功",
"code": 200,
"permissions": [
"*:*:*"
],
"roles": [
"admin"
],
"user": {
"searchValue": null,
"createBy": "admin",
"createTime": "2021-10-12 08:45:24",
"updateBy": null,
"updateTime": null,
"remark": "管理员",
"params": {},
"userId": 1,
"deptId": 103,
"userName": "admin",
"nickName": "若依",
"email": "ry@163.com",
"phonenumber": "15888888888",
"sex": "1",
"avatar": "",
"salt": null,
"status": "0",
"delFlag": "0",
"loginIp": "127.0.0.1",
"loginDate": "2022-04-03T17:06:44.000+08:00",
"dept": {
"searchValue": null,
"createBy": null,
"createTime": null,
"updateBy": null,
"updateTime": null,
"remark": null,
"params": {},
"deptId": 103,
"parentId": 101,
"ancestors": null,
"deptName": "研发部门",
"orderNum": "1",
"leader": "若依",
"phone": null,
"email": null,
"status": "0",
"delFlag": null,
"parentName": null,
"children": []
},
"roles": [
{
"searchValue": null,
"createBy": null,
"createTime": null,
"updateBy": null,
"updateTime": null,
"remark": null,
"params": {},
"roleId": 1,
"roleName": "超级管理员",
"roleKey": "admin",
"roleSort": "1",
"dataScope": "1",
"menuCheckStrictly": false,
"deptCheckStrictly": false,
"status": "0",
"delFlag": null,
"flag": false,
"menuIds": null,
"deptIds": null,
"admin": true
}
],
"roleIds": null,
"postIds": null,
"roleId": null,
"admin": true
}
}

其返回信息主要包括:权限permissions, 角色roles,当前登录用户信息user,user中又包括当前用户的基础信息,部门信息,角色信息等。

我们后台来查看其接口执行逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return ajax;
}

首先,通过Spring-Security来根据登录token信息获取用户信息,即:

1
SysUser user = SecurityUtils.getLoginUser().getUser();

我们依次深入方法,其调用链条:

1
SecurityContextHolder.getContext().getAuthentication().getLoginUser()

其设置位置位于过滤器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.ruoyi.framework.security.filter;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.TokenService;

/**
* token过滤器 验证token有效性
*
* @author ruoyi
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}

在token转换为用户信息的关键代码是:

1
LoginUser loginUser = tokenService.getLoginUser(request);

其方法体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
}
}
return null;
}

如此,便通过将token转换为了保存在redis中的用户信息。

起源

目前我司生产环境使用的后台服务系统为:后端国产开源框架jfinal+前端开源框架vue-element-admin组成的前后端分离的综合管理系统。目前该套系统已实现约两三个子功能模块。

上周需求方提出,需要给后台管理系统增加上权限管理功能,经过调研发现,若依框架中的权限管理比较符合需求方理想中的权限管理,因此对若依中的权限管理展开了学习。

若依系统中的角色管理

下载若依系统源码后,按照官网文档中的提示,配置数据库、端口等,我们分别打开前后端项目,如图所示:

image-20220403175721228

前端项目目录如下图:

image-20220403175803794

我们分别运行前后端:

后端运行入口为ruoyi-admin模块的RuoYiApplication.java;

前端在ruoyi-ui目录下执行命令:npm run dev。

运行结果,可以在浏览器中查看到如下页面:

image-20220403180132046

输入验证码后进入系统,页面如下:

image-20220403180250477

可以看到,其系统管理大致分为用户管理、角色管理、菜单管理,部门管理,岗位管理等。

今天我们先来看一下其中的菜单管理。主页面如图所示:

image-20220403181234671

新增功能分为新增目录,新增菜单,新增按钮,如图所示为新增目录的窗口:

image-20220403181448020

新增菜单时的窗口如下:

image-20220403181849866

新增按钮的窗口如下所示:

image-20220403181920042

相比目录来说,新增菜单多了四个需要填写的内容,分别是组件路径,权限字符,路由参数,是否缓存。新增按钮功能相比目录增加了权限字符。

我们分别操作尝试一下:

image-20220403182427922

image-20220403182739070

image-20220403183115858

操作完后,我们刷新页面:

image-20220403183212203

可以看到,左侧新增了“测试目录”这一目录,但是点开并没有子菜单。我们经过测试,发现,只有将“测试菜单”的权限修改为目前已经可以查看的一个权限字符,并且将其路由参数设为空时,左侧菜单栏才会有“测试菜单”出现。

有过vue前端开发经验的人应该对权限字符并不陌生,它表示一个菜单我们是否有权查看,其位于路由组件部分的permissions字段中。

路由参数是什么意思呢?为什么写了路由参数的菜单不会默认出现在左侧栏中呢?时间关系,我们另外再阐述。

1

起源

最近要学习使用shiro做后端接口的权限校验,生产环境目前使用的开源框架jfinal,本文我们将来看看如何在jfinal中实现shiro嵌入与使用。

当前环境jfinal版本

目前使用的jfinal版本为:4.8。

1
2
3
4
5
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>jfinal</artifactId>
<version>4.8</version>
</dependency>

maven引入shiro

在网站https://shiro.apache.org/download.html 中我们可以查看到所有可用的shiro maven配置,我们首先尝试使用shiro-web,即引入:

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.9.0</version>
</dependency>

按照官网示例,我们在resources目录下,创建文件shiro.ini,配置以下内容:

1
2
3
#[main]
realm=com.yourpackage.Realm
securityManager.realm=$realm

实现我们自己的Realm类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.youpackage.test;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.ArrayList;
import java.util.List;

public class Realm extends AuthorizingRealm {

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//认证的实体信息, 可以放对象,以后可以随时取出来用。
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 用户名
Object principal = token.getPrincipal();
// 密码
Object credentials = token.getCredentials();
// TODO 执行数据库数据获取与算法认证
//封装一个带数据的对象,Shiro会拿这个对象和传进来的Token的密码进行对比,验证是否登录成功
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, token.getName());
return simpleAuthenticationInfo;
}


//这个方法是用来授权的
//查询登陆人是否有权限时就查询这个 如果设置了缓存,只会查一次
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
List<String> permissions=new ArrayList<>();
List<String> roles=new ArrayList<>();
String username= (String) principalCollection.getPrimaryPrincipal();
permissions.add("sys:test:user");
roles.add("abc");
info.addRoles(roles);//设置角色
info.addStringPermissions(permissions);//设置权限
return info;
}
}

第二个方法我们先放一放,首先来看第一个方法,该方法是用来获取用户验证信息。我们需要嵌入自己数据库的密码验证逻辑。其中的token对象我们通过获取getPrincipal与getCredentials我们可以获取到用户登录的用户名与密码。

principals,身份,主体的标识,如用户名,邮箱,电话等。

credentials,证明/凭证,只有主体知道的安全值,如密码,数字整数。

在登录接口中我们实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// XXXController.java中

@Clear(AuthInterceptor.class)
public void shiroLogin() {
Subject currentUser = SecurityUtils.getSubject();
String username = getPara("usename");
String password = getPara("password");
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 调用此行代码时,会调用到Realm中继承的第一个方法。
currentUser.login(token);
// 还需要返回给前端token
renderAppMsg("登录成功");
}

这段代码首先排除验证器,因为登录之前肯定是没有验证的。

然后在接口内部获取用户名与密码,构造UsernamePasswordToken对象,调用Subject.login进行登录,从而自动进行用户名密码验证。

另外,我们需要在jfinal环境中配置Security Manager。在jfinal程序入口,在其方法configPlugin中进行以下配置:

1
2
3
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

如此,便可以通过接口登录,并使用shiro的验证机制,并嵌入自己的用户名,密码验证算法。

1

起源

最近,工作环境要求引入权限管理系统,提出了四个可能的方案,大致是:

第一种:自我设计与实现;

第二种:参考现有较为成熟的框架,如若依系统中的spring-security,或开源的shiro。

第三种:参考现有系统,复制其数据表,直接在其权限框架下做业务;

第四种:其他。

本文就来自于我对shiro的调研情况。

shiro是什么

shiro,官网地址:https://shiro.apache.org/,从其官网地址不难猜出,其是apache基金会下的一个开源项目。

根据其介绍,Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。

废话少说,看代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testHelloworld() {
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2、得到SecurityManager实例 并绑定给SecurityUtils
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
//4、登录,即身份验证
subject.login(token);
} catch (AuthenticationException e) {
//5、身份验证失败
}
Assert.assertEquals(true, subject.isAuthenticated()); //断言用户已经登录
//6、退出
subject.logout();
}

这个验证的例子是官网以及一个博主发的例子,其中我们将配置文件shiro.ini配置为以下内容:

1
2
3
#[main]
realm=com.xxx.test.Realm
securityManager.realm=$realm

对应的Realm文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.holdoa.test;

import com.holdoa.core.model.SysUser;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class Realm extends AuthorizingRealm {


//这个方法是用来授权的
//查询登陆人是否有权限时就查询这个 如果设置了缓存,只会查一次
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// Object user = principalCollection.getPrimaryPrincipal();
// SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// return info;
return null;
}

/**
* 获取用户验证信息
* @param authenticationToken 所需验证的token
* @return null or 身份信息
* @throws AuthenticationException 验证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 断点位置
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
token.getUsername();
// 用户名
Object principal = token.getPrincipal();
// 密码
Object credentials = token.getCredentials();

// TODO 执行数据库数据获取与算法认证
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, getName());
return simpleAuthenticationInfo;
}
}

经过运行,我们发现代码成功的在Realm第二个方法被断点解惑,从而我们可以在这里嵌入我们数据库的以及特定算法要求来验证登录是否正确,很简单吧!

说回四个方案

文章开头提到的四种方案,我个人倾向于“第一种+第三种”,借鉴若依框架的实现逻辑,结合现有数据库结构来实现。

现有的框架前后端分离,权限管理系统首先要做满足的一点是可以通过纯后台的配置来修改权限,而不需要前端做修改,这需要将前端页面的路由信息动态生成,即:用户登录 -> 获取用户角色 -> 返回用户权限-> 返回用户路由列表。

其中,“用户权限”一方面控制用户路由,另一方面控制每个路由页面上的按钮的显示情况。

概念

归一化,原本是物理学上的概念,指的是将有量纲(即带单位)的表达式,经过转换,化为无量纲的表达式,成为标量。

在机器学习中的归一化,也叫标准化,就是将需要处理的特征数据经过算法处理后,限定在一定的范围内,通常是【0, 1】或【-1, 1】。

通常是由于数据的各个特征的计量单位差异较大,从而造成数据特征在执行机器学习算法中所占的特征比重不同,因此需要进行归一化。

例如,在《机器学习实战》一书中提到的“改进约会网站的配对效果”,在数据中,有以下三个特征:

每年获得的飞行常客里程数

玩视频游戏所耗时间百分比

每周消费的冰淇淋公升数

三者的计量单位没有可比性,从而数值也没有可比性,比如飞行里程数可能达到134000,而玩游戏百分比范围为0-100,如果直接按照欧式距离来计算,则里程数在其中所占的比重是相当大的。那么,如何让两个特征值站在同一个起跑线呢,那就是归一化算法要解决的问题。

线性归一化

最常用的一种归一化方法,对原数据按照以下算法执行:

1
newValue = (oldValue - min) /  (max - min)

其中,oldValue表示特征的原始数值,min表示样本中该特征的最小值或允许的最小值,max表示样本中该特征的最大值或者可能的最大值,newValue即为归一化之后的特征值,通过该算法,总是能将特征限定在[0, 1]之间。

该方法缺点是容易受到极值影响。

标准差归一化

该方法也称为Z-score标准化。首先计算出某一个特征的均值μ,标准差σ,处理算法为:

1
newValue = (oldValue -  μ) / σ

其他非线性归一化方法大多数使用log函数、指数函数等,将数据映射到同一个范围区间内,以便或许使用。

因人之力而敝之,不仁。失其所与,不知。以乱易整,不武。——古文观止 · 烛之武退秦师

在学习机器学习书籍时,一般都推荐python,matlab或者octave首先来做算法实现。

在Python中,涉及到一个非常好用的绘图库就是matplotlib。

今天我们来介绍一个matplotlib的基本操作。

基础演示

首先我们看一个基本的样例:

1
2
3
4
5
6
7
8
9
10
import numpy as np
from matplotlib import pyplot as plt

x = np.arange(1, 15)
y = 2 * x + 5
plt.title("demo")
plt.xlabel("x")
plt.ylabel("y")
plt.plot(x, y)
plt.show()

运行结果如下图所示:

image-20220318151128220

上述代码是Matplotlib与numpy库一起使用的一个基础样例。

下面列举几个Matplotlib中常用的函数以及基础的使用样例。

plot函数

plot函数调用有两种形式,我们可以通过help命令查看:

1
2
plot([x], y, [fmt], data=None, **kwargs)
plot([x], y, [fmt], [x2], y2, [fmt2], ..., **kwargs)

其中中括号扩起来的部分表示可选参数。

其中, fmt表示线/点的样式,包括颜色,点型,线型。fmt = ‘[颜色] [点型] [线型]’,例如plot(x, y, 'b+-')表示绘制蓝色+号实线图,如下所示:

image-20220318155042373

也可以按照如下代码编写:

1
plt.plot(x, y, color='blue', marker='+', linestyle='-')

这一句的效果与上面的plot(x, y, 'b+-')是一样的,其他可添加的属性还包括:

线条宽度:linewidth, 点标记大小:markersize,其他相关属性可参考:line2D

subplot函数

见字知意,该函数表示向现有图形窗口中添加子图。来看示例:

1
subplot(nrows, ncols, index, **kwargs)

其中的参数分别表示子图在整个图形窗口网格中的位置与索引。

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
from matplotlib import pyplot as plt

x = np.arange(1, 15)
y = 1 * x + 5
y2 = 2 * x + 0
y3 = 5 * x + 0
y4 = 10 * x + 0
plt.subplot(2, 2, 1)
plt.xlim((0, 15))
plt.ylim((0, 150))
plt.title("y")
plt.plot(x, y)
plt.subplot(2, 2, 2)
plt.xlim((0, 15))
plt.ylim((0, 150))
plt.title("y2")
plt.plot(x, y2)
plt.subplot(2, 2, 3)
plt.xlim((0, 15))
plt.ylim((0, 150))
plt.title("y3")
plt.plot(x, y3)
plt.subplot(2, 2, 4)
plt.xlim((0, 15))
plt.ylim((0, 150))
plt.title("y4")
plt.plot(x, y4)
plt.show()

输出结果如下所示:

image-20220318164444557

其中,可以看到,通过nrows与ncols将整个视窗划分为nrows*ncols的网格,然后通过index按照从左到右,从上到下的次序依次补充图形。当然,图形并不总是相同的,还可以进行如下的绘制:

image-20220318165228992

scatter 散点图

1
matplotlib.pyplot.scatter(x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, *, edgecolors=None, plotnonfinite=False, data=None, **kwargs)

这个函数需要引起重视,为什么呢?

因为在机器学习中,我们常常需要查看原始数据的分布情况,得出一个初步的结论,这是就会用到散点图了。

散点图scatter各个参数的具体含义可以参考链接

其他常用函数:

函数 含义
pyplot.bar 柱状图
pyplot.pie 饼图

https://zhuanlan.zhihu.com/p/481675767

1990年每年造的房地产面积 1000万平米,2000以来房地产商市场化建房面积达到 1亿平米,到了2010年房地产商建的房子面积达到10亿平米,2017年中国房产商当年建设的房地产面积达到17亿平方米,达到顶峰不再增长。

眼下中国房地产,进入新阶段,指标出现了拐点

第一个指标:大家基本认为14亿人口是天花板,因为人口逐渐在减少。

第二个指标:中国的城市化率已经达到65%,几乎等同于欧洲、美国的75%,中国的城市化率实际上是把农村里的青年人和中年人抽出来,再增长的话5-6%到头。

第三个指标:中国的老龄化已经达到20%以上,到了2030年会达到30%以上,甚至2050、2060会达到40-50%都有可能, 老龄人越多需要的房子就越少,反而死亡的人越多会退出许多房子来。

这三个指标都会影响住房结构,对新房子的需求会大幅下降。

第四个指标:中国房地产商手中造好的房子,一年以上没有买掉的有6亿,这就是一个库存,中国老百姓买下的房子没有住的,即投资房占比20%,中国总的房屋有400亿平方米,90年后以来新建的商品房在老百姓手中的有300亿平米,这300亿平方米有60亿是空关闲置的。

这种闲置每年涨20-30%,5年翻一番,手里有1-2套房子闲置的人在房价涨价的预期下也不怕,房产商房子没有卖掉,但是房子在涨价,他会觉得房子没有卖点反而划算;但是房价如果不涨了,这个库存就会造成积压,需求会大幅萎缩。

第五个指标:整个中国目前人均住房,19年统计时48平方米,到了2020年已经涨到50平方米。全世界发达国家里,欧洲、美国50平米人均就是天花板;40年前我们人均5平方,现在变成50平方,上了天花板。这个再空洞的需求,想泡沫化的需求都泡沫不出来了。

第六个指标:年度的投资,2000年时1亿平米,到了2010年是10亿,到了2016-2017是17亿平米,这17年里面造房的建设量涨了16倍。20年翻了4番,也是个天花板了。

2020年,全世界新建的房子就是20亿平方(除中国之外),而中国就造了17亿平米;接近全球的一半。中国只占全球人口的20%凭什么要造全球将近一半的房子,而且人均居住面积已经达到50平米,天花板的水平。国内房地产市场已经没有任何高速增长的内在需求了。

三个指标是人口指标在吐出需求,三个指标是人均居住面积和建设量都严重过剩。

第七个,中国的土地价格和房地产的价格,翻了四番,上海最贵的地方衡山路那时候的房价是6000多块,算7000块。涨了20倍的话也是14万每平;在北京王府井最高的地段,你用20年前去计算,现在总归也是16倍以上;现在你讲新疆、乌鲁木齐二道桥2000年时是700快一平方,20年后一万几到两万每平米了。全国在原来的基础上,20年翻了四番,土地价格每亩从100万,变成1000万,变成2000万,也是十倍十几倍的涨;当一个地方二十年翻四番的趋势存在的时候,所有房产商都会囤房,那怕造了房子卖不掉,过个5年也能卖掉,所有大家都不怕库存,不怕规模,然后拼命的融资贷款,这个规律前面20年一直如此。

第八个指标,危旧房改造的指标,基础设施建设大规模的也差不多了,没有这个需求了。

第九个指标,城市的学校、医院也到头了,现在大学过剩了。80我们大学生在校生的占地,人均只有10平米,教育部当时定了一个制度,大学在校生,人均学校占地50平方米。现在真个中国 3000多个大学,人均50平方已经到了。所以也不需要再扩张,也不需要变成4000所大学。教育公共设施也到头了。

第十个指标,金融,房地产金融;中国的房地产商负债率都在90,你总不能零资产。房地产商的杠杆从10%、20%到80%、90%的负债率基本上也上了天花板,在过十年也不会变成105%的。

在这个意义上,不管是举债,还是公共基础设施,还是就成改造,还是房地产的四、五个指标,还是老年化城市人群,这个概念到这,可以这么说,新阶段,新常态要出现了。

现在我们说房地产出现困境,有人把他归结我们是我们地方政府呀,国家的房地产调控政策,过头了,太集中了,太厉害了,把它给打压了,这是不公平的,不合理的,是不对的。

你要从终极的供需求关系,这30年的演变来看,它不是政府调控过头,造成现在的困难,因为房地产上1985年1990年,或者1995年,或者2005年到2010年,他们都是这么的行为方式。

为什么当时不破产,现在这些龙头企业集中破产呢?就是当你这样的行为方式,在90年代末,新世纪初到2010年,在扩张的时候,在5年翻番,10年翻两番的势头下,他们的高举债,高周转,超级大盘炒地皮,造楼盘都是可以循环的。但是到了这会儿,你继续一根筋的按照这个逻辑来你就要碰壁了。这个就要做理性的分析,客观形势。

那么下一步今后5年,10年,中国房地产开发的新常态,房产商运行方式的新常态,就应该出现5个调整。

第一, 一定要明白全世界的房地产商,包括欧洲、美国的房地产商没有超过50%的,香港的房地产商全部的负债率不超过40%,中国到了80%、90%。是过去的这个三高:地价、房价、市场需求量,20年翻了三、四番,那么一旦这个需求没有了,它们就会进入到一个新常态,负债率会从90%到80%到60%,50%甚至40%。也就是我们现在的三条红线,负载率不高于70%,是因为他们都在80%,90%,我们提了个70%。等到它们自己一旦要降下来,70%也挡不住它,它自己会往50%,40%上降了。这是一个。

第二, 高地价的买地,大楼盘的造房,造成大量库存的这种状态会收敛,会事实就是的根据市场需求结构性的开发,

第三, 就房产商跨界,又搞金融,又搞商业,又搞制造业,觉得房产商赚了钱以后,就可以到处去搞的,这种术业有专攻,它们就不再胡闹了。这个方面也会收敛。

第四, 房产商乱生子公司,房产商多如牛毛,这个状态会减少,中国房地产公司独立法人的有9万个了。那么我们9亿城市人口,1万人一个。那么整个美国的房产公司,法人登记的。造房子开发房子的开发商没超过500个。中国的开发上比全世界的开发上加起来都多。以后中国的房产商用不着这么多。有1万个不得了了。今后十来年,9万个房地产商会变成1万个,法人登记里不断注销,有的是倒闭,有的是自己转行,关闭等。

第五, 房产商的卖房,就不是那个造100万平方米,卖100万平米的大周转,以后就可能变成了造50万平方米,有25万是商品房卖掉的。有25万是商业性的租赁房。就变成商住,长租公寓销售。面对资本金的变大,恰恰是商住两用,他就变成了负载率就下降了。什么概念呢?一旦是租赁,它的租金就可以去发ABS(资产支持证券化)债券,REITS债券一发,资产市场的债券资本就来了,覆盖它一半的资本。然后银行的贷款,造房子,卖房子又产生一半的资本,反而变成了一个均衡,

这个五个变化,会是今后房地产常态的经营状态,所以你一点不要担心,这一次的房地产阵痛,这个阵痛恰恰是中国房地产发展到了这一步,必然带来的一个拐点,拐点过后是房产商良型循环的开始,而且这个开始不仅仅是靠政府约束。政府约束是一个导向,政府哪怕拼命的约束房地产开发商,房产商还是5个炒,5个大周转。到了土壤转轨的时候,他自己就会转到我刚才说的五个状态,政府在旁边又正常的进行引导,这个时候真正房子是拿来住的而不是炒的。良性循环这个土壤一到位,我们今后十年的房地产状态就会进入良性状态。没有这样一次痛苦的阵痛,房产商的行为方式是转不过弯来的。

房地产未来的投入量会逐年消减,同时房价增长的幅度会低于gdp的增长率,跟以前5年翻一番,10年翻两番这个势头就拐过来了。并没有说他5年跌一半,10年跌两半,没这个概念,因为房产总有折旧,折旧的话我们如果有550亿平方米,每年折旧2%或者1.5%,差不多折旧就是10亿平方米,你不要去造20亿平方米,这个时候形成的课循环的持续的状态,就是以折旧状态为平衡,宏观来说有个大体平衡。

我们现在对眼下,对房产商崩盘也好,或者有一些暴雷也好,由于房地产的形势出现了拐点,他怎么适应,一下子怎么办呢?大体上应该这么说,房地产是支柱产业,占gdp 5%-10%的分量,增长可以影响到你10%,负增长也影响你10%,一上一下就是20%,所以房地产是支柱产业要稳定,第二房地产也是民生产业,中国老百姓的财富60%是房产也该稳定。第三房地产是半金融企业,里面大量的按揭贷款,它崩盘的话对金融机构也是问题。第四房地产还和大量的供应链上的工业企业相关,它是龙头产业,带动几十个工业的销售,产品,消费品。

追求房地产产业的稳定,不大起大落,就是在限制土地价格,房产价格,建设量炒作翻番。对政府来说,当下呢,对这些困难的房产商我们不是简单的要去救,一旦它要崩盘,它的债务就实际上进入破产重整的保全。他的资产不是房产商的,是债权人的。这个债权人包括银行,包括非银行或者老百姓,集资款的债主,也包括老百姓买了按揭贷款的房子,还没有拿到房,他也是个债权人。还有许多施工队,施工欠款。如果简单让它破产,造成社会震荡,所以怎么样使得它软着陆,就要用重组的办法,重组是一切危机中解决危机重要的软着陆的一种方法。

这种重组有5种:

第一:大型的房地产商,出了问题的房地产上自救,自我重组,壮士断臂,保存最主要的一个机体。不使得整体上瘫痪。

第二:优势的房地产商,没有出问题的房地产商,去收购,兼并、重组出问题的房产商

第三:国有资本运营公司也可以拿出它的资本金成为战略投资者,入股到那些优质房产商去收购兼并。

第四,地方政府也可以拿出一定的钱,把过去10年5年批租给的房产商的地可以根据现在的市场行情,打个折回购。

第五、房产商库存的房屋,政府也可以回购一部分,回购的中低端的成为保障房,中高端的成为人才房,这也是一种。

总之,五管齐下,那么为了稳住局面,这个五个渠道进行重组的时候,当然要资金,这种资金国家可以专项债,或者专门的贷款,这种债务不放在三条红线里,算体外循环。

平稳落地,软着陆,房地产商也好,政府的调控方式也好,都会随着新的形势,进入良性循环的状态。

视频整理,侵删。

辅车相依,唇亡齿寒。————《左传·僖公五年》

K近邻算法概述

K近邻算法采用测量不同特征值之间的距离方法进行分类。

具有精度高、对异常值不敏感、无数据输入假定,可以适用于数值型和标称型的优点,但是计算复杂度高、空间复杂度高。

K近邻算法的工作原理

存在一个样本数据集合,也称作训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。

以电影分类为例,统计电影中的打斗镜头次数与接吻镜头次数作为特征值,计算所预测的电影与已知电影的距离,假定k=3,即选择三个“距离”最靠近的电影,根据这三个电影的类型决定未知电影的类型。

K近邻算法的一般流程

(1) 收集数据:可以使用任何方法。

(2) 准备数据:距离计算所需要的数值,最好是结构化的数据格式。

(3) 分析数据:可以使用任何方法。

(4) 训练算法:此步骤不适用于k-近邻算法。

(5) 测试算法:计算错误率。

(6) 使用算法:首先需要输入样本数据和结构化的输出结果,然后运行k-近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续的处理。

算法程序步骤

导入数据:创建数据集与标签

python程序如下:

1
2
3
4
5
6
7
from numpy import *
import operator

def createDataSet():
group = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
labels = ['A', 'A', 'B', 'B']
return group, labels

这里我们创建了两组数据,一组4行两列的group矩阵,表示我们已知的4组数据,每组数据包含两个特征;labels表示我们已知的数据标签,分别对应group中的4行数据。

执行kNN算法

kNN算法伪代码:

对未知类别属性的数据集中的每个点依次执行以下操作:

(1) 计算已知类别数据集中的点与当前点之间的距离;

(2) 按照距离递增次序排序;

(3) 选取与当前点距离最小的k个点;

(4) 确定前k个点所在类别的出现频率;

(5) 返回前k个点出现频率最高的类别作为当前点的预测分类。

其python程序源码以及代码解读如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from numpy import *
import operator

def createDataSet():
group = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
labels = ['A', 'A', 'B', 'B']
return group, labels


def classify0(inX, dataSet, labels, k):
"""
:param inX: 预测分类的输入向量,比如[0, 0]
:param dataSet: 训练样本集,比如[[1.0, 1.1], [1.0, 1.0], [0.0, 0.0], [0, 0.1]]
:param labels: 标签向量 ['A', 'A', 'B', 'B']
:param k: 选择最近邻居的数目,比如k=3
:return: 预测的结果标签
"""
dataSetSize = dataSet.shape[0] # dataSetSize = 4
inXCopy = tile(inX, (dataSetSize, 1)) # 复制inX,结果为[[0, 0], [0, 0], [0, 0], [0, 0]],为计算欧式距离做准备
diffMat = inXCopy - dataSet # 坐标差: [[-1.,-1.1], [-1.0,-1.0], [ 0.0, 0.0], [ 0.0, -0.1]]
sqDiffMat = diffMat**2 # 坐标差平方:[[1. 1.21], [1. 1. ], [0. 0. ], [0. 0.01]]
sqDistances = sqDiffMat.sum(axis=1) # 按行求和:[2.21 2. 0. 0.01]
distances = sqDistances**0.5 # 距离平方根,即欧式距离:[1.48660687 1.41421356 0. 0.1 ]
sortedDistances = distances.argsort() # 数值从小到大的索引:[2 3 1 0], 即数组distance中实际顺序为[distance[2], distance[3], distance[1], distance[0]]
# 下面4行代码计算距离数组中前k个元素的标签数量
classCount = {} # 计算结果为:{'B': 2, 'A': 1}
for i in range(k):
# sortedDistances实际是labels中距inX从小到大次序的索引
l = sortedDistances[i]
voteIlabel = labels[l]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1

# classCount.items() 结果:dict_items([('B', 2), ('A', 1)])
# operator.itemgetter(1) 表示对字典数据第一个域进行排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# sortedClassCount最终计算结果:<class 'list'>: [('B', 2), ('A', 1)]
return sortedClassCount[0][0]

group, labels = createDataSet()
r = classify0([0, 0], group, labels, 3)
print(r)

如何测试分类器

错误率:错误结果的次数除以测试执行的总数。

完美分类器的错误率为0,最差分类器的错误率是1.0。