此項目使用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; }