您好,登錄后才能下訂單哦!
從接觸springboot開(kāi)始,便深深的被它的簡(jiǎn)潔性深深的折服了,精簡(jiǎn)的配置,方便的集成,使我再也不想用傳統(tǒng)的ssm框架來(lái)搭建項(xiàng)目,一大堆的配置文件,維護(hù)起來(lái)很不方便,集成的時(shí)候也要費(fèi)力不少。從第一次使用springboot開(kāi)始,一個(gè)簡(jiǎn)單的main方法,甚至一個(gè)配置文件也不需要(當(dāng)然我是指的沒(méi)有任何數(shù)據(jù)交互,沒(méi)有任何組件集成的情況),就可以把一個(gè)web項(xiàng)目啟動(dòng)起來(lái),下面總結(jié)一下自從使用springboot依賴,慢慢完善的自己的一個(gè)web系統(tǒng)的架構(gòu),肯定不是最好的,但平時(shí)自己用著很舒服。
1. 配置信息放到數(shù)據(jù)庫(kù)里邊
個(gè)人比較不喜歡配置文件,因此有一個(gè)原則,配置文件能不用就不用,配置信息能少些就少些,配置內(nèi)容能用代碼寫堅(jiān)決不用xml,因此我第一個(gè)想到的就是,能不能把springboot的配置信息寫到數(shù)據(jù)庫(kù)里,在springboot啟動(dòng)的時(shí)候自動(dòng)去加載,而在application.properties里邊只寫一個(gè)數(shù)據(jù)源。最終找到了方法:
注意圖中箭頭指向的兩行,構(gòu)造了一個(gè)properties對(duì)象,然后將這個(gè)對(duì)象放到了springboot的啟動(dòng)對(duì)象application中,properties是一個(gè)類似map的key-value容器,springboot可以將其中的東西當(dāng)做成原來(lái)application.properties中的內(nèi)容一樣,因此在properties對(duì)象的內(nèi)容也就相當(dāng)于寫在了application.properties文件中。知道了這個(gè)之后就簡(jiǎn)單了,我們將原本需要寫在application.properties中的所有配置信息寫在數(shù)據(jù)庫(kù)中,在springboot啟動(dòng)的時(shí)候從數(shù)據(jù)庫(kù)中讀取出來(lái)放到properties對(duì)象中,然后再將這個(gè)對(duì)象set到application中即可。上圖中PropertyConfig.loadProperties()方法就是進(jìn)行了這樣的操作,代碼如下:
PropertyConfig.java
public class PropertyConfig { /** * 生成Properties對(duì)象 */ public static Properties loadProperties() { Properties properties = new Properties(); loadPropertiesFromDb(properties); return properties; } /** * 從數(shù)據(jù)庫(kù)中加載配置信息 */ private static void loadPropertiesFromDb(Properties properties) { InputStream in = PropertyConfig.class.getClassLoader().getResourceAsStream("application.properties"); try { properties.load(in); } catch (Exception e) { e.printStackTrace(); } String profile = properties.getProperty("profile"); String driverClassName = properties.getProperty("spring.datasource.driver-class-name"); String url = properties.getProperty("spring.datasource.url"); String userName = properties.getProperty("spring.datasource.username"); String password = properties.getProperty("spring.datasource.password"); Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { Class.forName(driverClassName); String tableName = "t_config_dev"; if ("pro".equals(profile)) { tableName = "t_config_pro"; } String sql = "select * from " + tableName; conn = DriverManager.getConnection(url, userName, password); pstmt = conn.prepareStatement(sql); rs = pstmt.executeQuery(); while (rs.next()) { String key = rs.getString("key"); String value = rs.getString("value"); properties.put(key, value); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (conn != null) { conn.close(); } if (pstmt != null) { pstmt.close(); } if (rs != null) { rs.close(); } } catch (Exception e) { e.printStackTrace(); } } } }
代碼中,首先使用古老的jdbc技術(shù),讀取數(shù)據(jù)庫(kù)t_config表,將表中的key-value加載到properties中,代碼中profile是為了區(qū)分開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境,以便于確定從那張表中加載配置文件,數(shù)據(jù)庫(kù)中的配置信息如下:
這樣以后,application.properties中就不用再寫很多的配置信息,而且,如果將這些配置信息放到數(shù)據(jù)庫(kù)中之后,如果起多個(gè)應(yīng)用可是公用這一張表,這樣也可以做到配置信息的公用的效果,這樣修改以后,配置文件中就只有數(shù)據(jù)源的信息了:
profile代表使用哪個(gè)環(huán)境,代碼中可以根據(jù)這個(gè)信息來(lái)從開(kāi)發(fā)表中加載配置信息還是從生產(chǎn)表中加載配置信息。
2. 統(tǒng)一返回結(jié)果
一般web項(xiàng)目中,大多數(shù)都是接口,以返回json數(shù)據(jù)為主,因此統(tǒng)一一個(gè)返回格式很必要。在本示例中,建了一個(gè)BaseController,所有的Controller都需要繼承這個(gè)類,在這個(gè)BaseController中定義了成功的返回和失敗的返回,在其他業(yè)務(wù)的Controller中,返回的時(shí)候,只需要return super.success(xxx)或者return super.fail(xxx, xxx)即可,例:
說(shuō)到這里,返回給前臺(tái)的狀態(tài)碼,建議也是封裝成一個(gè)枚舉類型,不建議直接返回200、400之類的,不方便維護(hù)也不方便查詢。那么BaseController里做了什么呢?如下:
定義一個(gè)ResultInfo類,該類只有兩個(gè)屬性,一個(gè)是Integer類型的狀態(tài)碼,一個(gè)是泛型,用于成功時(shí)返回給前臺(tái)的數(shù)據(jù),和失敗時(shí)返回給前臺(tái)的提示信息。
3. 統(tǒng)一異常捕獲
在上一步中的Controller代碼中看到拋出了一個(gè)自定義的異常,在Controller中,屬于最外層的代碼了,這個(gè)時(shí)候如果有異常就不能直接拋出去了,這里再拋出去就沒(méi)有人處理了,服務(wù)器只能返回給前臺(tái)一個(gè)錯(cuò)誤,用戶體驗(yàn)不好。因此,建議所有的Controller代碼都用try-catch包裹,捕獲到異常后統(tǒng)一進(jìn)行處理,然后再給前臺(tái)一個(gè)合理的提示信息。在上一步中拋出了一個(gè)自定義異常:
throw new MyException(ResultEnum.DELETE_ERROR.getCode(), "刪除員工出錯(cuò),請(qǐng)聯(lián)系網(wǎng)站管理人員。", e);
該自定義異常有三個(gè)屬性,分別是異常狀態(tài)碼,異常提示信息,以及捕獲到的異常對(duì)象,接下來(lái)定義一個(gè)全局的異常捕獲,統(tǒng)一對(duì)異常進(jìn)行處理:
@Slf4j @ResponseBody @ControllerAdvice public class GlobalExceptionHandle { /** * 處理捕獲的異常 */ @ExceptionHandler(value = Exception.class) public Object handleException(Exception e, HttpServletRequest request, HttpServletResponse resp) throws IOException { log.error(AppConst.ERROR_LOG_PREFIX + "請(qǐng)求地址:" + request.getRequestURL().toString()); log.error(AppConst.ERROR_LOG_PREFIX + "請(qǐng)求方法:" + request.getMethod()); log.error(AppConst.ERROR_LOG_PREFIX + "請(qǐng)求者IP:" + request.getRemoteAddr()); log.error(AppConst.ERROR_LOG_PREFIX + "請(qǐng)求參數(shù):" + ParametersUtils.getParameters(request)); if (e instanceof MyException) { MyException myException = (MyException) e; log.error(AppConst.ERROR_LOG_PREFIX + myException.getMsg(), myException.getE()); if (myException.getCode().equals(ResultEnum.SEARCH_PAGE_ERROR.getCode())) { JSONObject result = new JSONObject(); result.put("code", myException.getCode()); result.put("msg", myException.getMsg()); return result; } else if (myException.getCode().equals(ResultEnum.ERROR_PAGE.getCode())) { resp.sendRedirect("/err"); return ""; } else { return new ResultInfo<>(myException.getCode(), myException.getMsg()); } } else if (e instanceof UnauthorizedException) { resp.sendRedirect("/noauth"); return ""; } else { log.error(AppConst.ERROR_LOG_PREFIX + "錯(cuò)誤信息:", e); } resp.sendRedirect("/err"); return ""; } }
統(tǒng)一捕獲異常之后,可以進(jìn)行相應(yīng)的處理,我這里沒(méi)有進(jìn)行特殊的處理,只是進(jìn)行了一下區(qū)分,獲取數(shù)據(jù)的接口拋出的異常,前臺(tái)肯定是使用的ajax請(qǐng)求,因此返回前臺(tái)一個(gè)json格式的信息,提示出錯(cuò)誤內(nèi)容。如果是跳轉(zhuǎn)頁(yè)面拋出的異常,類似404之類的,直接跳轉(zhuǎn)到自定義的404頁(yè)面。補(bǔ)充一點(diǎn),springboot項(xiàng)目默認(rèn)是有/error路由的,返回的就是error頁(yè)面,所以,如果你在你的項(xiàng)目中定義一個(gè)error.html的頁(yè)面,如果報(bào)404錯(cuò)誤,會(huì)自動(dòng)跳轉(zhuǎn)到該頁(yè)面。
補(bǔ)充,統(tǒng)一異常處理類中使用了一個(gè)注解@Slf4j,該注解是lombok包中的,項(xiàng)目中加入了該依賴后,再也不用寫繁瑣的get、set等代碼,當(dāng)然類似的像上邊的聲明log對(duì)象的代碼也不用寫了:
4. 日志配置文件區(qū)分環(huán)境
本示例使用的是logback日志框架。需要在resources目錄中添加logback.xml配置文件,這是一個(gè)比較頭疼的地方,我本來(lái)想一個(gè)配置文件也沒(méi)有的,奈何我也不知道怎么將這個(gè)日志的配置文件放到數(shù)據(jù)庫(kù)中,所以暫時(shí)先這么著了,好在幾乎沒(méi)有需要改動(dòng)它的時(shí)候。
我在項(xiàng)目中添加了兩個(gè)日志的配置文件,分別是logback-dev.xml和logback-pro.xml可以根據(jù)不同的環(huán)境決定使用哪個(gè)配置文件,在數(shù)據(jù)庫(kù)配置表中(相當(dāng)于寫在了application.properties中)添加一條配置logging.config=classpath:logback-dev.xml來(lái)區(qū)分使用哪個(gè)文件作為日志的配置文件,配置文件內(nèi)容如下:
logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_HOME" value="/Users/oven/log/demo"/> <!-- INFO日志定義 --> <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_HOME}/demo.info.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/demo.info.%d{yyyy-MM-dd}.log</FileNamePattern> <maxHistory>180</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- ERROR日志定義 --> <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_HOME}/demo.error.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/demo.error.%d{yyyy-MM-dd}.log</FileNamePattern> <maxHistory>180</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- DEBUG日志定義 --> <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_HOME}/demo.debug.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/demo.debug.%d{yyyy-MM-dd}.log</FileNamePattern> <maxHistory>180</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 定義控制臺(tái)日志信息 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT"/> </root> <logger name="com.oven.controller" level="ERROR"> <appender-ref ref="ERROR"/> </logger> <logger name="com.oven.exception" level="ERROR"> <appender-ref ref="ERROR"/> </logger> <logger name="com.oven.mapper" level="DEBUG"> <appender-ref ref="DEBUG"/> </logger> <logger name="com.oven.aop" level="INFO"> <appender-ref ref="INFO"/> </logger> </configuration>
在配置文件中,定義了三個(gè)級(jí)別的日志,info、debug和error分別輸出到三個(gè)文件中,便于查看。在生成日志文件的時(shí)候,進(jìn)行了按照日志進(jìn)行拆分的配置,每一個(gè)級(jí)別的日志每一天都會(huì)重新生成一個(gè),根據(jù)日期進(jìn)行命名,超過(guò)180天的日志將自動(dòng)會(huì)刪除。當(dāng)然你還可以按照日志大小進(jìn)行拆分,我這里沒(méi)有進(jìn)行這項(xiàng)的配置。
5. 全局接口請(qǐng)求記錄
進(jìn)行全局的接口請(qǐng)求記錄,可以記錄接口的別調(diào)用情況,然后進(jìn)行一些統(tǒng)計(jì)和分析,在本示例中,只是將全局的接口調(diào)用情況記錄到了info日志中,沒(méi)有進(jìn)行相應(yīng)的分析操作:
@Slf4j @Aspect @Component public class WebLogAspect { @Pointcut("execution(public * com.oven.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore() { // 獲取請(qǐng)求 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); @SuppressWarnings("ConstantConditions") HttpServletRequest request = attributes.getRequest(); // 記錄請(qǐng)求內(nèi)容 log.info(AppConst.INFO_LOG_PREFIX + "請(qǐng)求地址:" + request.getRequestURL().toString()); log.info(AppConst.INFO_LOG_PREFIX + "請(qǐng)求方法:" + request.getMethod()); log.info(AppConst.INFO_LOG_PREFIX + "請(qǐng)求者IP:" + request.getRemoteAddr()); log.info(AppConst.INFO_LOG_PREFIX + "請(qǐng)求參數(shù):" + ParametersUtils.getParameters(request)); } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) { // 請(qǐng)求返回的內(nèi)容 if (ret instanceof ResultInfo) { log.info(AppConst.INFO_LOG_PREFIX + "返回結(jié)果:" + ((ResultInfo) ret).getCode().toString()); } } }
6. 集成shiro實(shí)現(xiàn)權(quán)限校驗(yàn)
集成shirl,輕松的實(shí)現(xiàn)了權(quán)限的管理,如果對(duì)shiro不熟悉朋友,還需要先把shiro入門一下才好,shiro的集成一般都需要自定義一個(gè)realm,來(lái)進(jìn)行身份認(rèn)證和授權(quán),因此先來(lái)一個(gè)自定義realm:
MyShiroRealm.java
public class MyShiroRealm extends AuthorizingRealm { @Resource private MenuService menuService; @Resource private UserService userService; /** * 授權(quán) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principals.getPrimaryPrincipal(); List<String> permissions = menuService.getAllMenuCodeByUserId(user.getId()); authorizationInfo.addStringPermissions(permissions); return authorizationInfo; } /** * 身份認(rèn)證 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String userName = String.valueOf(token.getUsername()); // 從數(shù)據(jù)庫(kù)獲取對(duì)應(yīng)用戶名的用戶 User user = userService.getByUserName(userName); // 賬號(hào)不存在 if (user == null) { throw new UnknownAccountException(ResultEnum.NO_THIS_USER.getValue()); } Md5Hash md5 = new Md5Hash(token.getPassword(), AppConst.MD5_SALT, 2); // 密碼錯(cuò)誤 if (!md5.toString().equals(user.getPassword())) { throw new IncorrectCredentialsException(ResultEnum.PASSWORD_WRONG.getValue()); } // 賬號(hào)鎖定 if (user.getStatus().equals(1)) { throw new LockedAccountException(ResultEnum.USER_DISABLE.getValue()); } ByteSource salt = ByteSource.Util.bytes(AppConst.MD5_SALT); return new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName()); } }
自定義完realm后需要一個(gè)配置文件但自定義的realm配置到shiro里:
ShiroConfig.java
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/font/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/*.js", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/doLogin", "anon"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/"); shiroFilterFactoryBean.setUnauthorizedUrl("/noauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 憑證匹配器 */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5"); hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } /** * 開(kāi)啟shiro aop注解 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError"); mappings.setProperty("UnauthorizedException", "403"); r.setExceptionMappings(mappings); r.setDefaultErrorView("error"); r.setExceptionAttribute("ex"); return r; } }
身份認(rèn)證如果簡(jiǎn)單的理解的話,你可以理解為登錄的過(guò)程。授權(quán)就是授予你權(quán)利,代表你在這個(gè)系統(tǒng)中有權(quán)限做什么動(dòng)作,具體shiro的內(nèi)容小伙伴們自行去學(xué)習(xí)吧。
7. 登錄校驗(yàn),安全攔截
在集成了shiro之后,登錄操作就需要使用到自定義的realm了,具體的登錄代碼如下:
/** * 登錄操作 * * @param userName 用戶名 * @param pwd 密碼 */ @RequestMapping("/doLogin") @ResponseBody public Object doLogin(String userName, String pwd, HttpServletRequest req) throws MyException { try { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userName, pwd); subject.login(token); User userInDb = userService.getByUserName(userName); // 登錄成功后放入application,防止同一個(gè)賬戶多人登錄 ServletContext application = req.getServletContext(); @SuppressWarnings("unchecked") Map<String, String> loginedMap = (Map<String, String>) application.getAttribute(AppConst.LOGINEDUSERS); if (loginedMap == null) { loginedMap = new HashMap<>(); application.setAttribute(AppConst.LOGINEDUSERS, loginedMap); } loginedMap.put(userInDb.getUserName(), req.getSession().getId()); // 登錄成功后放入session中 req.getSession().setAttribute(AppConst.CURRENT_USER, userInDb); logService.addLog("登錄系統(tǒng)!", "成功!", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req)); return super.success("登錄成功!"); } catch (Exception e) { User userInDb = userService.getByUserName(userName); if (e instanceof UnknownAccountException) { logService.addLog("登錄系統(tǒng)!", "失敗[" + ResultEnum.NO_THIS_USER.getValue() + "]", 0, "", IPUtils.getClientIPAddr(req)); return super.fail(ResultEnum.NO_THIS_USER.getCode(), ResultEnum.NO_THIS_USER.getValue()); } else if (e instanceof IncorrectCredentialsException) { logService.addLog("登錄系統(tǒng)!", "失敗[" + ResultEnum.PASSWORD_WRONG.getValue() + "]", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req)); return super.fail(ResultEnum.PASSWORD_WRONG.getCode(), ResultEnum.PASSWORD_WRONG.getValue()); } else if (e instanceof LockedAccountException) { logService.addLog("登錄系統(tǒng)!", "失敗[" + ResultEnum.USER_DISABLE.getValue() + "]", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req)); return super.fail(ResultEnum.USER_DISABLE.getCode(), ResultEnum.USER_DISABLE.getValue()); } else { throw new MyException(ResultEnum.UNKNOW_ERROR.getCode(), "登錄操作出錯(cuò),請(qǐng)聯(lián)系網(wǎng)站管理人員。", e); } } }
身份認(rèn)證的操作交給了shiro,利用用戶名和密碼構(gòu)造一個(gè)身份的令牌,調(diào)用shiro的login方法,這個(gè)時(shí)候就會(huì)進(jìn)入自定義reaml的身份認(rèn)證方法中,也就是上一步中的doGetAuthenticationInfo方法,具體的認(rèn)證操作看上一步的代碼,無(wú)非就是賬號(hào)密碼的校驗(yàn)等。身份認(rèn)證的時(shí)候,通過(guò)拋出異常的方式給登錄操作返回信息,從而在登錄方法中判斷身份認(rèn)證失敗后的信息,從而返回給前臺(tái)進(jìn)行提示。
在身份認(rèn)證通過(guò)后,拿到當(dāng)前登錄用戶的信息,首先放到session中,便于后續(xù)的使用。其次在放到application對(duì)象中,防止同一個(gè)賬號(hào)的多次登錄。
有了身份任何和授權(quán)自然就少不了安全校驗(yàn),在本示例中使用了一個(gè)攔截器來(lái)實(shí)現(xiàn)安全校驗(yàn)的工作:
SecurityInterceptor.java
@Component public class SecurityInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { resp.setContentType("text/plain;charset=UTF-8"); String servletPath = req.getServletPath(); // 放行的請(qǐng)求 if (servletPath.startsWith("/login") || servletPath.startsWith("/doLogin") || servletPath.equals("/err")) { return true; } if (servletPath.startsWith("/error")) { resp.sendRedirect("/err"); return true; } // 獲取當(dāng)前登錄用戶 User user = (User) req.getSession().getAttribute(AppConst.CURRENT_USER); // 沒(méi)有登錄狀態(tài)下訪問(wèn)系統(tǒng)主頁(yè)面,都跳轉(zhuǎn)到登錄頁(yè),不提示任何信息 if (servletPath.startsWith("/")) { if (user == null) { resp.sendRedirect(getDomain(req) + "/login"); return false; } } // 未登錄或會(huì)話超時(shí) if (user == null) { String requestType = req.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { // ajax請(qǐng)求 ResultInfo<Object> resultInfo = new ResultInfo<>(); resultInfo.setCode(ResultEnum.SESSION_TIMEOUT.getCode()); resultInfo.setData(ResultEnum.SESSION_TIMEOUT.getValue()); resp.getWriter().write(JSONObject.toJSONString(resultInfo)); return false; } String param = URLEncoder.encode(ResultEnum.SESSION_TIMEOUT.getValue(), "UTF-8"); resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param); return false; } // 檢查是否被其他人擠出去 ServletContext application = req.getServletContext(); @SuppressWarnings("unchecked") Map<String, String> loginedMap = (Map<String, String>) application.getAttribute(AppConst.LOGINEDUSERS); if (loginedMap == null) { // 可能是掉線了 String requestType = req.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { // ajax請(qǐng)求 ResultInfo<Object> resultInfo = new ResultInfo<>(); resultInfo.setCode(ResultEnum.LOSE_LOGIN.getCode()); resultInfo.setData(ResultEnum.LOSE_LOGIN.getValue()); resp.getWriter().write(JSONObject.toJSONString(resultInfo)); return false; } String param = URLEncoder.encode(ResultEnum.LOSE_LOGIN.getValue(), "UTF-8"); resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param); return false; } String loginedUserSessionId = loginedMap.get(user.getUserName()); String mySessionId = req.getSession().getId(); if (!mySessionId.equals(loginedUserSessionId)) { String requestType = req.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { // ajax請(qǐng)求 ResultInfo<Object> resultInfo = new ResultInfo<>(); resultInfo.setCode(ResultEnum.OTHER_LOGINED.getCode()); resultInfo.setData(ResultEnum.OTHER_LOGINED.getValue()); resp.getWriter().write(JSONObject.toJSONString(resultInfo)); return false; } String param = URLEncoder.encode(ResultEnum.OTHER_LOGINED.getValue(), "UTF-8"); resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param); return false; } return true; } /** * 獲得域名 */ private String getDomain(HttpServletRequest request) { String path = request.getContextPath(); return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path; } }
在攔截器中,首先對(duì)一些不需要校驗(yàn)的請(qǐng)求進(jìn)行放行,例如登錄動(dòng)作、登錄頁(yè)面請(qǐng)求以及錯(cuò)誤頁(yè)面等。然后獲取當(dāng)前登錄的用戶,如果沒(méi)有登錄則自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面。在返回前臺(tái)的時(shí)候,判斷請(qǐng)求屬于同步請(qǐng)求還是異步請(qǐng)求,如果是同步請(qǐng)求,直接進(jìn)行頁(yè)面的跳轉(zhuǎn),跳轉(zhuǎn)到登錄頁(yè)面。如果是異步請(qǐng)求,則返回前臺(tái)一個(gè)json數(shù)據(jù),提示前臺(tái)登錄信息失效。這里補(bǔ)充一點(diǎn),前臺(tái)可以使用ajaxhook進(jìn)行異步請(qǐng)求的捕獲,相當(dāng)于一個(gè)前端的全局?jǐn)r截器,攔截所有的異步請(qǐng)求,可以監(jiān)視所有異步請(qǐng)求的返回結(jié)果,如果返回的是登錄失效,則進(jìn)行跳轉(zhuǎn)到登錄頁(yè)面的操作。具體ajaxhook的使用方法請(qǐng)自行學(xué)習(xí),本示例中暫時(shí)沒(méi)有使用。
下面是判斷同一個(gè)賬號(hào)有沒(méi)有多次登錄,具體方法就是使用當(dāng)前的sessionId,將當(dāng)前登錄用戶和請(qǐng)求sissionId作為一個(gè)key-value放到了application中,如果該用戶的sessionId發(fā)生了變化,說(shuō)明又有一個(gè)人登錄了該賬號(hào),然后就進(jìn)行相應(yīng)的提示操作。
8. 配置虛擬路徑
web項(xiàng)目中免不了并上傳的操作,圖片或者文件,如果上傳的是圖片,一般還要進(jìn)行回顯的操作,我們不想將上傳的文件直接存放在項(xiàng)目的目錄中,而是放在一個(gè)自定義的目錄,同時(shí)項(xiàng)目還可以訪問(wèn):
這樣在進(jìn)行上傳操作的時(shí)候,就可以將上傳的文件放到項(xiàng)目以外的目錄中,然后外部訪問(wèn)的時(shí)候,通過(guò)虛擬路徑進(jìn)行映射訪問(wèn)。
9. 集成redis緩存
springboot的強(qiáng)悍就是集成一個(gè)東西太方便了,如果你不想做任何配置,只需要加入redis的依賴,然后在配置文件(本示例中配置是在數(shù)據(jù)庫(kù)中)中添加redis的鏈接信息,就可以在項(xiàng)目中使用redis了。
本示例中使用redis做緩存,首先寫了一個(gè)緩存的類,代碼有些長(zhǎng)不做展示。然后在service層進(jìn)行緩存的操作:
代碼中使用了double check的騷操作,防止高并發(fā)下緩存失效的問(wèn)題(雖然我的示例不可能有高并發(fā),哈哈)。另外就是緩存更新的問(wèn)題,網(wǎng)上說(shuō)的有很多,先更新數(shù)據(jù)再更新緩存,先更新緩存再更新數(shù)據(jù)庫(kù)等等,具體要看你是做什么,本示例中沒(méi)有什么需要特殊注意的地方,因此就先更新數(shù)據(jù)庫(kù),然后再移除緩存:
10. 項(xiàng)目代碼和依賴以及靜態(tài)資源分別打包
之前遇到一個(gè)問(wèn)題,springboot打包之后是一個(gè)jar文件,如果將所有依賴也打到這個(gè)jar包中的話,那么這個(gè)jar包動(dòng)輒幾十兆,來(lái)回傳輸不說(shuō),如果想改動(dòng)其中的一個(gè)配置內(nèi)容,還異常的繁瑣,因此,將項(xiàng)目代碼,就是自己寫的代碼打成一個(gè)jar包(一般只有幾百k),然后將所有的依賴打包到一個(gè)lib目錄,然后再將所有的配置信息以及靜態(tài)文件打包到resources目錄,這樣,靜態(tài)文件可以直接進(jìn)行修改,瀏覽器清理緩存刷新即可出現(xiàn)改動(dòng)效果,而且打包出來(lái)的項(xiàng)目代碼也小了很多,至于依賴,一般都是不變的,所以也沒(méi)必要每次都打包它。具體操作就是在pom.xml中增加一個(gè)插件即可,代碼如下:
代碼太長(zhǎng),不做展示
11. 項(xiàng)目啟動(dòng)
到現(xiàn)在都沒(méi)有貼一個(gè)項(xiàng)目的目錄結(jié)構(gòu),先來(lái)一張。目錄中項(xiàng)目跟目錄下的demo.sh就是啟動(dòng)腳本,當(dāng)時(shí)從網(wǎng)上抄襲改裝過(guò)來(lái)的,源代碼出自那位大師之手我就不知道了,先行謝過(guò)。在部署到服務(wù)器的時(shí)候,如果服務(wù)器上安裝好了jdk、maven、git,每次修改完代碼,直接git pull下來(lái),然后mvn package打包,然后直接./demo.sh start就可以啟動(dòng)項(xiàng)目,方便快速。慢著,忘記了,如果你提交到github中的application.properties中的數(shù)據(jù)源配置信息是開(kāi)發(fā)環(huán)境的話,那么你在打包之后,target/resources中的application.properties中的數(shù)據(jù)源需要改成開(kāi)發(fā)環(huán)境才可以啟動(dòng)。當(dāng)然如果你嫌麻煩,可以直接將開(kāi)發(fā)環(huán)境的數(shù)據(jù)源配置push到github中,安不安全就要你自己考慮了。
12. 總結(jié)
示例中可能還有一些細(xì)節(jié)沒(méi)有說(shuō)到,總之這個(gè)項(xiàng)目是慢慢的添磚添瓦弄出來(lái)的,自己在寫很多其他的項(xiàng)目的時(shí)候,都是以此項(xiàng)目為模板進(jìn)行改造出來(lái)的,個(gè)人感覺(jué)很實(shí)用很方便,用著也很舒服。github地址:https://github.com/503612012/demo歡迎收藏。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。