SSM_后台数据管理+安全认证
数据列表
1. 商品表Product
| 变量名 | 类型 | 备注 |
|---|---|---|
| id | String | 主键 |
| productNum | String | 商品编号 |
| productName | String | 商品名 |
| cityName | String | 出发城市 |
| departureTime | Date | 出发时间 |
| departureTimeStr | String | 出发时间的字符串,不在数据库中 |
| productPrice | double | 商品价格 |
| productDesc | String | 商品描述 |
| productStatus | Integer | 值0为关闭状态,值1为打开状态 |
| productStatusStr | String | 商品状态的字符串,不在数据库中 |
1.1. Date与String之间的类型转换
赋值
Controller接收参数时,需要把用户输入的String类型的departureTime转为date类型,这里我设置了全局的类型转换器,由springMVC处理转换:
1 | public class StringToDate implements Converter<String, Date> { |
springMVC配置:
1 | <!-- 配置类型转换器de固定步骤--> |
取值
输出时,可以使用事先定义的departureTimeStr,这样可以保持departureTime不变,需要对departureTimeStr赋值,在get方法中写:
1 | public String getDepartureTimeStr() { |
DateAndString是自定义的工具类,将date转为string,详情:
1 | public class DateAndString { |
注解方式
详情:@DateTimeFormat与@JsonFormat
2. 订单表Orders
| 变量名 | 类型 | 备注 |
|---|---|---|
| id | String | 无意义,主键id |
| orderNum | String | 订单编号 不为空 且唯一 |
| orderTime | Date | 下单时间 |
| orderTimeStr | String | 用于输出下单时间,不在数据库中 |
| peopleCount | int | 出行人数 |
| orderDesc | String | 订单备注和描述信息 |
| payType | int | 支付方式(0支付宝,1微信,2其他) |
| payTypeStr | String | 用于输出支付方式,不在数据库中 |
| orderStatus | int | 订单的状态(0未支付 1已支付) |
| orderStatusStr; | String | 用于输出下单状态,不在数据库中 |
| productId | int | 产品的id,外键 |
| memberid | int | 会员(联系人)id外键 |
| travellers | List |
旅客 |
| member | Member | 会员 |
2.1. 订单查询
一个订单对应一个产品、一个会员(联系人)、多个旅客,使用注解方式查询数据时,使用@Results指定关系,一对一多对一使用@One指定方法,一对多多对多使用@Many指定方法
1 | //查询一个订单的具体信息 |
3. 会员表Member
| 变量名 | 类型 | 备注 |
|---|---|---|
| id | String | 无意义、主键id |
| name | String | 姓名 |
| nickName | String | 昵称 |
| phoneNum | String | 电话号码 |
| String | 邮箱 |
3.1. 单个会员查询:
1 | ("select * from member where id=#{id}") |
4. 旅客表Traveller
| 变量名 | 类型 | 备注 |
|---|---|---|
| id | String | 无意义、主键id |
| name | String | 姓名 |
| sex | String | 性别 |
| phoneNum | String | 电话号码 |
| credentialsType | int | 证件类型 0身份证 1护照 2军官证 |
| credentialsTypeStr | String | 用于输出证件类型,不在数据库中 |
| credentialsNum | String | 证件号码 |
| travellerType | int | 旅客类型(人群) 0 成人 1 儿童 |
| travellerTypeStr | String | 用于输出旅客类型,不在数据库中 |
5. 旅客与订单之间的多对多关系,order_traveller中间表
| 字段名 | 字段类型 | 字段描述 |
|---|---|---|
| orderId | varchar(32) | 订单id,与对应表绑定外键 |
| travellerId | varchar(32) | 旅客id,与对应表绑定外键 |
5.1. 根据指定订单号,多个旅客的查询:
1 | ("select * from traveller where id in( |
6. 用户表Users
| 变量名 | 类型 | 备注 |
|---|---|---|
| id | String | 无意义,主键id |
| String | 非空,唯一 | |
| username | String | 用户名 |
| password | String | 密码(加密) |
| phoneNum | String | 电话 |
| status | int | 状态0 未开启 1 开启 |
| roles | List |
角色集 |
6.1. 用户的查询:
1 | //查询所有用户 |
7. 角色表Role
| 变量名 | 类型 | 备注 |
|---|---|---|
| id | String | 无意义,主键id |
| roleName | String | 角色名 |
| roleDesc | String | 角色描述 |
| UserInfos | List |
用户集 |
| permissions | List |
权限集 |
8. 用户与角色的多对多关系,user_role中间表
| 变量名 | 类型 | 备注 |
|---|---|---|
| userId | String | 用户id,与用户id外键关联 |
| roleId | String | 角色id,与角色id外键关联 |
8.1. 根据用户查询角色集合:
1 | //根据用户id查询角色集 |
9. 权限表Permission
| 变量名 | 类型 | 备注 |
|---|---|---|
| id | String | 无意义,主键id |
| permissionName | String | 权限名 |
| url | String | 资源路径 |
| roles | List |
角色集 |
10. 角色与权限多对多关系,role_permission中间表
| 变量名 | 类型 | 备注 |
|---|---|---|
| permissionId | String | 权限id,与权限id关联外键 |
| roleId | String | 角色id,与角色id关联外键 |
Spring Security安全框架
Spring Security是一种基于 Spring AOP 和 Servlet 过滤器的安全框架,它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理认证和授权。
1.Pom依赖
1 | <properties> |
2. spring-Security.xml配置文件
1 |
|
3. Web.xml配置文件
1 | <!--监听器 --> |
4. 密码加密流程
由于用户注册时,数据库中的用户密码需要加密保存,以保护用户信息安全。Spring Security提供的加密方式里,有一种为BCryptPasswordEncoder类,使用BCrypt强哈希方法来加密密码。这是种加盐哈希方式,每次加密产生的密文都不同,密码验证时通过匹配hash值来进行认证,可以抵御彩虹表,提高破解难度。
使用时,只需在接收用户信息后,调用BCryptPasswordEncoder对象的encode方法,对用户密码进行加密,然后将加密后的用户信息放入数据库即可,由于加密后数据比较长,注意数据库字符长度。如:
1 | ("userService") |
5. 登录认证流程
5.1. 创建UserInfo类,用来封装数据库返回的用户信息
1 | public class UserInfo { |
5.2. Dao层查询出用户信息
1 | //按照用户名查找单个用户,验证登录 |
5.3. 创建IuserService接口,继承UserDetailsService接口
1 | public interface IuserService extends UserDetailsService { |
5.4. 创建userServiceImpl类,实现IuserService接口
1 | //放入IOC容器,取名为userService,供xml中配置 |
tips:
5.4.1. 关于”{noop}”前缀
在spring5.0之后,springsecurity存储密码的格式发生了改变,新的密码存储格式为:加密方式和加密后的密码,{id}encodedPassword
1 | //均为字符串 |
5.4.2. 关于框架提供的User类
Security的User类,提供了两个构造方法:
1 | //Security提供的User类 |
三参构造(用户名,密码,权限集合 ),如:
1 | user = new User(userInfo.getUsername(), "{bcrypt}" + uPwd, authorities); |
七參构造(用户名,密码,是否启用,账号是否过期,认证信息是否过期,是否被锁定,权限集合),如:
1 | User user = new User(userInfo.getUsername(), "{bcrypt}" + userInfo.getPassword(), |
5.5. 在Security的xml文件配置userService即可
1 | <security:authentication-manager> |
6. 注解方式的权限控制
注解都默认关闭,使用前均需开启,在Spring-Security.xml中配置:
1 | <!-- 启用注解,用于进行权限控制--> |
6.1. JSR250注解
依赖、jar包:
1 | <dependency> |
@RolesAllowed注解,指定类、或方法需要的角色,无需加ROLE_前缀,使用:
1 |
|
@PermitAll注解,表示允许所有的角色进行访问,也就是说不进行权限控制
@DenyAll注解,是和PermitAll相反的,表示无论什么角色都不能访问
6.2. @Secured注解
此注解为Spring Security自带注解,用法与@RolesAllowed大致相同,不过角色要加ROLE_前缀,如:
1 |
|
6.3. 支持SPEL表达式的注解
常用的权限表达式:
| 表达式 | 说明 |
|---|---|
| permitAll | 永远返回true |
| denyAll | 永远返回false |
| anonymous | 当前用户是anonymous时返回true |
| rememberMe | 当前用户是rememberMe用户时返回true |
| authenticated | 当前用户不是anonymous时返回true |
| fullAuthenticated | 当前用户既不是anonymous也不是rememberMe用户时返回true |
| hasRole(role) | 用户拥有指定的角色权限时返回true |
| hasAnyRole([role1,role2]) | 用户拥有任意一个指定的角色权限时返回true |
| hasAuthority(authority) | 用户拥有指定的权限时返回true |
| hasAnyAuthority([authority1,authority2]) | 用户拥有任意一个指定的权限时返回true |
| hasIpAddress(’192.168.1.0’) | 请求发送的Ip匹配时返回true |
@PreAuthorize注解, 在方法调用之前,基于表达式的计算结果来限制对方法的访问
如:
1 |
|
@PostAuthorize 注解,允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
示例:
1 |
|
@PostFilter 注解,允许方法调用,但必须按照表达式来过滤方法的结果
@PreFilter 注解,允许方法调用,但必须在进入方法之前过滤输入值
7. 权限控制标签
依赖、jar(已有):
1 | <properties> |
jsp页面引入taglib:
1 | <% prefix="security" uri="http://www.springframework.org/security/tags" %> |
常用标签:
authentication
允许访问当前的
Authentication对象,获得属性的值,用来取值和获取对象。
1 | <security:authentication property="" htmlEscape="" scope="" var=""/> |
property: 只允许指定Authentication所拥有的属性,可以进行属性的级联获取 如“principle.username”,
不允许直接通过方法进行调用htmlEscape:表示是否需要将html进行转义。默认为true。scope:与var属性一起使用,用于指定存放获取的结果的属性名的作用范围,默认我pageContext。Jsp中拥
有的作用范围都进行进行指定var: 用于指定一个属性名,这样当获取到了authentication的相关信息后会将其以var指定的属性名进行存
放,默认是存放在pageConext中- 实例:
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %> |
当然,你可以在你的MVC控制器中访问Authentication对象 (通过调用SecurityContextHolder.getContext().getAuthentication()) 然后直接在模型中添加数据,来渲染视图:
1 | // Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
- authorize
authorize是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示。
1 | <security:authorize access="" method="" url="" var=""></security:authorize> |
access: 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限method:是配合url属性一起使用的,表示用户应当具有指定url指定method访问的权限,method的默认值为GET,可选值为http请求的7种方法url:表示如果用户拥有访问指定url的权限即表示可以显示authorize标签包含的内容var:用于指定将权限鉴定的结果存放在pageContext的哪个属性中
- accesscontrollist
accesscontrollist标签是用于鉴定ACL权限的。其一共定义了三个属性:hasPermission、domainObject和var,
其中前两个是必须指定的。
1 | <security:accesscontrollist hasPermission="" domainObject="" var=""></security:accesscontrollist> |
hasPermission:用于指定以逗号分隔的权限列表domainObject:用于指定对应的域对象var:则是用以将鉴定的结果以指定的属性名存入pageContext中,以供同一页面的其它地方使用
操作日志
记录每个用户的操作详情,方便管理和监控。这里使用Spring AOP的前置通知、后置通知,来控制日志的生成。
1. 日志表sysLog
1.1. 数据库表
| 字段名称 | 字段类型 | 字段描述 |
|---|---|---|
| id | VARCHAR | 无意义,完成时间的字符串 |
| visitTime | timestamp | 访问时间 |
| username | VARCHAR | 操作者用户名 |
| ip | VARCHAR | 访问ip |
| url | VARCHAR | 访问资源url |
| executionTime | int | 执行时长 |
| method | VARCHAR | 访问方法 |
1.1.1. 插入日志
1 | ("insert into syslog(id,visitTime,username,ip,url,executionTime,method) values(#{id},#{visitTime},#{username},#{ip},#{url},#{executionTime},#{method})") |
1.1.2. 查询日志
1 | ("select * from syslog") |
1.2. 实体类
1 | public class SysLog { |
1.3. AOP生成数据
开启spring-MVC对AOP的注解支持
1 | <!-- |
创建sysLogAOP类,使用aop的前置通知、后置通知,生成需要的数据,详细如下:
1 | package com.SH.AOP; |
tips:
1.3.1. 关于获取IP
spring提供了一个RequestContextListener,可以在spring中直接使用(先注入)HttpServletRequest对象。在web.xml中配置监听器:
1 | <!-- 为spring提供 request对象,监听器--> |
1.3.2. 关于获取用户信息
可以通过SecurityContextHolder.getContext()获取sercurity上下文对象,从而可以getAuthentication().getPrincipal()获得用户对象,这个上文在权限控制标签中说过。
1 | SecurityContext context = SecurityContextHolder.getContext();//获取Security上下文对象 |
创建sysLogAOP类,使用aop的前置通知、后置通知,详细如下:
1.3.3. 关于获取URL
这里的url是拼接Controller类&方法的@RequestMapping值得到的。
- 首先需要获取类、方法。
类使用JoinPoint获取:
1 | //获取对象的类 |
至于方法,因为将调用的Class对象的getMethod方法为:
1 |
|
所以要按有无参数分开获取:
先使用JoinPoint得到方法名和方法的参数:
1
2
3
4//1. 获取切入对象(方法)的名字
String methodName=joinPoint.getSignature().getName();
//2. 获得方法的参数(一个Object数组)
Object[] args = joinPoint.getArgs();通过判断参数是否为空,来确认方法是否有參。
若无参数:
1
2
3
4
5//3. 判断要获取的方法是否有参数
if (args==null||args.length==0)//没有参数
{
//通过方法名获取方法//无参方法获取
method = aClass.getMethod(methodName);若有参数:
1
2
3
4
5
6
7
8
9
10
11
12}else {//有参数
//创建一个argsClass数组
Class[] argsClass=new Class[args.length];
//循环,获取args数组里每个参数的类,并且装入argsClass数组
for (int i=0;i<args.length;i++){
argsClass[i]= args[i].getClass();//这里会将int等基础数据类型获取成Integer包装类型
// System.out.println("参数:"+args[i]);
}
//通过方法名+参数类型获取方法//有参方法获取
method= aClass.getMethod(methodName,argsClass);//有的方法,参数是基本数据类型如int,需要将方法内int参数换成Integer包装类,也就Controller层形参都使用Inter类型
}
此处参数类型问题的详情:
这里参数获取类型(arg.getClass()),会把基本数据类型(如int等)获取成包装类型(如Integer等),而实际上是基本数据类型,这会使class.getMethod(String name, Class<?>... parameterTypes)执行时找不到匹配的方法对象,报NoSuchMethodException异常,以及后续的空指针异常。因为获取时便是Integer,使用isPrimitive()(确认是否为基本数据类型)的结果始终为false,目前我并未找到完美的解决方法。
临时的解决方式:
①让Controller内方法的参数类型只使用Integer等包装类,不能使用int等基本数据类型。直接把Controller内的int、char等类型改成Integer、Char就行了,不再用代码举例了。
②创建一个HashMap用来存放包装类型与基本类型的<K、V>对,将获取的包装类型转换为基本类型。这样做就会使Controller类内方法的参数类型只能用int等基本数据类型,不能使用Integer等包装类型。当然,其他类型是不影响的。详细代码如下:
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
public class sysLogAOP {
//创建一个HashMap,存放包装类与基本类型的KV对,用来将包装类型转为基本数据类型
private static HashMap<String, Class> map = new HashMap<String, Class>() {
{
put("java.lang.Integer", int.class);
put("java.lang.Double", double.class);
put("java.lang.Float", float.class);
put("java.lang.Long", long.class);
put("java.lang.Short", short.class);
put("java.lang.Boolean", boolean.class);
put("java.lang.Char", char.class);
}
};
//获取切入对象的类
aClass = joinPoint.getTarget().getClass();
//★获取方法
String methodName=joinPoint.getSignature().getName();//1. 获取切入对象(方法)的名字
Object[] args = joinPoint.getArgs();//2. 获得方法的参数
//3. 判断要获取的方法是否有参数
Class[] argsClass=null;//参数类型数组
if (args==null||args.length==0)//没有参数
{
//通过方法名获取方法//无参方法获取
method = aClass.getMethod(methodName);//获取指定的方法,第二个参数可以不传
}else {//有参数
argsClass=new Class[args.length];//创建一个argsClass数组,长度与参数数组相同
for (int i=0;i<args.length;i++){//循环
argsClass[i]= args[i].getClass();//获取args数组里每个参数的类,并且装入argsClass数组
//打印,以供观察
System.out.println("遍历出的参数的类名为:"+args[i].getClass().getName());
if (map.get(args[i].getClass().getName())!=null){//能根据参数的类名在自定义的hashMap中找到对应的基本类型
argsClass[i]=map.get(args[i].getClass().getName());//则放入class数组,覆盖掉之前的class数组值,此时通过map将参数类型转为了基本数据类型
//打印,以供观察
System.out.println("参数类型转换为:"+argsClass[i]);
}else {//如果根据参数的类名在自定义的map集合中取不到值,则说明参数是其他类型
//打印,以供观察
System.out.println("参数是其他类型,或者是基本类型,保持class不变");
}
}
//打印出最终参数类型
System.out.println("最终参数类型:"+ Arrays.toString(argsClass));
//通过方法名+参数类型获取方法//有参方法获取
method= aClass.getMethod(methodName,argsClass);//此时Controller类内方法参数类型就不能为包装类型了,只能用int、char等基本数据类型
}
补充:还有个同样的问题,它有时还会把其他类型的参数获取成特定类型,如java.util.Map会获取成org.springframework.validation.support.BindingAwareModelMap。我将Controller类内方法的Map类型替换为BindingAwareModelMap类型,暂时避免异常。

BindingAwareModelMap类的信息如图所示,目前使用中尚未出现其他问题。
- 获取类和方法后,就可获取需要的注解(需要转换),这里是@RequestMapping注解
1 | RequestMapping classAnnotation =(RequestMapping) aClass.getAnnotation(RequestMapping.class);//类的RequestMapping注解 |
当然前提是类和方法不为null
1 | if (aClass!=null&&method!=null) |
然后就可以通过获得的RequestMapping对象,获得需要的属性。
注意:
虽然是Controller类,但类和方法不能保证都一定有@RequestMapping注解,并且value属性是数组
1 | if (classAnnotation!=null){ |
将两个RequestMapping的value值拼接起来,就拿到一个Controller-方法的URL了
1 | String URL=classURL+methodURL; |
1.3.4. 关于获取参数值和参数名
先获取参数
1 | //获得方法的参数(一个Object数组) |
1.参数值
for循环打印出参数值
1
2
3
4//循环,打印args数组里的值
for (int i=0;i<args.length;i++){
System.out.println("参数:"+args[i]);
}利用Array的toString方法打印参数值
1
System.out.println("传递参数值:"+ Arrays.toString(args));
参考:数组输出的三种方式
2.参数名
1 | ParameterNameDiscoverer dpnd = new DefaultParameterNameDiscoverer(); |
然后做个数据分页即可,操作日志就完成了
2. 登录足迹loginLog
我的做法是:在上文Security登录流程中的userServiceImpl类里,获取用户登录时间、ip。将数据拿到后封装,插入到数据库即可。
1 | package com.SH.Service.ServiceImpl; |
3. 日志数据分页
依然是使用MyBatis的分页插件PageHelper,分页上次说过,这里简要复习,有一些知识的更新。
3.1. 依赖、Jar包
1 | <dependency> |
3.2. 分页后台
这里使用了@RequestParam注解,属性name是前端参数名、required为是否必要、defaultValue为默认值。
分页插件的使用:
分页需要pageNum、pageSize两个参数,int或Integer类型。
PageHelper.startPage(int pageNum,int pageSize)方法后直接跟需要分页的方法即可,在service层写好后调用service也是可以的。
将查询方法返回的List集合交给PageInfo封装
在request域放入PageInfo对象即可
1 | ("/selectBypage") |
PageInfo包装类的属性:
1 | //当前页 |
3.3. 分页前端
3.3.1. 环境准备
EL表达式
前端Jsp页面使用EL表达式较为方便,要使用EL表达式注意将isELIgnored设为false,是否需要设置,要根据web.xml文件的声明部分的xsd版本而定,因为有的版本默认这个属性是true,会将EL表达式当字符串处理。
.jsp页面设置isELIgnored=”false”:
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" |
web.xml,一个默认开启EL的版本:
1 |
|
JSTL标签
在jsp页面头部引入JSP标准标签库
1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> |
3.3.2. 功能实现
数据展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<!--数据列表-->
...前略
<!--使用jstl的forEach标签,进行数据遍历,items是要遍历的集合-->
<c:forEach items="${pageInfo.list}" var="syslog">
<tr>
<td><input name="ids" type="checkbox"></td>
<td>${syslog.id}</td>
<td>${syslog.visitTimeStr }</td>
<td>${syslog.username }</td>
<td>${syslog.ip }</td>
<td>${syslog.url}</td>
<td>${syslog.executionTime}毫秒</td>
<td>${syslog.method}</td>
</tr>
</c:forEach>
...后略
<!--数据列表/-->分页按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<a
href="${pageContext.request.contextPath}/sysLogController/selectBypage.action?pageNum=1&pageSize=${pageInfo.size}" >首页</a>
<a
href="${pageContext.request.contextPath}/sysLogController/selectBypage.action?pageNum=${pageInfo.pageNum-1}&pageSize=${pageInfo.size}">上一页</a>
<!-- 第一页、第二页、第三页...-->
<ul>
<c:forEach begin="1" end="${pageInfo.pages}" var="num">
<li><a href="${pageContext.request.contextPath}/sysLogController/selectBypage.action?pageNum=${num}&pageSize=${pageInfo.size}">${num}</a></li>
</c:forEach>
</ul>
<a href="${pageContext.request.contextPath}/sysLogController/selectBypage.action?pageNum=${pageInfo.pageNum+1}&pageSize=${pageInfo.size}">下一页</a>
<a href="${pageContext.request.contextPath}/sysLogController/selectBypage.action?pageNum=${pageInfo.pages}&pageSize=${pageInfo.size}">尾页</a>改变每页容量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<div class="form-group form-inline">
总共${pageInfo.pages} 页,共${pageInfo.total} 条数据。
每页 <select id="selectSize" class="form-control" onchange="checkChange()">
<option>10</option>
<option>15</option>
<option>20</option>
<option>50</option>
<option>80</option>
</select> 条
</div>
<script>
//改变每页条数js
function checkChange(){
var size=$("#selectSize").val();
location.href= "${pageContext.request.contextPath}/sysLogController/selectBypage.action?pageNum=${pageInfo.pageNum}&pageSize="+size;
}
</script>
项目中使用了AdminLTE来美化页面。
AdminLTE:一款建立在bootstrap和jquery之上的开源的模板主题工具,它提供了一系列响应的、可重复使用的组件,并内置了多个模板页面;同时自适应多种屏幕分辨率,兼容PC和移动端。