此项目使用Spring+SpringMVC+MyBatis框架整合,用于企业后台权限管理。数据库使用MySQL,前端页面使用Jsp基于AdminLTE模板进行改写。html
数据库使用MySQL前端
由 userId 和 roleId 构成,分别为users表 以及 role表的外键,用来关联用户与角色的多对多关系java
由 perimissionId 和 roleId 构成,分别为permission表 以及 role表的外键,用来关联资源权限与角色的多对多关系。mysql
在 web.xml 中git
整合思路:将mybatis配置文件(mybatis.xml)中内容配置到spring配置文件中。github
建立 db.properties 存放数据库链接属性web
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
在 applicationContext.xml 中配置链接池spring
将 SqlSessionFactory 交给IOC管理sql
自动扫描全部Mapper接口和文件数据库
配置Spring事务
配置Spring的声明式事务管理
主要包括查询全部产品以及添加产品两个功能,下面是两个功能的流程图。
商品的状态属性数据库存放的为int数据 productStatus,0表明关闭1表明开启,实体类中多添加了一个String类型的变量为productStatusStr,在该变量的getter中对productStatus进行判断并处理成对应属性以放到页面中展现。
出发时间的属性经过 @DateTimeFormat(pattern="yyyy-MM-dd HH:mm") 注解来转换格式,并编写了一个工具类data2String,将时间类转换成字符串用于页面展现。
实体类中加日期格式化注解
@DateTimeFormat(pattern="yyyy-MM-dd hh:MM") private Date creationTime;
属性编辑器
spring3.1以前 在Controller类中经过@InitBinder完成
/** * 在controller层中加入一段数据绑定代码 * @param webDataBinder */ @InitBinder public void initBinder(WebDataBinder webDataBinder) throws Exception{ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm"); simpleDateFormat.setLenient(false); webDataBinder.registerCustomEditor(Date.class , new CustomDateEditor(simpleDateFormat , true)); }
**备注:自定义类型转换器必须实现PropertyEditor接口或者继承PropertyEditorSupport类 **
写一个类 extends propertyEditorSupport(implements PropertyEditor){ public void setAsText(String text){ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy -MM-dd hh:mm"); Date date = simpleDateFormat.parse(text); this.setValue(date); } public String getAsTest(){ Date date = (Date)this.getValue(); return this.dateFormat.format(date); } }
类型转换器Converter
(spring 3.0之前使用正常,之后的版本须要使用< mvc:annotation-driven/>
注册使用)使用xml配置实现类型转换(系统全局转换器)
(1)注册conversionservice
<!-- 注册ConversionService--> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.ezubo.global.portal.util.StringToDateConverter"> <constructor-arg index="0" value="yyyy-MM-dd hh:mm"/> </bean> </set> </property> </bean>
StringToDateConverter.java的实现
public class StringToDateConverter implements Converter<String,Date> { private static final Logger logger = LoggerFactory.getLogger(StringToDateConverter.class); private String pattern; public StringToDateConverter(String pattern){ this.pattern = pattern; } public Date convert(String s) { if(StringUtils.isBlank(s)){ return null; } SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); simpleDateFormat.setLenient(false); try{ return simpleDateFormat.parse(s); }catch(ParseException e){ logger.error("转换日期异常:"+e.getMessage() , e); throw new IllegalArgumentException("转换日期异常:"+e.getMessage() , e); } } }
(2)使用 ConfigurableWebBindingInitializer 注册conversionService
<!--使用 ConfigurableWebBindingInitializer 注册conversionService--> <bean id="webBindingInitializer" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer"> <property name="conversionService" ref="conversionService"/> </bean>
(3)注册ConfigurableWebBindingInitializer到RequestMappingHandlerAdapter
<!-- 注册ConfigurableWebBindingInitializer 到RequestMappingHandlerAdapter--> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="webBindingInitializer" ref="webBindingInitializer"/> <!-- 线程安全的访问session--> <property name="synchronizeOnSession" value="true"/> </bean>
(spring 3.2之后使用正常)使用<mvc:annotation-driven/>
注册conversionService
(1)注册ConversionService
<!-- 注册ConversionService--> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.ezubo.global.portal.util.StringToDateConverter"> <constructor-arg index="0" value="yyyy-MM-dd hh:mm"/> </bean> </set> </property> </bean>
(2)须要修改springmvc.xml配置文件中的annotation-driven,增长属性conversion-service指向新增的 conversionService。
<mvc:annotation-driven conversion-service="conversionService"> <mvc:message-converters register-defaults="true"> <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter"> <property name="supportedMediaTypes" value="text/html;charset=UTF-8"/> <!--转换时设置特性--> <property name="features"> <array> <!--避免默认的循环引用替换--> <ref bean="DisableCircularReferenceDetect"/> <ref bean="WriteMapNullValue"/> <ref bean="WriteNullStringAsEmpty"/> <ref bean="WriteNullNumberAsZero"/> </array> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>
在此项目中使用的是第一种,比较简便。
订单操做的相关功能介绍:
订单的查询操做,它主要完成简单的多表查询操做,查询订单时,须要查询出与订单关联的其它表中信息。下图为订单表及其关联表关系。
下图为查询全部订单流程:
下图为查询订单详情流程:
使用PageHelper进行分页查询,PageHelper是国内很是优秀的一款开源的mybatis分页插件,它支持基本主流与经常使用的数据库,例如mysql、oracle、mariaDB、DB二、SQLite、Hsqldb等。
PageHelper使用起来很是简单,只须要导入依赖而后在spring配置文件中配置后便可使用。
分页插件参数介绍:
helperDialect
:分页插件会自动检测当前的数据库连接,自动选择合适的分页方式。 你能够配置offsetAsPageNum
:默认值为 false ,该参数对使用 RowBounds 做为分页参数时有效。 当该参数设置为rowBoundsWithCount
:默认值为 false ,该参数对使用 RowBounds 做为分页参数时有效。 当该参数设置pageSizeZero
:默认值为 false ,当该参数设置为 true 时,若是 pageSize=0 或者 RowBounds.limit =reasonable
:分页合理化参数,默认值为 false 。当该参数设置为 true 时, pageNum<=0 时会查询第一params
:为了支持 startPage(Object params) 方法,增长了该参数来配置参数映射,用于从对象中根据属pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
supportMethodsArguments
:支持经过 Mapper 接口参数来传递分页参数,默认值 false ,分页插件会从查autoRuntimeDialect
:默认值为 false 。设置为 true 时,容许在运行时根据多数据源自动识别对应方言closeConn
:默认值为 true 。当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类基本使用有6种方式,最经常使用的有两种:
List<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(1, 10));
使用这种调用方式时,可使用RowBounds参数进行分页,这种方式侵入性最小,经过RowBounds方式调用只是使用这个参数并无增长其余任何内容。分页插件检测到使用了RowBounds参数时,就会对该查询进行物理分页。
关于这种方式的调用,有两个特殊的参数是针对 RowBounds
的,具体参考上面的分页插件参数介绍。
注:不仅有命名空间方式能够用RowBounds,使用接口的时候也能够增长RowBounds参数,例如:
//这种状况下也会进行物理分页查询 List<Country> selectAll(RowBounds rowBounds);
注意: 因为默认状况下的
RowBounds
没法获取查询总数,分页插件提供了一个继承自RowBounds
的
PageRowBounds
,这个对象中增长了total
属性,执行分页查询后,能够从该属性获得查询总数。
这种方式在你须要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage 静态方法便可,紧
跟在这个方法后的第一个MyBatis 查询方法会被进行分页。
例如:
//获取第1页,10条内容,默认查询总数count PageHelper.startPage(1, 10); //紧跟着的第一个select方法会被分页 List<Country> list = countryMapper.selectIf(1);
使用步骤总结以下:
主要涉及用户、角色、资源权限三个模块的功能,下图为三表的关系。
Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。
Spring Security 为基于J2EE企业应用软件提供了全面安全服务。包括两个主要操做:
快速入门步骤以下:
使用数据库完成springSecurity用户登陆流程:
spring security的配置
<security:authentication-manager> <security:authentication-provider user-service-ref="userService"> <!-- 配置加密的方式 <security:password-encoder ref="passwordEncoder"/> --> </security:authentication-provider> </security:authentication-manager>
Service
@Service("userService") @Transactional public class UserServiceImpl implements IUserService { @Autowired private IUserDao userDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfo userInfo = userDao.findByUsername(username); List<Role> roles = userInfo.getRoles(); List<SimpleGrantedAuthority> authoritys = getAuthority(roles); User user = new User(userInfo.getUsername(), "{noop}" + userInfo.getPassword(), userInfo.getStatus() == 0 ? false : true, true, true, true, authoritys); return user; } private List<SimpleGrantedAuthority> getAuthority(List<Role> roles) { List<SimpleGrantedAuthority> authoritys = new ArrayList(); for (Role role : roles) { authoritys.add(new SimpleGrantedAuthority(role.getRoleName())); } return authoritys; } }
这里从userInfo中 getPassword 前面须要加上"{noop}"是由于数据库中的密码还未进行加密,后续在添加用户中进行加密处理后便可删除。
Dao
public interface IUserDao { @Select("select * from user where id=#{id}") public UserInfo findById(Long id) throws Exception; @Select("select * from user where username=#{username}") @Results({ @Result(id = true, property = "id", column = "id"), @Result(column = "username", property = "username"), @Result(column = "email", property = "email"), @Result(column = "password", property = "password"), @Result(column = "phoneNum", property = "phoneNum"), @Result(column = "status", property = "status"), @Result(column = "id", property = "roles", javaType = List.class, many = @Many(select = "com.itheima.ssm.dao.IRoleDao.findRoleByUserId")) }) public UserInfo findByUsername(String username); }
使用spring security完成用户退出,很是简单
<security:logout invalidate-session="true" logout-url="/logout.do" logout-success- url="/login.jsp" />
<a href="${pageContext.request.contextPath}/logout.do" class="btn btn-default btn-flat">注销</a>
<!-- 配置加密类 --> <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
Dao
@Select("select * from user where id=#{id}") @Results({ @Result(id = true, property = "id", column = "id"), @Result(column = "username", property = "username"), @Result(column = "email", property = "email"), @Result(column ="password", property = "password"), @Result(column = "phoneNum", property = "phoneNum"), @Result(column ="status", property = "status"), @Result(column = "id", property = "roles", javaType = List.class, many = @Many(select = "com.itheima.ssm.dao.IRoleDao.findRoleByUserId")) }) public UserInfo findById(Long id) throws Exception; @Select("select * from role where id in( select roleId from user_role where userId=#{userId})") @Results( { @Result(id=true,column="id",property="id"), @Result(column="roleName",property="roleName"), @Result(column="roleDesc",property="roleDesc"), @Result(column="id",property="permissions",javaType=List.class,many=@Many(select="com.itheima.ssm .dao.IPermissionDao.findByRoleId")) }) public List<Role> findRoleByUserId(Long userId);
咱们须要将用户的全部角色及权限查询出来因此须要调用IRoleDao中的findRoleByUserId,而在IRoleDao中须要调用IPermissionDao的findByRoleId
@Select("select * from permission where id in (select permissionId from role_permission where roleId=#{roleId})") public List<Permission> findByRoleId(Long roleId);
资源权限查询以及添加的流程和角色管理模块的同样(参考上图),只是针对的表不一样。
用户与角色之间是多对多关系,咱们要创建它们之间的关系,只须要在中间表user_role插入数据便可。
流程以下:
角色与权限之间是多对多关系,咱们要创建它们之间的关系,只须要在中间表role_permission插入数据便可。
流程和用户角色关联相同,参考上图。
在服务器端咱们能够经过Spring security提供的注解对方法来进行权限控制。Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解,这三种注解默认都是没有启用的,须要单独经过global-method-security元素的对应属性进行启用。
示例:
@RolesAllowed({"USER", "ADMIN"})
该方法只要具备"USER", "ADMIN"任意一种权限就能够访问。这里能够省略前缀ROLE_,实际的权限多是ROLE_ADMIN
示例: @PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)") void changePassword(@P("userId") long userId ){ } 这里表示在changePassword方法执行以前,判断方法参数userId的值是否等于principal中保存的当前用户的userId,或者当前用户是否具备ROLE_ADMIN权限,两种符合其一,就能够访问该方法。
示例: @PostAuthorize User getUser("returnObject.userId == authentication.principal.userId or hasPermission(returnObject, 'ADMIN')");
示例: @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public Account readAccount(Long id); @Secured("ROLE_TELLER")
在jsp页面中咱们可使用spring security提供的权限标签来进行权限控制
导入:
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>version</version> </dependency>
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%>
在jsp中咱们可使用如下三种标签,其中authentication表明的是当前认证对象,能够获取当前认证对象信息,例如用户名。其它两个标签咱们能够用于权限控制
<security:authentication property="" htmlEscape="" scope="" var=""/>
authorize是用来判断普通权限的,经过判断用户是否具备对应的权限而控制其所包含内容的显示
<security:authorize access="" method="" url="" var=""></security:authorize>
accesscontrollist标签是用于鉴定ACL权限的。其一共定义了三个属性:hasPermission、domainObject和var,
其中前两个是必须指定的
<security:accesscontrollist hasPermission="" domainObject="" var=""></security:accesscontrollist>
基于AOP来获取每一次操做的访问时间、操做者用户名、访问ip、访问资源url、执行市场以及访问方法存入到数据库日志表sysLog中,并展现到页面中。
流程以下:
@Component @Aspect public class LogAop { @Autowired private HttpServletRequest request; @Autowired private ISysLogService sysLogService; private Date startTime; // 访问时间 private Class executionClass;// 访问的类 private Method executionMethod; // 访问的方法 // 主要获取访问时间、访问的类、访问的方法 @Before("execution(* com.itheima.ssm.controller.*.*(..))") public void doBefore(JoinPoint jp) throws NoSuchMethodException, SecurityException { startTime = new Date(); // 访问时间 // 获取访问的类 executionClass = jp.getTarget().getClass(); // 获取访问的方法 String methodName = jp.getSignature().getName();// 获取访问的方法的名称 Object[] args = jp.getArgs();// 获取访问的方法的参数 if (args == null || args.length == 0) {// 无参数 executionMethod = executionClass.getMethod(methodName); // 只能获取无参数方法 } else { // 有参数,就将args中全部元素遍历,获取对应的Class,装入到一个Class[] Class[] classArgs = new Class[args.length]; for (int i = 0; i < args.length; i++) { classArgs[i] = args[i].getClass(); } executionMethod = executionClass.getMethod(methodName, classArgs);// 获取有参数方法 } } // 主要获取日志中其它信息,时长、ip、url... @After("execution(* com.itheima.ssm.controller.*.*(..))") public void doAfter(JoinPoint jp) throws Exception { // 获取类上的@RequestMapping对象 if (executionClass != SysLogController.class) { RequestMapping classAnnotation = (RequestMapping)executionClass.getAnnotation(RequestMapping.class); if (classAnnotation != null) { // 获取方法上的@RequestMapping对象 RequestMapping methodAnnotation = executionMethod.getAnnotation(RequestMapping.class); if (methodAnnotation != null) { String url = ""; // 它的值应该是类上的@RequestMapping的value+方法上的@RequestMapping的value url = classAnnotation.value()[0] + methodAnnotation.value()[0]; SysLog sysLog = new SysLog(); // 获取访问时长 Long executionTime = new Date().getTime() - startTime.getTime(); // 将sysLog对象属性封装 sysLog.setExecutionTime(executionTime); sysLog.setUrl(url); // 获取ip String ip = request.getRemoteAddr(); sysLog.setIp(ip); // 能够经过securityContext获取,也能够从request.getSession中获取 SecurityContext context = SecurityContextHolder.getContext(); //request.getSession().getAttribute("SPRING_SECURITY_CONTEXT") String username = ((User) (context.getAuthentication().getPrincipal())).getUsername(); sysLog.setUsername(username); sysLog.setMethod("[类名]" + executionClass.getName() + "[方法名]" + executionMethod.getName()); sysLog.setVisitTime(startTime); // 调用Service,调用dao将sysLog insert数据库 sysLogService.save(sysLog); } } } } }
在切面类中咱们须要获取登陆用户的username,还须要获取ip地址,咱们怎么处理?
username获取
SecurityContextHolder获取
ip地址获取
ip地址的获取咱们能够经过request.getRemoteAddr()方法获取到。
在Spring中能够经过RequestContextListener来获取request或session对象。
@RequestMapping("/sysLog") @Controller public class SysLogController { @Autowired private ISysLogService sysLogService; @RequestMapping("/findAll.do") public ModelAndView findAll() throws Exception { ModelAndView mv = new ModelAndView(); List<SysLog> sysLogs = sysLogService.findAll(); mv.addObject("sysLogs", sysLogs); mv.setViewName("syslog-list"); return mv; } }
@Service @Transactional public class SysLogServiceImpl implements ISysLogService { @Autowired private ISysLogDao sysLogDao; @Override public void save(SysLog log) throws Exception { sysLogDao.save(log); } @Override public List<SysLog> findAll() throws Exception { return sysLogDao.findAll(); } }
public interface ISysLogDao { @Select("select * from syslog") @Results({ @Result(id=true,column="id",property="id"), @Result(column="visitTime",property="visitTime"), @Result(column="ip",property="ip"), @Result(column="url",property="url"), @Result(column="executionTime",property="executionTime"), @Result(column="method",property="method"), @Result(column="username",property="username") }) public List<SysLog> findAll() throws Exception; @Insert("insert into syslog(visitTime,username,ip,url,executionTime,method) values(#{visitTime},#{username},#{ip},#{url},#{executionTime},#{method})") public void save(SysLog log) throws Exception; }