SSO是公司一個已經存在了若干年的項目,後端採用SpringMVC、MyBatis,數據庫使用MySQL,前端展現使用Freemark。今年,咱們對該項目進行了一次革命性的改進,改形成SpringCloud架構,而且把先後端分離,前端採用Vue框架。php
SpringCloud的核心是SpringBoot,相比較於傳統的Spring,SpringCloud具備如下優勢:css
SSO項目改造前充斥着大量的配置文件,主要包含如下這些部分:前端
本文着重介紹如下幾個部分:java
SpringMVC中,若是mvc:interceptors
配置的URL規則以下,則不會攔截靜態資源。mysql
<mvc:mapping path="/*.do" />
複製代碼
可是若是配置的是:nginx
<mvc:mapping path="/**" />
複製代碼
方案1: 在web.xml中配置<servlet-name>default</servlet-name>
,用defaultServlet
先處理請求如:git
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.png</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.gif</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.ico</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.gif</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
複製代碼
方案2:使用<mvc:resources />
標籤聲明靜態資源路徑github
<mvc:resources mapping="/resources/js/**" location="/js/" />
<mvc:resources mapping="/resources/images/**" location="/images/" />
<mvc:resources mapping="/resources/css/**" location="/css/" />
複製代碼
方案3:使用mvc:default-servlet-handler/
標籤web
SpringBoot解決方案:繼承WebMvcConfigurerAdapter實現addResourceHandlers方法。redis
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/resource/")//sso靜態資源
.addResourceLocations("classpath:/META-INF/resources/")//swagger靜態資源
.setCachePeriod(0);//0表示不緩存
}
複製代碼
sso靜態資源文件路徑如圖:
SpringMVC配置文件內容:
攔截任何請求而且初始化參數,有些請求是不須要攔截的,有的請求登陸後不須要通過權限校驗直接放行。
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean class="自定義攔截器PermissionInterceptor">
<!-- 未登陸便可訪問的地址 -->
<property name="excludeUrls">
<list><value>請求地址<value></list>
</property>
<!-- 只要登陸了就不須要攔截的資源 -->
<property name="LogInExcludeUrls">
<list><value>請求地址<value></list>
</property>
</bean>
</mvc:interceptor>
</mvc:interceptors>
複製代碼
SpringBoot中添加攔截器只需繼承WebMvcConfigurerAdapter,並重寫addInterceptors方法便可。
/*** 攔截器 * @param registry */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(permissionInterceptor).
addPathPatterns("/**");
super.addInterceptors(registry);
}
複製代碼
自定義的攔截器須要初始化一些參數,所以須要在註冊攔截器以前註冊,這裏咱們設置爲懶加載。免登陸攔截的路徑,以及登陸後不須要判斷權限的路徑都寫在yml文件了,經過系統環境變量Environment獲取值。
@Autowired
@Lazy
private PermissionInterceptor permissionInterceptor;
@Autowired
private Environment environment;
/** * */
@Bean
public PermissionInterceptor permissionInterceptor() {
PermissionInterceptor permissionInterceptor = new PermissionInterceptor();
List<String> excludeUrls = Arrays.asList(environment.getProperty("intercept.exclude.path").split(","));
List<String> commonUrls = Arrays.asList(environment.getProperty("intercept.login.exclude.path").split(","));
permissionInterceptor.setCommonUrls(commonUrls);
permissionInterceptor.setExcludeUrls(excludeUrls);
return permissionInterceptor;
}
複製代碼
A、數據源配置
數據源注入的三種狀況:
【狀況一】
【狀況二】
【狀況三】
pom.xml依賴:
<!-- 狀況1、二 測試引入的依賴 -->
<!--<dependency>-->
<!--<groupId>com.alibaba</groupId>-->
<!--<artifactId>druid</artifactId>-->
<!--<version>${druid.version}</version>-->
<!--</dependency>-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>RELEASE</version>
</dependency>
複製代碼
yml配置:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 當前數據源操做類型
driver-class-name: com.mysql.jdbc.Driver # mysql驅動包
url: jdbc:mysql://yourURL # 數據庫名稱
username: yourusername
password: yourpassword
druid:
initial-size: 5 # 初始化大小
min-idle: 5 # 最小
max-active: 20 # 最大
max-wait: 60000 # 鏈接超時時間
time-between-eviction-runs-millis: 60000 # 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒
min-evictable-idle-time-millis: 300000 # 指定一個空閒鏈接最少空閒多久後可被清除,單位是毫秒
validationQuery: select 'x'
test-while-idle: true # 當鏈接空閒時,是否執行鏈接測試
test-on-borrow: false # 當從鏈接池借用鏈接時,是否測試該鏈接
test-on-return: false # 在鏈接歸還到鏈接池時是否測試該鏈接
filters: config,wall,stat
複製代碼
B、MyBatis配置
經過引入mybatis-spring-boot-starter依賴,能夠簡單配置mybatis上手使用。
下面簡單分析mybatis-starter的源碼以及如何配置mybatis。
先看mybatis-spring-boot-starter中mybatis-spring-boot-autoconfigure的spring.factories文件
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
複製代碼
能夠看到自動注入類是MybatisAutoConfiguration,咱們從這個類入手分析能夠知道,必須先建立好了數據源後,纔會加載mybatis的sqlSessionFactory。
@EnableConfigurationProperties({MybatisProperties.class})註解指定了配置文件中 prefix = "mybatis" 那部分屬性有效,這部分屬性值將注入到已建立的SqlSessionFactoryBean中,最後生成SqlSessionFactory對象。
@Configuration
//當SqlSessionFactory,SqlSessionFactoryBean存在的狀況下加載當前Bean
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
//當指定數據源在容器中只有一個或者有多個可是隻指定首選數據源
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
//當數據源注入到Spring容器後纔開始加載當前Bean
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
private final MybatisProperties properties;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
//設置mybatis配置文件所在路徑
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource
(this.properties.getConfigLocation())); }
}
//設置其餘MyBatisProperties對象中有的屬性略....
return factory.getObject();
}
}
複製代碼
MybatisProperties含有的屬性:
@ConfigurationProperties(prefix = "mybatis" )
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
private String configLocation;
private String[] mapperLocations;
private String typeAliasesPackage;
private Class<?> typeAliasesSuperType;
private String typeHandlersPackage;
private boolean checkConfigLocation = false;
private ExecutorType executorType;
private Properties configurationProperties;
@NestedConfigurationProperty
private Configuration configuration;
}
複製代碼
C、使用mybatis
application.yml
mybatis:
config-location: classpath:mybatis.xml # mybatis配置文件所在路徑
type-aliases-package: com.creditease.permission.model # 全部Entity別名類所在包
mapper-locations: classpath:mybatis/**/*.xml
複製代碼
從上面的MybatisProperties能夠看出,mybatis能夠指定一些configuration,好比自定義攔截器pageHelper。
mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
<plugin interceptor="com.creditease.permission.manager.MybatisInterceptor"></plugin>
</plugins>
</configuration>
複製代碼
@MapperScan("com.creditease.permission.dao")//mapper類所在目錄
public class SsoApplication {
public static void main(String[] args) {
SpringApplication.run(SsoApplication.class, args);
}
}
複製代碼
Spring事務有兩種處理方式:
用TransactionTemplate或者直接使用底層的PlatformTransactionManager將事務代碼寫在業務代碼中。
優勢:能夠在代碼塊中處理事務,比較靈活。
缺點:對代碼具備侵入性。
採用@Transactional註解或者基於配置文件方式,在方法先後進行攔截。
優勢:非侵入性不會污染代碼。
缺點:事務只能在方法和類上控制,粒度較小。
A、使用@Transactional註解
非SpringBoot工程,須要在配置文件中加入配置:
<tx:annotation-driven/>
複製代碼
SpringBoot工程能夠用@EnableTransactionManagement註解代替上面的配置內容。
B、採用配置文件方式
以前的sso是基於配置的方式,配置代碼以下:
<aop:config>
<aop:pointcut expression="execution(public * com.creditease.permission.service.impl.*Impl.*(..))" id="pointcut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="query*" propagation="REQUIRED" read-only="true"/>
<tx:method name="find*" propagation="REQUIRED" read-only="true"/>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="modify*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
複製代碼
改造後的SpringBoot基於Java代碼:
@Aspect
@Configuration
public class TransactionAdviceConfig {
/** * 指定切入點 */
private static final String AOP_POINTCUT_EXPRESSION = "execution(public * com.creditease.permission.service.impl.*Impl.*(..))";
@Resource
DruidDataSource dataSource;
/** * 指定處理事務的PlatformTransactionManager * @return */
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource);
}
/** * 指定切入點處理邏輯,執行事務 * @return */
@Bean
public TransactionInterceptor txAdvice() {
DefaultTransactionAttribute txAttrRequired = new DefaultTransactionAttribute();
txAttrRequired.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
DefaultTransactionAttribute txAttrRequiredReadonly = new DefaultTransactionAttribute();
txAttrRequiredReadonly.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txAttrRequiredReadonly.setReadOnly(true);
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
source.addTransactionalMethod("query*", txAttrRequiredReadonly);
source.addTransactionalMethod("find*", txAttrRequiredReadonly);
source.addTransactionalMethod("save*", txAttrRequired);
source.addTransactionalMethod("delete*", txAttrRequired);
source.addTransactionalMethod("add*", txAttrRequired);
source.addTransactionalMethod("modify*", txAttrRequired);
return new TransactionInterceptor(transactionManager(), source);
}
/** * Advisor組裝配置,將Advice的代碼邏輯注入到Pointcut位置 * @return */
@Bean
public Advisor txAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
複製代碼
通常編碼時有異常咱們都會try-catch捕獲異常,有時爲了區分不一樣的異常還會一次catch多個異常,大量的try-catch語句,這樣使得代碼也不夠優雅;一個相同的異常處理寫屢次代碼也比較冗餘,因此引入全局的異常處理很是必要。
改造前的異常處理配置文件:
<!--定義異常處理頁面-->
<bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="com.creditease.permissionapi.exception.NopermissionException">/permission/noSecurity</prop>
</props>
</property>
</bean>
複製代碼
使用SimpleMappingExceptionResolver類處理異常,設置自定義異常類型NopermissionException,以及異常發生後的請求路徑/permission/noSecurity。
SpringBoot中採用@RestControllerAdvice或者@ControllerAdvice設置全局異常類。這二者區別相似於@Controller和@RestController註解。
SSO中定義了三種全局的異常處理:普通的Exception處理;自定的NopermissionException異常和參數校驗異常。
全局異常處理代碼以下:
@Configuration
@Slf4j
@RestControllerAdvice
public class GlobalExceptionConfig {
//無權限處理
@ExceptionHandler(value = {NopermissionException.class})
public void noPermissionExceptionHandler(HttpServletRequest request, Exception ex, HttpServletResponse response, @Value("${sso.server.prefix}") String domain) throws IOException {
printLog(request,ex);
response.sendRedirect("跳轉到無權限頁面地址");
}
//參數校驗處理
@ExceptionHandler(value = {BindException.class})
public ResultBody BindExceptionHandler(BindException bindException){
List<ObjectError> errors = bindException.getBindingResult().getAllErrors();
//這個ResultBody是一個返回結果對象,這裏須要返回json,裏面包含了狀態碼和提示信息
return ResultBody.buildFailureResult(errors.get(0).getDefaultMessage());
}
//全部未捕獲的異常處理邏輯
@ExceptionHandler(value = {Exception.class})
public ResultBody exceptionHandler(HttpServletRequest request,Exception ex){
printLog(request,ex);
return ResultBody.buildExceptionResult();
}
//將請求參數和異常打印出來,結合@slf4j註解
public void printLog(HttpServletRequest request,Exception ex){
String parameters = JsonHelper.toString(request.getParameterMap());
log.error("url>>>:{},params>>>:{} ,printLog>>>:{}",request.getRequestURL(),parameters,ex);
}
}
複製代碼
@RestControllerAdvice結合@Validation,能夠對Bean進行校驗,校驗不經過會拋出BindException異常。經過註解能夠少寫if-else代碼,判斷請求的接口參數是否爲空,提升代碼的美觀性。例如:
//常規作法
if(StringUtils.isEmpty(ssoSystem.getSysCode())
//SSO作法
//在Controller請求方法上添加@Valid註解
@RequestMapping(value = "/add", method = RequestMethod.POST)
public ResultBody add(@Valid @RequestBody SsoSystem ssoSystem) {
}
//在須要處理的SsoSystem Bean的屬性上加@NotNull註解
@NotNull(message = "系統編號不能爲空")
private String sysCode;
複製代碼
當sysCode傳入參數爲空時,就會拋出BindException被全局的異常處理類,捕獲處理返回json格式的參數:
{
"resultCode":2,
"resultMsg":"系統編號不能爲空",
"resultData":null
}
複製代碼
SpringBoot1.5默認使用內嵌tomcat8.5版本,而原來SpringMVC的SSO部署在tomcat7上。tomcat的升級對此次改造影響最明顯的就是cookie。 tomcat8後採用的cookie校驗協議是Rfc6265CookieProcessor。該協議要求domain的命名必須遵循如下規則:
因爲是兩個不一樣的應用,必然會有兩個不一樣的端口。不一樣的端口就會有跨域問題,SSO採用的方式是經過nginx區分來自先後端的請求,反向代理請求對應到不一樣的服務去。
swagger是後端接口展現的插件,經過修改攔截器代碼,mock登陸對象免登陸,直接訪問接口進行先後端的調試。在swagger插件上能夠看到具體接口請求路徑和參數、參數是否必須、返回值、接口統計信息等。
接口統計信息
請求參數和路徑
以前是經過SpringMvc的modeAndview方式跳轉的,如今作了兩種處理:
注意:老代碼跳轉採用的是經過SpringMvc在return的頁面路徑前加redirect的形式,如:return "redirect:index",這樣默認會在return的URL後加jessionID。
特別須要注意代碼中的相關校驗路徑的地方。好比在此次改造過程當中路徑修改會影響如下幾個方面。
做者: 黃玲峯
來源:宜信技術學院