(1)准备一个加密算法的工具类
package cn.itsource.aisell.common;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.hibernate.loader.plan.spi.Return;
/*
* 这是一个加密算法的工具类。
* 加盐(随意定的source)、加密方式、加密次数(随机定的遍历10次)
* */
public class MD5Util {
//1.设置盐值的常量---SALT
private static final String SALT="source";
//2.准备一个加密次数的常量--HASHITERATIONS
private static final int HASHITERATIONS=10;
//3.提供一个加密的方法,将密码参数传过来
public static String creatMD5(String pwd){
//4.传入加密方式、密码、盐值、加密次数4个参数
SimpleHash hash=new SimpleHash("MD5",pwd,SALT,HASHITERATIONS);
//5.返回一个经过MD5编码的值
return hash.toString();
}
}
(2)为了测试方便,通过算法来修改数据库的密码。
使数据库中的用户名个密码相同
package cn.itsource.aisell.service;
import cn.itsource.aisell.BaseSpringTest;
import cn.itsource.aisell.common.MD5Util;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
public class EmployeeServiceTest extends BaseSpringTest{
//注入IEmployeeService对象
@Autowired
private IEmployeeService iEmployeeService;
//测试查询所有员工
@Test
public void myTestFindAll() throws Exception{
iEmployeeService.findAll().forEach(e-> System.out.println(e));
}
//将数据库中的用户和密码一致,并且密码加密加盐值
@Test
public void myTestFindAllAndUpdatePwd() throws Exception{
//从数据库中查询出所有的员工对象集合,并遍历
iEmployeeService.findAll().forEach(e-> {
//通过遍历出来的用户获取到用户名
String username=e.getUsername();
//设置加密后的密码到用户对象e中去--调用加密算法工具类的加密方法,将查询到的用户名设置为密码(用户名和密码一致,方便测试)
e.setPassword(MD5Util.creatMD5(username));
//将e对象再保存到数据库中
iEmployeeService.save(e);
});
}
}
(3)员工添加和修改保存操作与密码之间
根据id判断是添加保存还是修改保存操作
在employeeServiceImpl中写覆写父类的sava方法,新增自己的功能
//覆写service层实现类的父类的sava方法---扩展功能:添加员工的时候将密码加密
//添加或者修改的保存
@Override
public void save(Employee employee) {
//根据id判断是添加还是修改保存操作
if(employee.getId()==null){
//id为空就是添加保存,给密码加密
employee.setPassword(MD5Util.creatMD5(employee.getPassword()));
}
//保存到数据库中
employeeRepository.save(employee);
}
(4)准备一个登录页面,并写上相应的js代码
<%--
Created by IntelliJ IDEA.
User: zhaoyi
Date: 2018/10/14
Time: 下午4:27
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
源码智销系统
<%--引入已经引入easyui样式的head.jsp文件--%>
<%@ include file="/WEB-INF/views/head.jsp"%>
(5)修改登录的controller层:LoginController
大体的改动地方:
使用GET和POST两种请求方式处理登录路径。
GET:负责跳转到登录页面
POST:在登录的jsp中,表单提交使用的post方式,还使用ajax交互技术,请求后台路径,请求成功就返回后台的json数据给前台,前台处理成功后页面的跳转,请求失败也返回相应的json数据给前台捕获处理。
注意点:
ajax交互技术不能跳转页面,只能返回json数据,交给前台处理
package cn.itsource.aisell.controller;
import cn.itsource.aisell.common.JsonResult;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class LoginController {
//跳转到login登录页面--用GET请求方式
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String index(){
return "/login";
}
/*
* 在js中使用ajax请求返回数据,千万不要跳转页面。需要返回的数据{success:true/false,msg:xxx},就是将JsonResult中的字段返回去
* 若ajax请求跳转了,那么:会返回跳转的页面当前字符串回来或者直接报错
* 使用前台ajax交互技术,请求后台路径,请求成功就返回后台的json数据给前台,前台处理成功后页面的跳转,请求失败也返回相应的json数据给前台捕获处理
* */
//实现登录功能--用POST请求(form表单)
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody//返回json数据(不要忘记这个注解,返回给前台都返回json数据)
public JsonResult login(String username, String password){
//当前台登录页面输入数据时会提交到后台来
// System.out.println(username+":"+password);
/*
* 已经叫给spring去管理shiro了,所以此处就不用再创建核心对象securityManager了,
* 也不需要拿到凭证适配器、设置加密方式、设置加密次数了,同样也不用拿自定义的realm类和
* 核心对象securityManager发生关系,也不用将核心对象securityManager放到上下文中去
* */
//1.直接拿到当前的用户
Subject currentName = SecurityUtils.getSubject();
//2.如果当前用户没有登录,就给他个令牌叫他登录
//currentName.isAuthenticated():布尔类型。false表示没有登录,反之则登录
if(!currentName.isAuthenticated()){
try {
//3.给个令牌,将前台传过来的参数传进去
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//4.用户登录
currentName.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
// System.out.println("用户名错误");
return new JsonResult(false,"用户名错误!");
}catch (IncorrectCredentialsException e) {
e.printStackTrace();
// System.out.println("密码错误");
return new JsonResult(false,"密码错误");
}catch (AuthenticationException e) {
e.printStackTrace();
// System.out.println("未知错误");
return new JsonResult(false,"未知错误");
}
}
//成功后直接跳转到主页面。用redirect跳转,就不受mvc视图解析配置(自动生成路径)的影响了
// return "redirect:/jsp/main.jsp";
//用ajax请求,返回数据,交由前台处理
return new JsonResult();
}
//用户名登录注销
@RequestMapping("/logout")
public String logout() {
//直接拿到用户名
Subject subject = SecurityUtils.getSubject();
//注销登录
subject.logout();
//注销后直接跳转到登录页面
return "redirect:/s/login.jsp";
}
}
(6)由于登录页面有静态资源,所以要在FilterChainDefinitionMapBuilder类中(解决权限路径在applicationcontext-shiro.xml中写死问题)放行静态资源,不予拦截
//设置静态资源放行
filterChainDefinitionMap.put("*.js","anon");
filterChainDefinitionMap.put("*.css","anon");
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/easyui/**","anon");
filterChainDefinitionMap.put("/images/**","anon");
(7)登录的用户名和密码直接从数据库中查询。
之前在自定义realm类中使用的是模拟的数据库用户名和密码,用于登录测试。
//登录验证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.拿到令牌。将authenticationToken令牌强转为usernamepasswordToken类型的令牌
UsernamePasswordToken token=(UsernamePasswordToken)authenticationToken;
//2.根据令牌拿到用户名
String username = token.getUsername();
// //3.再根据用户名去数据库(模拟的数据库)拿到对应的密码
// String password=getByName(username);
//3.根据用户名直接去数据库中查找相应的用户
Employee employee = employeeService.findByUsername(username);
//4.判断如果用户为空
if(employee==null){
//返回空,代表登录失败,shiro会自动报UnknownAccountException异常
return null;
}
//5.准备好盐值
ByteSource salt = ByteSource.Util.bytes("source");
//6.返回AuthenticationInfo对象。通过SimpleAuthenticationInfo类,参数(用户名、密码、盐值、随意的realm名字)
//用户名与密码放进去-注:它会自动的比数据库获取的密码与你前台传过来的密码----employee.getPassword():数据库查询出来的用户的密码
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username, employee.getPassword(), salt, getName());
return authenticationInfo;
}
/* 从数据库中查询用户,就不用模拟的了
//根据当前登录用户拿到密码(模拟数据库)--456经过加盐遍历10此得到的加密密码42a02ed97752caf987d13d1a5cb53847
private String getByName(String username){
if("admin".equals(username)){
return "42a02ed97752caf987d13d1a5cb53847";
}else if("it".equals(username)){
return "42a02ed97752caf987d13d1a5cb53847";
}
return null;
}*/
(8)扩展:在login.jsp登录页面使用回车键登录功能
//扩展:回车键登录功能
//keyup:键盘事件监听 event:事件对象
$(document.documentElement).on("keyup", function(event) {
//不同的按键会打印出不同的值
console.debug(event.keyCode);
var keyCode = event.keyCode;
console.debug(keyCode);
if (keyCode === 13) { // 捕获回车键(13就是回车键对应的值)
submitForm(); // 调用上面的提交表单方法
}
});
(9)扩展:检查自己是否是顶级页面
// 扩展:检查自己是否是顶级页面 /login--解决页中还有页的问题
if (top != window) {// 如果不是顶级
//把子页面的地址,赋值给顶级页面显示
window.top.location.href = window.location.href;
}
(10)在主页面main.jsp上面展示用户名和注销功能新
先在主页面引入shiro的标签
<%--引入shiro标签:--%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
再通过shiro标签获取到用户名
新建一个div标签,被包含于外面的div标签
"/logout"注销路径。在LoginController登录中
//用户名登录注销
@RequestMapping("/logout")
public String logout() {
//直接拿到用户名
Subject subject = SecurityUtils.getSubject();
//注销登录
subject.logout();
//注销后直接跳转到登录页面
return "redirect:/login";
}
(11)角色role和权限permission通过代码生成器生成
(12)配置多对多关系。都是单向
员工和角色多对多关系
在Employee.java中:
/*
* 员工和角色role是多对多关系 @ManyToMany后面可以配置个懒加载,但是list集合默认就是懒加载,所以可以不管
* name = "employee_role":生成的中间表的名字
* joinColumns = @JoinColumn(name = "employee_id"):中间表中的当前类的id
* inverseJoinColumns = @JoinColumn(name = "role_id"):中间表中的另一个类的id
*
* */
@ManyToMany
@JoinTable(name = "employee_role",joinColumns = @JoinColumn(name = "employee_id"),inverseJoinColumns = @JoinColumn(name = "role_id"))
private List roles=new ArrayList<>();
//提供get/set方法。跟其他表有关联的字段不要覆写tostring
public List getRoles() {
return roles;
}
public void setRoles(List roles) {
this.roles = roles;
}
角色与权限之间是多对多关系。单向
在Role.java中;
/*
* 角色和权限之间是单向多对多关系 @ManyToMany后面可以配置个懒加载,但是list集合默认就是懒加载,所以可以不管
*name = "role_permission":生成的中间表的名字
* joinColumns = @JoinColumn(name = "role_id"):中间表中的当前类的id
*inverseJoinColumns = @JoinColumn(name ="permission_id" ):中间表中的另一个类的id
* */
@ManyToMany
@JoinTable(name = "role_permission",joinColumns = @JoinColumn(name = "role_id"),inverseJoinColumns = @JoinColumn(name ="permission_id" ))
private List permissions=new ArrayList<>();
//提供get/set方法,注意:与其他表有关联的字段不要覆写tostring
public List getPermissions() {
return permissions;
}
public void setPermissions(List permissions) {
this.permissions = permissions;
}
(13)在角色Role.jsp页面上展示权限数据
使用formatter属性。灵活在展示数据库中的数据
先在Role.jsp中添加formatter属性。
<%--要在角色中拿到权限。formatter:更加灵活的显示数据库中的数据--%>
权限
在role.js中提供方法,返回并展示数据在Role.jsp页面上
//formatter:灵活在展示数据库中的数据---在role.jsp中的formatter:formatPerms属性
//value;当前格子中的值(权限名的值) row:当前行的值 index:当前行的下标(索引)
function formatPerms(value,row,index) {
//先定义一个空的权限值
var permsSt="";
//value:所有角色对应的array数组形式的permission权限值(包括id、name、url,有的都查出来)
// console.debug(value);
//遍历数组Array类型的value--使用ES6新语法,变量let相当于var
//let perm in value:拿到的是下标,取值value[perm] let perm of value:拿到的是对象,取值直接对象.字段
for(let perm of value){
//遍历后赋值给permsSt,并每个中间隔一个空格(" "),也可以空字符串等等其他的
permsSt+=perm.name+" "
}
//将值返回展示在页面上
return permsSt;
}
(14)角色Role.jsp添加数据的弹出框效果
(15)在Role.jsp中准备上面图片的弹出框
注意:使用javaScript的方式创建两个数据表格,就不需要class=“easyui-datagrid”,直接js,以便于方便注册事件
Role.jsp部分:
<%--
Created by IntelliJ IDEA.
User: Administrator
Date: 2019/3/15
Time: 11:11
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Title
<%@include file="/WEB-INF/views/head.jsp" %>
<%--引入role特有的js--%>
<%--toolbar:'#gridToolBar':与上面的添加、删除、修改按钮的div相关联--%>
角色名称
角色编码
<%--要在角色中拿到权限。formatter:更加灵活的显示数据库中的数据--%>
权限
<%--表单弹出框--%>
<%--弹出框(表单)操作--%>
role.js中用js创建左边数据表格(包含角色权限信息)和右边数据表格(包含所有权限信息)
$(function () {
//获取到常用的组件
//左边数据表格组件
var LeftrolePermissionDataGrid=$("#LeftrolePermissionDataGrid");
//右边数据表格组件
var RightpermissionDataGrid=$("#RightpermissionDataGrid");//右边的数据表格
//左边数据表格LeftrolePermissionDataGrid。当前角色所有权限
LeftrolePermissionDataGrid.datagrid({
fit:true,
fitColumns:true,
singleSelect:true,
onDblClickRow:itsource.removePerms
})
//右边数据表格RightpermissionDataGrid。所有权限---使用js创建数据表格
RightpermissionDataGrid.datagrid({
//通过url路径到后台去查询所有权限
url:'/permission/page',
fit:true,
fitColumns:true,
singleSelect:true,
pagination:true,
})
}
(16)就上面的两个数据表格完成一个功能;当双击右边的所有权限数据表格中的一条数据时,会自动将这个数据添加到左边角色所有权限数据表格中去。并且左边数据表格有的,双击右边表格时不能重复添加
在Role.js中的代码:
$(function () {
//获取到常用的组件
//右边数据表格组件
var RightpermissionDataGrid=$("#RightpermissionDataGrid");
itsource = {
//当双击的时候,添加权限到左边数据表格中(当前角色权限)
//onDblClickRow事件的两个参数:index:点击的行的索引值,该索引值从0开始。 row:对应于点击行的记录。
addPerms(index,rightRows){
//1。获取到左边表格的所有值。--当左边表格已经有权限了,而双击右边的表格添加权限,如果是一样的就不用再添加到左边去。根据id判断是否重复
var leftRows=LeftrolePermissionDataGrid.datagrid("getRows");
//2.遍历左边的数据行,根据id判断是否跟右边的一致
for(let r of leftRows){
//若果两边的id相同,就在屏幕右下方给出提示
if(r.id==rightRows.id){
$.messager.show({
title:'温馨提示',
msg:'已存在相同的权限!',
showType:'show'
});
//返回
return;
}
}
//3.否则就把右边的权限添加到左边去
LeftrolePermissionDataGrid.datagrid("appendRow",rightRows);
},
}
//右边数据表格RightpermissionDataGrid。所有权限---使用js创建数据表格
RightpermissionDataGrid.datagrid({
//通过url路径到后台去查询所有权限
url:'/permission/page',
fit:true,
fitColumns:true,
singleSelect:true,
pagination:true,
//onDblClickRow事件:在用户双击一个单元格的时候触发。itsource.addPerms:触发事件后就调用这个方法
onDblClickRow:itsource.addPerms
})
}
(17)功能;双击左边角色的所有权限的时候,删除双击的行。
$(function () {
//常用的控件先获取
//左边数据表格组件
var LeftrolePermissionDataGrid=$("#LeftrolePermissionDataGrid");
itsource = {
//双击左边角色所有权限行,删除权限的方法
removePerms(index,row){
LeftrolePermissionDataGrid.datagrid("deleteRow",index);
}
}
//左边数据表格LeftrolePermissionDataGrid。当前角色所有权限
LeftrolePermissionDataGrid.datagrid({
fit:true,
fitColumns:true,
singleSelect:true,
//删除权限的方法。双击左边角色所有权限表格的时候,调用itsource.removePerms方法删除选中的行
onDblClickRow:itsource.removePerms
})
}
(18)提交保存。角色与权限是多对多关系,关联对象权限permissions是个集合
问题:无法将权限提交到数据库中。
分析;传权限是以何种方式、何种结构传的。以前都是传的对象,而现在权限permissions是个List集合。
解决:使用form表单的提交额外参数的方式。param参数:form表单提交额外的参数,它的数据也会进行提交。可以提交List、Set、Array等集合、数组形式的参数
$('#ff').form('submit', {
url:...,
onSubmit: function(param){
param.p1 = 'value1';
param.p2 = 'value2';
}
});
在role.js中写js代码处理提交集合参数。
//保存功能
save(){
//根据id确定是添加路径还是修改路径
var url = "/role/save";
var id = $("#roleId").val();
if(id){
url = "/role/update?cmd=update";
}
roleForm.form('submit', {
url:url,
/*
* 解决权限保存提交问题:原来是提交的对象。而现在权限是集合的方式,怎么提交?
* 使用form表单的提交额外参数,param参数。可以提交集合参数:List、Set、Array都行
* */
//提交表单前的方法。parm参数:form表单提交额外的参数,它的数据也会进行提交
onSubmit: function(param){
// param.a="Xx";
// param.name="张";
//1.先拿到左边角色所有权限的数据表格
var rows=LeftrolePermissionDataGrid.datagrid("getRows");
//2.提交集合参数,遍历集合。---角色和权限是多对多关系。关联的是list集合形式的permissions权限
for(let i in rows){//拿到的是array数组的下标
//3.拼接相应的传过去。permissions[0].id = 1, permissions[1].id = 2
//将左边表格的数据行id赋值给param参数提交到后台去--使用到了ES6新语法
param[`permissions[${i}].id`]=rows[i].id;
}
return $(this).form('validate');
},
//操作成功后的回调 {success:true,msg:xxx}
success:function(data){
//返回的是JSON字符串,我们需要把转成一个JSON对象
var result = JSON.parse(data);
if(result.success){
//成功后刷新
roleDataGrid.datagrid("reload");
//关闭窗口
roleDialog.dialog("close");
}else{
//把失败信息做一个提示
$.messager.alert("提示",`你出错了! 原因是:${result.msg}`,"error");
}
}
});
(19)清空上次左边数据表格中添加的权限数据,达到新点击添加时,左边表格中的数据时空的
使用数据表格datagrid中的loadData方法 : 加载本地数据,旧的行将被移除。需要传个参数data加载,提供一个[] 就相当于加载空的(清空)。
在role.js中的add()方法中:
itsource = {
add(){
//把所有带data-show的元素显示起来
$("*[data-show]").show();
$("*[data-show] input").validatebox("enable");
//打开对话框
roleDialog.dialog("center").dialog("open");
//把form中的数据清空
roleForm.form("clear");
//把左边的datagird表格中值进行清空--添加时清空表格中上次添加的数据
//loadData方法:加载本地数据,旧的行将被移除。需要传个参数data加载,提供一个[] 就相当于加载空的(清空)
LeftrolePermissionDataGrid.datagrid("loadData",[]);
},
(20)修改角色信息时的回显。重点又是权限数据的回显
修改时,将角色的权限回显在左边的数据表格中
使用数据表格datagrid的loadData方法。
//修改按钮功能
edit(){
var row = roleDataGrid.datagrid("getSelected");
if(!row){
$.messager.alert("提示","选中再来!瓜!","info");
return;
}
//把所有带data-show的元素隐藏起来
$("*[data-show]").hide();
$("*[data-show] input").validatebox("disable");
//打开对话框
roleDialog.dialog("center").dialog("open");
//把form中的数据清空
roleForm.form("clear");
//完成修改的回显--load方法:读取数据填充到表单中
roleForm.form("load",row);
//修改时,左边表格回显角色的权限数据。loadData方法:加载本地数据,旧的行将被移除。需要传个参数data加载。
LeftrolePermissionDataGrid.datagrid("loadData",row.permissions);
},
(21)上面修改出现的问题:
当修改时,双击回显的左边的数据,会被删除。当关闭修改页面后再次修改同一角色时,回显的权限数据没有之前双击的那个数据行。但是数据库中没有删除。只是不能再回显出当初的那行数据
分析:
LeftrolePermissionDataGrid.datagrid("loadData",row.permissions);
row.permissions:回显到数据表格中的角色权限
datagrid:角色原本的权限
理解:
这两个共用了一个数据表格,修改时,双击增加或删除权限后关闭页面,再次修改时都会是关闭页面之前操作的数据。但是数据库中角色的权限没有变化
解决思路:
拷贝一份权限数据表格的副本,专门用于操作。这样就不影响原本的权限数据
//完成修改的回显--load方法:读取数据填充到表单中
roleForm.form("load",row);
// 会出现问题:
//修改时,左边表格回显角色的权限数据。loadData方法:加载本地数据,旧的行将被移除。需要传个参数data加载。
// LeftrolePermissionDataGrid.datagrid("loadData",row.permissions);
//解决上述问题:拷贝一份数据表格副本拿去操作-- ES6的新语法(拷备一个数组)
var copyPermissions=[...row.permissions];
LeftrolePermissionDataGrid.datagrid("loadData",copyPermissions);
(22)问题:点击确认按钮保存时,数据库中并没有更改数据。
permissions权限集合是关联对象。
记住:关联对象修改保存的时候,都先清空
在RoleController类中:
/**
* 在执行任何一个路径(方法) 之前都会先执行这个方法
*/
@ModelAttribute("editRole")
public Role beforeEdit(Long id,String cmd){
//只有修改才做查询
if(id!=null && "update".equals(cmd)){
Role dbRole = roleService.findOne(id);
//解决n-to-n的问题(凡是要传过来的关联对象,都把它清空)
dbRole.getPermissions().clear();
return dbRole;
}
return null;
}