文章目录
技术栈 —— Spring + SpringMVC + Mybatis +(AdminLTE模板工具+jsp、Oracle)
一、环境搭建及产品订单操作
技术难点:
1、项目环境搭建
2、页面上、实体类、数据库中的关于时间类型字段的处理
解决:
- 页面上接收后传递到后端的都是字符串类型,Oracle数据库中相关字段为timestamp 类型,实体类中对应时间字段应定义成如下形式来起到中转的作用
public Class Product{
...
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm") //将从页面上接收的字符串类型数据转换为Date类型
private Date departureTime;
private String departureTimeStr; //存放上面时间的字符串形式
/*将Date类型数据转换为字符串类型,并封装在相应Str属性上发送给页面*/
public String getDepartureTimeStr() {
if(departureTime!=null){
departureTimeStr= DateUtils.date2String(departureTime,"yyyy-MM-dd HH:mm:ss");
}
return departureTimeStr;
}
...
}
//===========================================================================
/*自定义的工具类*/
public class DateUtils {
//日期转换成字符串
public static String date2String(Date date, String patt) {
SimpleDateFormat sdf = new SimpleDateFormat(patt);
String format = sdf.format(date);
return format;
}
//字符串转换成日期
public static Date string2Date(String str, String patt) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(patt);
Date parse = sdf.parse(str);
return parse;
}
}
3、在Service层实现分页查询
- 使用的是PageHelper插件
- 准备工作:导入依赖、在applicationContext.xml配置相关信息、在查询语句上开启分页
配置文件中的
<prop key="reasonable">true</prop>
为分页合理化配置,开启则不会产生越界行为(例如在当前处于最后一页的情况下,点击下一页不会继续跳转,也不会报出异常)。
原理就是
当请求页数大于最大页数时,将直接当做请求最后一页处理;同样的,当请求页数<1时,将当做请求第一页处理。
- 跳转到展示分页数据的页面(要带上
page
和size
参数,并展示第一页)
<li id="system-setting"><a
href="${pageContext.request.contextPath}/orders/findAll.do?page=1&size=4">
<i class="fa fa-circle-o"></i> 订单管理
</a></li>
- 翻页按钮上的处理
<li>
<a href="${pageContext.request.contextPath}/orders/findAll.do?page=1&size=${pageInfo.pageSize}" aria-label="Previous">首页</a>
</li>
<li>
<a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageInfo.pageNum-1}&size=${pageInfo.pageSize}">上一页</a></li>
<c:forEach begin="1" end="${pageInfo.pages}" var="pageNum">
<li>
<a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageNum}&size=${pageInfo.pageSize}">${pageNum}</a>
</li>
</c:forEach>
<li>
<a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageInfo.pageNum+1}&size=${pageInfo.pageSize}">下一页</a>
</li>
<li>
<a href="${pageContext.request.contextPath}/orders/findAll.do?page=${pageInfo.pages}&size=${pageInfo.pageSize}" aria-label="Next">尾页</a>
</li>
- Controller层处理
@Controller
@RequestMapping("/orders")
public class OrdersController {
@Autowired
private IOrdersService ordersService;
@RequestMapping("/findAll.do")
public ModelAndView findAll(@RequestParam(name = "page", required = true, defaultValue = "1") int page,
@RequestParam(name = "size", required = true, defaultValue = "4") int size) throws Exception {
ModelAndView mv = new ModelAndView();
List<Orders> ordersList = ordersService.findAll(page, size);
//PageInfo就是一个分页Bean
PageInfo pageInfo=new PageInfo(ordersList);
mv.addObject("pageInfo",pageInfo);
mv.setViewName("orders-page-list");
return mv;
}
}
- Service层调用
public List<Orders> findAll(int page, int size) throws Exception {
//参数pageNum 是页码值 参数pageSize 代表是每页显示条数
PageHelper.startPage(page, size);
return ordersDao.findAll();
}
- Dao层实现
@Select("select * from orders")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "orderNum", column = "orderNum"),
@Result(property = "orderTime", column = "orderTime"),
@Result(property = "orderStatus", column = "orderStatus"),
@Result(property = "peopleCount", column = "peopleCount"),
@Result(property = "peopleCount", column = "peopleCount"),
@Result(property = "payType", column = "payType"),
@Result(property = "orderDesc", column = "orderDesc"),
@Result(property = "product", column = "productId", javaType = Product.class, one = @One(select = "com.itheima.ssm.dao.IProductDao.findById")),
})
public List<Orders> findAll() throws Exception;
4、多表查询
- 三表之间的关系:
- Dao层的设计
public interface IOrdersDao {
//多表操作
@Select("select * from orders where id=#{ordersId}")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "orderNum", column = "orderNum"),
@Result(property = "orderTime", column = "orderTime"),
@Result(property = "orderStatus", column = "orderStatus"),
@Result(property = "peopleCount", column = "peopleCount"),
@Result(property = "peopleCount", column = "peopleCount"),
@Result(property = "payType", column = "payType"),
@Result(property = "orderDesc", column = "orderDesc"),
@Result(property = "product", column = "productId", javaType = Product.class, one = @One(select = "com.itheima.ssm.dao.IProductDao.findById")),
@Result(property = "travellers",column = "id",javaType =java.util.List.class,many = @Many(select = "com.itheima.ssm.dao.ITravellerDao.findByOrdersId"))
})
public Orders findById(String ordersId) throws Exception;
}
public interface IProductDao {
//根据id查询产品
@Select("select * from product where id=#{id}")
public Product findById(String id) throws Exception;
}
public interface ITravellerDao {
@Select("select * from traveller where id in (select travellerId from order_traveller where orderId=#{ordersId})")
public List<Traveller> findByOrdersId(String ordersId) throws Exception;
}
- Service层调用
@Override
public Orders findById(String ordersId) throws Exception{
return ordersDao.findById(ordersId);
}
- Controller层的处理
@RequestMapping("/findById.do")
public ModelAndView findById(@RequestParam(name = "id", required = true) String ordersId) throws Exception {
ModelAndView mv = new ModelAndView();
Orders orders = ordersService.findById(ordersId);
mv.addObject("orders",orders);
mv.setViewName("orders-show");
return mv;
}
二、springSecurity框架
技术难点:
1、springSecurity的登入认证和授权
- 它是 Spring 项目组中用来提供安全认证服务的框架。
- 如何在项目中使用它
spring-security.xml
配置文件
<!-- 配置不拦截的资源 -->
<security:http pattern="/login.jsp" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<!--配置具体的规则
auto-config="true" 不用自己编写登录的页面,框架提供默认登录页面
use-expressions="false" 是否使用SPEL表达式(没学习过)
-->
<security:http auto-config="true" use-expressions="false">
<!-- 配置具体的拦截的规则 pattern="请求路径的规则"
access="访问系统的人,必须有ROLE_USER、ROLE_ADMIN的角色"
-->
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN"/>
<!-- 定义跳转的具体的页面 (接收参数名称默认就是:username/password)-->
<security:form-login //
login-page="/login.jsp"
login-processing-url="/login.do" //登入表单的发送路径(不是自己定义的Controller)
default-target-url="/index.jsp" //账号密码正确但无权限则进入该页面
authentication-failure-url="/failer.jsp"
authentication-success-forward-url="/pages/main.jsp" />
web.xml
中配置的filter-name不能修改(源码中用会直接引用这个名字)。
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- 自定义UserService
/*自定义UserService接口必须实现 UserDetailsService */
public interface IUserService extends UserDetailsService {
}
//=========================================================
@Service("userService")
@Transactional
public class UserServiceImpl implements IUserService {
@Autowired
private IUserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo userInfo = null;
try {
userInfo = userDao.findByUsername(username);
} catch (Exception e) {
e.printStackTrace();
}
//处理自己的用户对象封装成UserDetails(User是它的实现类,springSecurity自带的)
User user = new User(userInfo.getUsername(), "{noop}" + userInfo.getPassword(),
userInfo.getStatus() == 0 ? false : true, true, true, true,
getAuthority(userInfo.getRoles()));
return user;
}
//作用就是返回一个List集合,集合中装入的是角色描述
public List<SimpleGrantedAuthority> getAuthority(List<Role> roles) {
List<SimpleGrantedAuthority> list = new ArrayList<>();
for (Role role : roles) {
list.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
}
return list;
}
}
这里补充一点,关于上述代码中User参数里的
"{noop}" + userInfo.getPassword(),
- 因为此时的数据时我手动添加到数据库中的,密码为明文,没有用到SpringSecurity的加密技术,所以前面要加上
{noop}
来强调密码无加密。- 后续我们说到BCryptPasswordEncoder加密时,这里的密码参数前面就不用加上
{noop}
了。
- 下面是SpringSecurity框架中自带的User类部分代码
public class User implements UserDetails, CredentialsContainer {
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired; //帐户是否过期
private final boolean accountNonLocked; //帐户是否锁定
private final boolean credentialsNonExpired; //认证是否过期
private final boolean enabled; //帐户是否可用
}
2、BCryptPasswordEncoder加密
- 说明:对于同一密码每次加密结果都不同。下面来验证一下
public class BCryptPasswordEncoderUtils {
private static BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
public static String encodePassword(String password){
return bCryptPasswordEncoder.encode(password);
}
public static void main(String[] args) {
String password="123";
String pwd = encodePassword(password);
System.out.print(pwd.length());
}
}
- 两次的运行结果分别是:
$2a$10$tJHudmJh6MRPdiL7mv0yfe0nZJbDHuhl7sSTnqNC4DauMik9ppi4K
$2a$10$Ce8LB3jdYDZ2f6HB281zA.4eC7v6ziJdK8MMWg0Yu8ETMg5ToMpIe
- 原理:
BCryptPasswordEncoder使用哈希算法+随机盐来对字符串加密。因为哈希是一种不可逆算法,所以密码认证时需要使用相同的算法+盐值(相同的加密策略)来对待校验的明文进行加密,然后比较这两个密文来进行验证。
- 如何在项目中使用该加密方式呢?
- 在spring-security.xml中配置加密类以及加密方式
<!-- 切换成数据库中的用户名和密码 -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userService">
<!-- 配置加密的方式-->
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<!-- 配置加密类 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
- 在账号的保存操作上多加一步加密处理
@Service("userService")
@Transactional
public class UserServiceImpl implements IUserService {
@Autowired
private IUserDao userDao;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
...
@Override
public void save(UserInfo userInfo) throws Exception {
//对密码进行加密处理
userInfo.setPassword(bCryptPasswordEncoder.encode(userInfo.getPassword()));
userDao.save(userInfo);
}
...
这里的
@Service("userService")
的参数名称要和spring-security.xml
配置文件中相同
三、用户/角色/权限之间的关联操作
技术要点:
1、对于相关联操作的思想
- 由于本次项目实际需求,这里本没有使用到Mybatis中的级联CUD;而是直接去操作中间表来实现对象多对多关系的更新。
2、Mybatis中insert的注意事项
- 先看看项目中的相关代码
@Insert("insert into users_role(userId,roleId) values(#{userId},#{roleId})")
void addRoleToUser(@Param("userId") String userId, @Param("roleId") String roleId);
- @Param注解
当我们向sql语句中插入的参数大于1个时,就必须得用
@Param("数据库中的字段名称")
注解来将我们自定义的参数与数据库中的相关字段给对应上,
四、服务器端方法级权限控制
技术难点:(以下三种方式都是默认关闭的)
1、JSR-250注解
- 基本用法:
- 对上述基本用法的补充说明:
@RolesAllowed
表示访问对应方法时所应该具有的角色
示例: @RolesAllowed({"USER", "ADMIN"})
该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。
这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
@PermitAll
表示允许所有的角色进行访问,也就是说不进行权限控制
@DenyAll
是和PermitAll相反的,表示无论什么角色都不能访问
- 若权限不符合注解规定,则会在客户端上显示出403的错误信息,403的错误类型就是权限不足。
- 解决办法:
在
web.xml
文件中配置相应的错误页面:<error-page> <error-code>403</error-code> <location>/403.jsp</location> </error-page>
2、@Secured注解(SpringSecurity自带的)
-
基本用法
-
产生的效果以及对于权限不足的解决办法同
JSR-250注解
相同。
3、支持表达式的注解(SpEL表达式)
-
基本用法
-
对上述
基与表达式注解
的补充说明:
支持表达式的注解在使用上会比前两种注解更加灵活,功能更加强大;
但同样的与其对于的学习成本也更高。
下面是比较复杂的一个示例:
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
void changePassword(@P("userId") long userId ){ ... }
//这里表示在changePassword方法执行之前,
//先判断方法参数userId的值是否等于principal中保存的当前用户的 userId,
//或者当前用户是否具有ROLE_ADMIN权限,两种符合其一,就可以访问该方法。
- 产生的效果以及对于权限不足的解决办法同
JSR-250注解
相同。
五、页面端标签控制权限
技术难点:
1、authentication/authorize的使用
- 基本用法
- 对于上述
security:authorize
用法的补充
authorize
是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示:<security:authorize access="" method="" url="" var=""></security:authorize>
- access: 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限
- method:method属性是配合url属性一起使用的,表示用户应当具有指定url指定method访问的权限,
- method的默认值为GET,可选值为http请求的7种方法
- url:url表示如果用户拥有访问指定url的权限即表示可以显示authorize标签包含的内容
- var:用于指定将权限鉴定的结果存放在pageContext的哪个属性中
六、AOP日志
技术难点:(JoinPoint)
1、JoinPoint
- 请参考博客:JoinPoint的用法
2、获取访问的类和方法
- 方法要分
有参
和无参
@Component
@Aspect
public class LogAop {
@Autowired
private HttpServletRequest request;
@Autowired
private ISysLogService sysLogService;
private Date visitTime; //开始时间
private Class clazz; //访问的类
private Method method;//访问的方法
//前置通知 主要是获取开始时间,执行的类是哪一个,执行的是哪一个方法
@Before("execution(* com.itheima.ssm.controller.*.*(..))")
public void doBefore(JoinPoint jp) throws NoSuchMethodException {
visitTime = new Date();//当前时间就是开始访问的时间
clazz = jp.getTarget().getClass(); //具体要访问的类
String methodName = jp.getSignature().getName(); //获取访问的方法的名称
Object[] args = jp.getArgs();//获取访问的方法的参数
//获取具体执行的方法的Method对象
if (args == null || args.length == 0) {
method = clazz.getMethod(methodName); //只能获取无参数的方法
} else {
Class[] classArgs = new Class[args.length];
for (int i = 0; i < args.length; i++) {
classArgs[i] = args[i].getClass();
}
method = clazz.getMethod(methodName, classArgs);
}
}
......
3、获取访问的url和访问的时长
访问时长
=@After
执行时间 -@Before
执行时间url
= 类上的@RequestMapping("/父路径")
+ 方法上的@RequestMapping("/子路径")
//后置通知
@After("execution(* com.itheima.ssm.controller.*.*(..))")
public void doAfter(JoinPoint jp) throws Exception {
long time = new Date().getTime() - visitTime.getTime(); //获取访问的时长
String url = "";
//获取url
if (clazz != null && method != null && clazz != LogAop.class) {
//1.获取类上的@RequestMapping("/orders")
RequestMapping classAnnotation = (RequestMapping) clazz.getAnnotation(RequestMapping.class);
if (classAnnotation != null) {
String[] classValue = classAnnotation.value();
//2.获取方法上的@RequestMapping("/xxx")
RequestMapping methodAnnotation = method.getAnnotation(RequestMapping.class);
if (methodAnnotation != null) {
String[] methodValue = methodAnnotation.value();
url = classValue[0] + methodValue[0];
...
4、获取访问的ip和当前操作的用户
- 用
HttpServletRequest
的getRemoteAddr()
方法获取ip - 通过
SecurityContextHolder.getContext()
方法获取当前用户
// 也可以从
request.getSession
中获取
request.getSession().getAttribute(“SPRING_SECURITY_CONTEXT”)
//获取访问的ip
String ip = request.getRemoteAddr();
//获取当前操作的用户
SecurityContext context = SecurityContextHolder.getContext();//从上下文中获了当前登录的用户
User user = (User) context.getAuthentication().getPrincipal();
String username = user.getUsername();