溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

發(fā)布時(shí)間:2020-08-28 20:02:38 來(lái)源:腳本之家 閱讀:150 作者:Oven5217 欄目:編程語(yǔ)言

從接觸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ù)源。最終找到了方法:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

注意圖中箭頭指向的兩行,構(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ù)中的配置信息如下:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

這樣以后,application.properties中就不用再寫很多的配置信息,而且,如果將這些配置信息放到數(shù)據(jù)庫(kù)中之后,如果起多個(gè)應(yīng)用可是公用這一張表,這樣也可以做到配置信息的公用的效果,這樣修改以后,配置文件中就只有數(shù)據(jù)源的信息了:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

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)即可,例:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

說(shuō)到這里,返回給前臺(tái)的狀態(tài)碼,建議也是封裝成一個(gè)枚舉類型,不建議直接返回200、400之類的,不方便維護(hù)也不方便查詢。那么BaseController里做了什么呢?如下:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

定義一個(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ì)象的代碼也不用寫了:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

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):

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

這樣在進(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)行緩存的操作:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

代碼中使用了double check的騷操作,防止高并發(fā)下緩存失效的問(wèn)題(雖然我的示例不可能有高并發(fā),哈哈)。另外就是緩存更新的問(wèn)題,網(wǎng)上說(shuō)的有很多,先更新數(shù)據(jù)再更新緩存,先更新緩存再更新數(shù)據(jù)庫(kù)等等,具體要看你是做什么,本示例中沒(méi)有什么需要特殊注意的地方,因此就先更新數(shù)據(jù)庫(kù),然后再移除緩存:

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

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中,安不安全就要你自己考慮了。

基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟

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í)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI