SSM
项目搭建和环境配置
01 数据库准备
先了解一些oracle数据库的基本知识,黑马之前的视频有教学如何在虚拟机搭建oracle数据库,然后在本机使用PL/SQL操作数据库。
oracle学习
在数据库中新建一个用户user,可以直接在USER中右键new一个,设置用户名ssm和密码itcast,需要给用户授权,选择角色Role privileges中的connect和resource。
然后我们就可以用刚才创建的用户来登录PL了。
我们可以新建一个SQL页面来编写sql语句,进行建表,添加数据等操作…
02 maven项目搭建
先搭建好maven的父工程,引入需要jar包的坐标,它用来承载项目的各个子模块,模块的创建和工程差不多,除了web层的模块我们需要使用maven的webapp骨架。
- 问题:oracle的依赖报错
跟着视频做的时候出现一个问题,oracle的jar包导入失败,进入本地仓库查看,发现jar包的下载失败。
解决:oracle的jar包导入失败解决
对于项目tomcat部署,可以和老师一样在web层的pom.xml中添加tomcat7插件,然后配置运行,不过我比较习惯直接部署本地的tomcat,比较方便。
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration><port>8888</port></configuration>
<version>2.2</version>
</plugin>
</plugins>
产品操作
01 查询全部产品
public interface IProductDao {
//查询所有产品信息
@Select("select * from product")
public List<Product> findAll() throws Exception;
}
controller调用service,service调用dao,dao层使用注解配置sql语句查询数据库。
@Controller
@RequestMapping("/product")
public class ProductController {
@Autowired
private IProductService productService;
@RequestMapping("/findAll.do") //配置前端控制器的时候拦截的是*.do文件
public ModelAndView findAll() throws Exception {
ModelAndView mv = new ModelAndView();
List<Product> ps = productService.findAll();
mv.addObject("productList",ps);
mv.setViewName("product-list");
return mv;
}
}
- 测试:运行tomcat后跳转到index.jsp,点击超链接访问controller,会查询数据库中产品信息,跳转到产品列表展示页面。
部分展示数据为空,是因为页面上读取的格式是字符串,需要到实体类中进行判断并给str赋值。
因为代码中很多地方需要时间转换,所以在工具类中添加日期转换的工具类DateUtils。
public String getDepartureTimeStr() {
if(departureTime!=null){
departureTimeStr = DateUtils.data2String(departureTime,"yyyy-MM-dd HH:mm:ss");
}
return departureTimeStr;
}
public String getProductStatusStr() {
if(productStatus != null){
//状态 0 关闭 1 开启
if(productStatus==0){
productStatusStr="关闭";
}
if(productStatus==1){
productStatusStr="开启";
}
}
return productStatusStr;
}
02 添加产品
//dao接口:
//添加
@Insert("insert into product(productNum,productName,cityName,departureTime,productPrice,productDesc,productStatus) " +
"values(#{productNum},#{productName},#{cityName},#{departureTime},#{productPrice},#{productDesc},#{productStatus})")
void save(Product product);
//controller:
//产品添加
@RequestMapping("/save.do")
public String save(Product product) throws Exception {
productService.save(product);
return "redirect:findAll.do"; //添加后重新加载列表显示
}
- bug:在添加产品时,从页面提交过来的数据都在字符串类型,而实体类中真正的类型是Date,这时出现了类型转换的问题,SpringMVC中提供了对应的解决方法,一种是使用类型转换器Convert,可以进行全局类型转换。另外两种方式,分别是在实体类中添加日期格式化注解,或者使用属性编辑器(Spring3.1之前,较复杂)
我们这里使用简单的加注解的方式解决类型转换问题:
//实体类属性加注解
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") //局部的处理方式
private Date departureTime; // 出发时间
订单操作
01 查询订单
//查询数据库
//订单的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 = "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;
//订单的控制器
@Controller
@RequestMapping("/orders")
public class OrdersController {
@Autowired
private IOrdersService ordersService;
@RequestMapping("/findAll.do")
public ModelAndView findAll() throws Exception {
ModelAndView mv = new ModelAndView();
List<Orders> ordersList = ordersService.findAll();
mv.addObject("ordersList",ordersList);//orders-list.jsp中定义的变量
mv.setViewName("orders-list");
return mv;
}
}
02 订单分页查询
-
PageHelper 是国内非常优秀的一款开源的mybatis分页插件,它支持基本主流与常用的数据库,例如mysql、oracle、mariaDB、DB2、SQLite、Hsqldb等。
-
使用步骤
- 导入坐标
- 在spring配置文件中配置
- helperDialect :分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。
- reasonable :分页合理化参数,默认值为 false 。当该参数设置为 true 时, pageNum<=0 时会查询第一页, pageNum>pages (超过总数时),会查询最后一页。默认 false 时,直接根据参数进行查询。
- 在你需要进行分页的 MyBatis 查询方法前调用PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页。
- 在controler中接受jsp传递的参数并进行处理
03 订单详情操作
//查询订单涉及其他表的操作,同样需要多表查询,其中关系有一对一,一对多
//多表操作
@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 = "member",column = "memberId",javaType = Member.class,one = @One(select = "com.itheima.ssm.dao.IMemberDao.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;
//旅客和订单是多对多,使用中间表order_traveller 连接
//旅客
public interface ITravellerDao {
//根据订单id查询游客id,再根据游客id查询游客所有信息
@Select("select * from traveller where id in (select travellerId from order_traveller where orderId=#{ordersId})")
public List<Traveller> findByOrdersId(String ordersId) throws Exception;
}
//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;
}
用户操作
Spring Security
- 概述
Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。- 安全包括两个主要操作:
- “认证”,是为用户建立一个他所声明的主体。主题一般式指用户,设备或可以在你系 统中执行动作的其他系统。
- “授权”指的是一个用户能否在你的应用中执行某个操作,在到达授权判断之前,身份的主题已经由 身份验证过程建立了
- 快速入门:
- 导入依赖
- 配置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>SpringSecurity314</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-security.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
</web-app>
- spring security配置
//auto-config配置后,会有一个默认的登录表单,登录成功后会跳转到index.jsp
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<security:http auto-config="true" use-expressions="false">
<!-- intercept-url定义一个过滤规则 pattern表示对哪些url进行权限控制,ccess属性表示在请求对应
的URL时需要什么权限,
默认配置时它应该是一个以逗号分隔的角色列表,请求的用户只需拥有其中的一个角色就能成功访问对应
的URL -->
<security:intercept-url pattern="/**" access="ROLE_USER" />
<!-- auto-config配置后,不需要在配置下面信息 <security:form-login /> 定义登录表单信息
<security:http-basic
/> <security:logout /> -->
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user"
authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin"
authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
- 使用自定义的跳转页面
// 在spring-security.xml中配置
关于用户角色权限我们之后会存储到数据库,而不是放在配置文件。
<!-- 配置不过滤的资源(静态资源及登录相关) -->
<security:http security="none" pattern="/login.html" />
<security:http security="none" pattern="/failer.html" />
<security:http auto-config="true" use-expressions="false" >
<!-- 配置资料连接,表示任意路径都需要ROLE_USER权限 -->
<security:intercept-url pattern="/**" access="ROLE_USER" />
<!-- 自定义登陆页面,login-page 自定义登陆页面 authentication-failure-url 用户权限校验失败之后才会跳转到这个页面,如果数据库中没有这个用户则不会跳转到这个页面。
default-target-url 登陆成功后跳转的页面。 注:登陆页面用户名固定 username,密码 password,action:login -->
<security:form-login login-page="/login.html"
login-processing-url="/login" username-parameter="username"
password-parameter="password" authentication-failure-url="/failer.html"
default-target-url="/success.html" authentication-success-forward-url="/success.html"
/>
<!-- 关闭CSRF,默认是开启的 -->
<security:csrf disabled="true" />
</security:http>
01 用户登录
spring security使用数据库认证
在Spring Security中如果想要使用数据进行认证操作,有很多种操作方式,这里介绍使用UserDetails、UserDetailsService来完成操作。
- UserDetails是一个接口,我们可以认为UserDetails作用是于封装当前进行认证的用户信息,但由于其是一个接口,所以我们可以对其进行实现,也可以使用Spring Security提供的一个UserDetails的实现类User来完成操作。
- UserDetailsService规范化了我们做认证时的具体方法。
登录认证
- IUuserDao继承UserDetailsService ,我们可以在接口的实现类中做一些认证的操作。
public interface IUserService extends UserDetailsService {
}
// UserServiceImpl
@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 user = new User(userInfo.getUsername(),"{noop}"+userInfo.getPassword(),getAuthority(userInfo.getRoles()));
User user = new User(userInfo.getUsername(),"{noop}"+userInfo.getPassword(),userInfo.getStatus() == 0 ? false:true,true,true,true,getAuthority(userInfo.getRoles()));
return user;
}
//这里面的role信息是查询用户的时候携带过来的
//作用就是返回一个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;
}
}
- 查询用户信息,使用多表查询
public interface IUserDao {
@Select("select * from users where username=#{username}")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "email",column = "email"),
@Result(property = "password",column = "password"),
@Result(property = "phoneNum",column = "phoneNum"),
@Result(property = "status",column = "status"),
@Result(property = "roles",column = "id",javaType = java.util.List.class,many = @Many(select = "com.itheima.ssm.dao.IRoleDao.findRoleByUserId")),
})
public UserInfo findByUsername(String username) throws Exception;
}
//查询用户对应角色信息涉及多表查询,多对多使用中间表
public interface IRoleDao {
//根据用户id查找所有对应角色信息
@Select("select * from role where id in (select roleId from users_role where userId=#{userId})")
public List<Role> findRoleByUserId(String userId) throws Exception;
}
- 用户退出(注销)
在jsp页面中点击注销即可退出账号
直接在spring-security中配置,会清除session信息,访问logout.do,然后跳转到登录界面。
02 用户查询
与之前查询产品类似操作,在页面上无法显示的数据大部分是因为数据库中存储的格式,在页面上显示的是string形式,因此在domain中需要去转换。
- 注意:在跳转查询页面时出现一个小bug,用户被拒绝访问报403,原因是在security中我们设置了访问任何页面都需要是特定的角色。
因此在给用户认证的代码处,添加角色我们需要保证从数据库读取的角色名称拼接后和设置的相同。
其他代码的实现比较简单,就是在dao中查询user表所有数据,controller封装后返回给jsp页面显示。
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
@RequestMapping("/findAll.do")
public ModelAndView findAll() throws Exception {
ModelAndView mv = new ModelAndView();
List<UserInfo> userList = userService.findAll();
mv.addObject("userList",userList);
mv.setViewName("user-list");
return mv;
}
}
03 用户添加
点击跳转到添加用户的界面,提交后表单将数据发送给controller,再一层层调用到dao添加用户,需要注意的是,添加数据时用户的密码需要加密。
之前我们在用户认证的时候,使用的密码是明文去访问,是不安全的,因此需要在密码前加:{noop},如果是密文则不需要添加。
配置加密:
在spring-security中有关于密码加密的配置:
可以直接将其封装成bean,也可以自己写一个工具类使用:
//密码加密的工具类
public class BCryptPasswordEncoderUtils {
public 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.println(pwd); //每一次加密的结果都不一样
}
}
- 使用:在userService中,这里直接使用注入,也可以调用工具类的静态方法。
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder; //密码加密
//添加用户
@Override
public void save(UserInfo userInfo) throws Exception {
//对密码进行加密处理
userInfo.setPassword(bCryptPasswordEncoder.encode(userInfo.getPassword())); //加密后在重新赋予
userDao.save(userInfo); //添加的密码是加密后的
}
- 添加用户:
//用户添加
@Insert("insert into users(email,username,password,phoneNum,status) values(#{email},#{username},#{password},#{phoneNum},#{status})")
void save(UserInfo userInfo) throws Exception;
- 添加完毕后跳转到显示列表,显示添加后的用户信息
//controller
//用户添加
@RequestMapping("/save.do")
public String save(UserInfo userInfo) throws Exception {
userService.save(userInfo);
return "redirect:findAll.do"; //添加完应该再查询一次
}
到此,我们可以使用自己添加的用户进行登录了。
04 用户详情
通过id查询用户 – > 查询对应角色 --> 查询角色对应权限,全部信息都封装在userInfo中,然后经过controller处理后到jsp页面显示。
//查询指定id的用户
@RequestMapping("/findById.do")
public ModelAndView findById(String id) throws Exception {
ModelAndView mv = new ModelAndView();
UserInfo userInfo = userService.findById(id);
mv.addObject("user",userInfo);
mv.setViewName("user-show");
return mv;
}
dao层代码:
- 用户和角色直接是多对多关系,权限信息需要通过角色的进一步查询。
//IUserDao
//根据id查询用户信息
@Select("select * from users where id=#{id}")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "email",column = "email"),
@Result(property = "password",column = "password"),
@Result(property = "phoneNum",column = "phoneNum"),
@Result(property = "status",column = "status"),
@Result(property = "roles",column = "id",javaType = java.util.List.class,many = @Many(select = "com.itheima.ssm.dao.IRoleDao.findRoleByUserId")),
})
UserInfo findById(String id) throws Exception;
- 角色与权限也是多对多的关系,通过角色id可以查询对应权限信息
//IRoleDao
//根据用户id查找所有对应角色信息
@Select("select * from role where id in (select roleId from users_role where userId=#{userId})")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "roleName",column = "roleName"),
@Result(property = "roleDesc",column = "roleDesc"),
//会根据role的id来查询当前权限信息
@Result(property = "permissions",column = "id",javaType = java.util.List.class,many = @Many(select = "com.itheima.ssm.dao.IPermissionDao.findPermissionByRoleId")),
})
public List<Role> findRoleByUserId(String userId) throws Exception;
- 权限的查询,需要通过中间表
@Select("select * from permission where id in (select permissionId from role_permission where roleId=#{id})")
public List<Permission> findPermissionByRoleId(String id) throws Exception;
展示信息:
使用AdminLTE框架的树表格,模仿搭建出显示用户详情的页面,因为用户可能有多个角色,所以使用foreach从数据库获取数据构建li,并且一个角色可能对于多个权限,所以需要多嵌套一层foreach,并且权限和用户的对应显示列表需要修改一下。
- 权限的父id应该是角色的id,当前用户具有的角色可能有多个,角色的id不能重复,所以使用varStatus.index+1来表示。(index从0开始)
- 效果:
角色管理&权限管理
后续还会做一些管理,用户管理角色,角色管理权限,现在是为后面的工作铺垫。
01 查询角色
- 权限与角色的查询和添加过程类似,不再赘述
//Controller
@RequestMapping("/findAll.do")
public ModelAndView findAll() throws Exception {
ModelAndView mv = new ModelAndView();
List<Role> roleList = roleService.findAll();
mv.addObject("roleList",roleList);
mv.setViewName("role-list");
return mv;
}
//IRoleDao
//查询所有角色
@Select("select * from role")
List<Role> findAll() throws Exception;
02 添加角色
//Controller
//添加角色
@RequestMapping("/save.do")
public String save(Role role) throws Exception {
roleService.save(role);
return "redirect:findAll.do"; //添加完查询一下
}
//dao
//添加角色
@Insert("insert into role(roleName,roleDesc) values(#{roleName},#{roleDesc})")
void save(Role role) throws Exception;
权限关联与控制
01 用户角色关联
- 先查询用户可以添加的角色,因为用户可以对应多个角色,所以角色查询出来是一个list集合,然后将结果携带到jsp展示。
//Controlle
//查询用户以及用户可以添加的角色
@RequestMapping("/findUserByIdAndAllRole")
public ModelAndView findUserByIdAndAllRole(@RequestParam(name = "id",required = true) String userId) throws Exception {
ModelAndView mv = new ModelAndView();
//1 根据用户id查询用户
UserInfo userInfo = userService.findById(userId);
//2 根据用户id查询可以添加的角色
List<Role> otherRoles = userService.findOtherRoles(userId); //角色是可以多选的
mv.addObject("user",userInfo);
mv.addObject("roleList",otherRoles);
mv.setViewName("user-role-add");
return mv;
}
//dao
//查询用户没有的角色(可添加的)
@Select("select * from role where id not in (select roleId from users_role where userId=#{userId})")
List<Role> findOtherRoles(String userId) throws Exception;
- 将选择的角色添加给用户,jsp页面中用户可以同时用复选框选择多个角色,参数会带一个角色的list集合,我们可以遍历集合,将角色一一添加给用户,添加完毕后再返回用户的展示页面,这时候点击详情,就可以查看用户有的新角色了。
//Controller
//给用户添加角色 -- 往中间表插数据
@RequestMapping("/addRoleToUser.do")
public String addRoleToUser(@RequestParam(name = "userId",required = true) String userId,
@RequestParam(name = "ids",required = true) String[] roleIds) throws Exception {
userService.addRoleToUser(userId,roleIds);
return "redirect:findAll.do";
}
关于param注解解决多参数问题:@Param
//Service
//给用户添加角色
@Override
public void addRoleToUser(String userId, String[] roleIds) throws Exception {
for (String roleId : roleIds) {
userDao.addRoleToUser(userId,roleId);
}
}
//dao
//给用户添加角色
/*
@Param是MyBatis所提供的,作为Dao层的注解,作用是用于传递参数,从而可以与SQL中的的字段名相对应。
报错:mybatis.system... 是因为它将我们传递的两个参数#{userId},#{roleId},都认为是String userId这个对象的属性
*/
@Insert("insert into users_role (userId,roleId) values(#{userId},#{roleId})")
void addRoleToUser(@Param("userId") String userId, @Param("roleId") String roleId) throws Exception;
02 角色权限关联
和上面用户与角色管理类似,先查询角色可以添加的权限,再把选择的权限添加给角色。
//Controller
//根据roleId查询角色,并查询可以添加的权限信息
@RequestMapping("/findRoleByIdAndAllPermission")
public ModelAndView findRoleByIdAndAllPermission(@RequestParam(name = "id",required = true) String roleId) throws Exception {
ModelAndView mv = new ModelAndView();
//根据roleId查询role
Role role = roleService.findById(roleId);
//根据roleId查询可以添加的权限
List<Permission> otherPermissions = roleService.findOtherPermissions(roleId);
mv.addObject("role",role);
mv.addObject("permissionList",otherPermissions);
mv.setViewName("role-permission-add");
return mv;
}
//给角色添加权限
@RequestMapping("/addPermissionToRole")
public String addPermissionToRole(@RequestParam(name = "roleId",required = true) String roleId,
@RequestParam(name = "ids",required = true) String[] permissionIds) throws Exception {
roleService.addPermissionToRole(roleId,permissionIds);
return "redirect:findAll.do";
}
//dao
//根据roleId查询角色可以添加的权限信息
@Select("select * from permission where id not in (select permissionId from role_permission where roleId=#{roleId})")
List<Permission> findOtherPermissions(String roleId) throws Exception;
//给角色添加权限 --- 操作中间表role_permission
@Insert("insert into role_permission(roleId,permissionId) values(#{roleId},#{permissionId})")
void addPermissionToRole(@Param("roleId") String roleId,@Param("permissionId") String permissionId);
03 服务器端方法级权限控制
在服务器端我们可以通过Spring security提供的注解对方法来进行权限控制。Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解,这三种注解默认都是没有启用的,需要单独通过global-method-security元素的对应属性进行启用。
JSR-250注解
步骤:
- spring-security中开启注解
<!--开启JSR-250注解-->
<security:global-method-security jsr250-annotations="enabled"></security:global-method-security>
- 在指定方法上指定
@RequestMapping("/findAll.do")
@RolesAllowed("ADMIN") //只有ADMIN可以访问,也可以是一个数组,省略前缀ROLE_
public ModelAndView findAll() throws Exception {
...
}
- 在pom.xml中导入依赖
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>
- 此时,只有角色为admin的用户才可以访问该方法,其他角色用户被禁止403
直接在页面上显示错误信息不美观,我们可以在web.xml中配置错误的跳转页面,当出现403错误的时候,说明权限不够,就会跳转到指定的jsp页面(403.jsp)
<error-page>
<error-code>403</error-code>
<location>/403.jsp</location>
</error-page>
@Secured注解注解
需要在web.xml中配置,不需要额外导入依赖,是springframework包中带的。
<!--开启Secured注解-->
<security:global-method-security secured-annotations="enabled"></security:global-method-security>
@RequestMapping("/findAll.do")
@Secured("ROLE_ADMIN") //不可省略前缀
public ModelAndView findAll( ...
支持表达式的注解
- 表达式写法:
<!--支持表达式的注解-->
<security:global-method-security pre-post-annotations="enabled"></security:global-method-security>
//controller
//查询所有
@RequestMapping("/findAll.do")
@PreAuthorize("hasRole('ROLE_ADMIN')") //限制角色
public ModelAndView findAll() throws Exception {
...
}
//用户添加
@RequestMapping("/save.do")
@PreAuthorize("authentication.principal.username == 'tom'") //限制用户
public String save(UserInfo userInfo) throws Exception {
...
}
04 页面端权限控制
步骤:
- 在pom.xml中导入依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring.security.version}</version>
</dependency>
- jsp中导入标签
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%>
- 在jsp中我们可以使用以下三种标签,其中authentication代表的是当前认证对象,可以获取当前认证对象信息,其它两个标签可以用于权限控制。
-
authentication 可以获取当前正在操作的用户
例如左侧导航栏用户名显示
-
authorize 可以控制页面上某些标签是否可以显示
设置只有admin可以看见用户管理一栏
这里使用了spel表达式,需要在spring-security中开启:
<!-- 如果设置use-expressions="false",仍想使用espl表达式,则打开这个bean -->
<bean id="webexpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler"></bean>
AOP日志
01 日志添加
需要在数据库中创建存储日志信息的表,并添加对应的实体类,另外需要编写一个切面类处理日志信息,将日志信息封装到实体类中,封装后的日志信息就可以调用service – 》 dao 存储到数据库中。
//LogAop。java
@Component
@Aspect
public class LogAop {
@Autowired
private HttpServletRequest request; //得到request,web.xml中配置监听器
@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(); //获取每一个参数的class存储到数组中
}
method = clazz.getMethod(methodName,classArgs);
}
}
//后置通知
@After("execution(* com.itheima.ssm.controller.*.*(..))")
public void doAfter(JoinPoint jp) throws Exception {
//获取访问时长
long time = new Date().getTime() - visitTime.getTime();
//获取url
String 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]; //拼接url /orders/findAll.do
}
}
}
//获取当前访问的ip地址
String ip = request.getRemoteAddr();
//获取当前操作的用户
//SecurityContext context = request.getSession().getAttribute("SPRING_SECURITY_CONTEXT") 也可以通过request对象获取
SecurityContext context = SecurityContextHolder.getContext(); //从上下文中获取当前登录的用户
User user = (User) context.getAuthentication().getPrincipal();
String username = user.getUsername();
//将日志相关信息封装到SysLog对象
Syslog syslog = new Syslog();
syslog.setExecutionTime(time);
syslog.setIp(ip);
syslog.setMethod("[类名]"+clazz.getName()+"[方法名]"+method.getName());
syslog.setUrl(url);
syslog.setUsername(username);
syslog.setVisitTime(visitTime);
//调用service完成操作
sysLogService.save(syslog);
}
}
- 在获取ip地址时,我们使用了request对象,需要在web.xml中配置监听器,这样就可以直接注入使用这个对象了。
- SecurityContext 我们也可以用request获取,或者使用Security的方法。
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
02 日志查询
@Controller
@RequestMapping("/sysLog")
public class SysLogController {
@Autowired
private ISysLogService sysLogService;
@RequestMapping("/findAll.do")
public ModelAndView findAll() throws Exception {
ModelAndView mv = new ModelAndView();
List<Syslog> syslogList = sysLogService.findAll();
mv.addObject("sysLogs",syslogList);
mv.setViewName("syslog-list");
return mv;
}
}
public interface ISysLogDao {
//添加日志
@Insert("insert into syslog(visitTime,username,ip,url,executionTime,method) values(#{visitTime},#{username},#{ip},#{url},#{executionTime},#{method})")
public void save(Syslog syslog) throws Exception;
//查询日志
@Select("select * from sysLog")
List<Syslog> findAll() throws Exception;
}
- 关于日志查询,如果是访问了日志查询的controller里的方法,其实是不必记录查询日志这个动作的,可以在LogAop后置通知中设置,不要调用service写入。
if(clazz.getName() == "com.itheima.ssm.controller.SysLogController"){
return;
}
//调用service完成操作
sysLogService.save(syslog);
总结:
- 用这个案例了解了三种框架如何整合使用,很多配置的地方有些记不清了,需要多复习。
- 难点在权限角色控制这些地方,不同的角色可以访问的范围不同,但是我做出来的程序有点bug,普通用户无论访问那个方法都行权限不够,只有admin可以做其他操作。
- 对于页面的编写,使用框架会很方便,案例里传递参数都使用了ModelAndView这个方法,还有其他参数传递的方法要了解。
- 关于表查询的一对多,多对多的sql语句,写熟练了就好,对于oracle数据库不熟练,可能会有bug出现,例如之前的中文乱码,或者是存储数据的长度超出。