SSO是公司一个已经存在了若干年的项目,后端采用SpringMVC、MyBatis,数据库使用MySQL,前端展示使用Freemark。今年,我们对该项目进行了一次革命性的改进,改造成SpringCloud架构,并且把前后端分离,前端采用Vue框架。
一、使用SpringCloud架构进行改造
1.1 为什么使用SpringCloud
SpringCloud的核心是SpringBoot,相比较于传统的Spring,SpringCloud具有以下优点:
- 部署简单,SpringBoot内置了Tomcat容器,可以将程序直接编译成一个jar,通过java-jar来运行。
- 编码简单,SpringBoot只需要在pom文件中添加一个starter-web依赖,即可帮助开发者快速启动一个web容器,非常方便。
- 配置简单,SpringBoot可以通过简单的注解方式来代替原先Spring非常复杂的xml方式。如果我想把一个普通的类交给Spring管理,只需要添加@Configuration和@Bean两个注解即可。
- 监控简单,我们可以引入spring-boot-start-actuator依赖,直接使用REST方式来获取进程的运行期性能参数,从而达到监控的目的。
1.2 一个常规项目都需要改造哪些部分
1.2.1 配置文件
SSO项目改造前充斥着大量的配置文件,主要包含以下这些部分:
- 静态资源相关
- 数据源
- mybatis配置
- redis配置
- 事务
- 拦截器拦截内容
- 监听器、过滤器
- 组件扫描路径配置
本文着重介绍以下几个部分:
1)静态资源处理
SpringMVC中,如果mvc:interceptors
配置的URL规则如下,则不会拦截静态资源。
1
|
<mvc:mapping path=
"/*.do"
/>
|
但是如果配置的是:
1
|
<mvc:mapping path=
"/**"
/>
|
方案1: 在web.xml中配置<servlet-name>default</servlet-name>
,用defaultServlet
先处理请求如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<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 />
标签声明静态资源路径
1
2
3
|
<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/
标签
SpringBoot解决方案:继承WebMvcConfigurerAdapter实现addResourceHandlers方法。
1
2
3
4
5
6
|
public
void
addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(
"/**"
)
.addResourceLocations(
"classpath:/resource/"
)
//sso静态资源
.addResourceLocations(
"classpath:/META-INF/resources/"
)
//swagger静态资源
.setCachePeriod(0);
//0表示不缓存
}
|
sso静态资源文件路径如图:
2)拦截器
SpringMVC配置文件内容:
拦截任何请求并且初始化参数,有些请求是不需要拦截的,有的请求登录后不需要经过权限校验直接放行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<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方法即可。
1
2
3
4
5
6
7
8
9
|
/*** 拦截器
* @param registry
*/
@Override
public
void
addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(permissionInterceptor).
addPathPatterns(
"/**"
);
super.addInterceptors(registry);
}
|
自定义的拦截器需要初始化一些参数,因此需要在注册拦截器之前注册,这里我们设置为懒加载。免登录拦截的路径,以及登录后不需要判断权限的路径都写在yml文件了,通过系统环境变量Environment获取值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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;
}
|
3)数据库和mybatis配置
A、数据源配置
数据源注入的三种情况:
【情况一】
- 条件:不引⼊druid-spring-boot-starter只依赖druid.jar,不指定spring.datasource.type。
- 结果:注入的数据源是tomcat的数据源。
- 解析:依赖的mybatis-spring-boot-starter工程依赖了tomcat的数据源,spring-boot-autoconfigure-starter的DataSourceAutoConfiguration自动注入类会在不指定数据源的情况下,判断路径中是否存在默认的4种数据源(Hikari,Tomcat,Dbcp,Dbcp2)的其一,如果有就注入。
【情况二】
- 条件:不引入druid-spring-boot-starter只依赖druid.jar ,指定spring.datasource.type为DruidDataSource。
- 结果:注入了DruidDataSource数据源,但配置文件中的druid配置不会生效。
- 解析: 指定了依赖的数据源后,spring自动注入的starter会将指定的数据源注入,yml指定了druid数据源。@ConfigurationProperties注解的DataSourceProperties没处理druid部分的性能参数属性,只处理了数据源部分的属性。
【情况三】
- 条件:引⼊ druid-spring-boot-starter 不依赖druid.jar,指定spring.datasource.type为DruidDataSource。
- 结果:注入了DruidDataSource数据源, 配置文件中的druid配置也会生效。
- 解析:druid-spring-boot-starter自动配置类会在DataSourceAutoConfiguration之前先创建数据源,并且@ConfigurationProperties注入的DataSourceProperties包含了配置文件中druid的属性。
pom.xml依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<!-- 情况一、二 测试引入的依赖 -->
<!--<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配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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文件
1
2
|
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
|
可以看到自动注入类是MybatisAutoConfiguration,我们从这个类入手分析可以知道,必须先创建好了数据源后,才会加载mybatis的sqlSessionFactory。
@EnableConfigurationProperties({MybatisProperties.class})注解指定了配置文件中 prefix = "mybatis" 那部分属性有效,这部分属性值将注入到已创建的SqlSessionFactoryBean中,最后生成SqlSessionFactory对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@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含有的属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@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
1
2
3
4
|
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
1
2
3
4
5
6
7
8
9
|
<?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注解
1
2
3
4
5
6
|
@MapperScan(
"com.creditease.permission.dao"
)
//mapper类所在目录
public
class
SsoApplication {
public
static
void
main(String[] args) {
SpringApplication.run(SsoApplication.
class
, args);
}
}
|
4)事务
Spring事务有两种处理方式:
- 编程式
用TransactionTemplate或者直接使用底层的PlatformTransactionManager将事务代码写在业务代码中。
优点:可以在代码块中处理事务,比较灵活。
缺点:对代码具有侵入性。
- 声明式
采用@Transactional注解或者基于配置文件方式,在方法前后进行拦截。
优点:非侵入性不会污染代码。
缺点:事务只能在方法和类上控制,粒度较小。
A、使用@Transactional注解
非SpringBoot工程,需要在配置文件中加入配置:
1
|
<tx:annotation-driven/>
|
SpringBoot工程可以用@EnableTransactionManagement注解代替上面的配置内容。
B、采用配置文件方式
之前的sso是基于配置的方式,配置代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<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代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
@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());
}
|
5)全局异常处理
一般编码时有异常我们都会try-catch捕获异常,有时为了区分不同的异常还会一次catch多个异常,大量的try-catch语句,这样使得代码也不够优雅;一个相同的异常处理写多次代码也比较冗余,所以引入全局的异常处理非常必要。
改造前的异常处理配置文件:
1
2
3
4
5
6
7
8
|
<!--定义异常处理页面-->
<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异常和参数校验异常。
全局异常处理代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@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代码,判断请求的接口参数是否为空,提高代码的美观性。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//常规做法
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格式的参数:
1
2
3
4
5
|
{
"resultCode"
:2,
"resultMsg"
:
"系统编号不能为空"
,
"resultData"
:
null
}
|
1.3 注意事项
1.3.1 内置tomcat版本太高引发的问题
SpringBoot1.5默认使用内嵌tomcat8.5版本,而原来SpringMVC的SSO部署在tomcat7上。tomcat的升级对这次改造影响最明显的就是cookie。 tomcat8后采用的cookie校验协议是Rfc6265CookieProcessor。该协议要求domain的命名必须遵循以下规则:
- 必须是1-9、a-z、A-Z、. 、- 这几个字符组成。
- 必须是数字或字母开头 (之前是以.creditease.corp 会报错tomcat cookie domain validation异常,最后改成了 creditease.corp)。
- 必须是数字或字母结尾。
二、前后端分离
2.1 解决跨域问题
由于是两个不同的应用,必然会有两个不同的端口。不同的端口就会有跨域问题,SSO采用的方式是通过nginx区分来自前后端的请求,反向代理请求对应到不同的服务去。
- sso.creditease.com对应的是后端的应用服务。
- sso.creditease.com/web对应的是前端的静态资源应用服务。
2.2 方便联调效率,引入swagger
swagger是后端接口展示的插件,通过修改拦截器代码,mock登录对象免登录,直接访问接口进行前后端的调试。在swagger插件上可以看到具体接口请求路径和参数、参数是否必须、返回值、接口统计信息等。
-
接口统计信息
-
请求参数和路径
- 返回值
2.3 跳转接口修改
之前是通过SpringMvc的modeAndview方式跳转的,现在做了两种处理:
- 改成restful接口的形式,前端控制跳转然后直接获取数据。
- 直接通过response.sendRedirect跳转页面。
注意:老代码跳转采用的是通过SpringMvc在return的页面路径前加redirect的形式,如:return "redirect:index",这样默认会在return的URL后加jessionID。
2.4 静态资源地址变更可能引发的问题
特别需要注意代码中的相关校验路径的地方。比如在这次改造过程中路径修改会影响以下几个方面。
- 菜单权限校验的时候,之前人、角色和路径已经绑定了,修改菜单访问路径会导致没权限。
- 扫码登录的接口判断了refer来源,修改路径会导致请求失败。
- 之前的sso-dome工程引用了静态资源,修改路径会报404。
原文地址:https://www.cnblogs.com/yixinjishu/p/11307121.html