SSO是公司一个已经存在了若干年的项目,后端采用SpringMVC、MyBatis,数据库使用MySQL,前端展示使用Freemark。今年,我们对该项目进行了一次革命性的改进,将其改造成SpringCloud架构,并且把前后端分离,前端采用Vue框架。
一、使用SpringCloud架构进行改造
1.1 为什么使用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 配置文件
静态资源相关 数据源 mybatis配置 redis配置 事务 拦截器拦截内容 监听器、过滤器 组件扫描路径配置
1)静态资源处理
mvc:interceptors
配置的URL规则如下,则不会拦截静态资源。
- <mvc:mapping path="/*.do" />
但是如果配置的是:
- <mvc:mapping path="/**" />
<servlet-name>default</servlet-name>
,用defaultServlet
先处理请求如:
- <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 />
标签声明静态资源路径
- <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方法。
- public void addResourceHandlers(ResourceHandlerRegistry registry) {
- registry.addResourceHandler("/**")
- .addResourceLocations("classpath:/resource/")//sso静态资源
- .addResourceLocations("classpath:/META-INF/resources/")//swagger静态资源
- .setCachePeriod(0);//0表示不缓存
- }
sso静态资源文件路径如图:
2)拦截器
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;
- }
3)数据库和MyBatis配置
-
条件:不引⼊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依赖:
- <!-- 情况一、二 测试引入的依赖 -->
- <!--<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配置
- # Auto Configure
- org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
可以看到自动注入类是MybatisAutoConfiguration,我们从这个类入手分析可以知道,必须先创建好了数据源后,才会加载MyBatis的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
配置文件
- mybatis:
- config-location: classpath:mybatis.xml # mybatis配置文件所在路径
- type-aliases-package: com.creditease.permission.model # 所有Entity别名类所在包
- mapper-locations: classpath:mybatis/**/*.xml
从上面的MybatisProperties可以看出,mybatis可以指定一些configuration,比如自定义拦截器pageHelper。
- <?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注解
- @MapperScan("com.creditease.permission.dao")//mapper类所在目录
- public class SsoApplication {
- public static void main(String[] args) {
- SpringApplication.run(SsoApplication.class, args);
- }
- }
4)事务
编程式
声明式
- <tx:annotation-driven/>
- <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());
- }
5)全局异常处理
- <!--定义异常处理页面-->
- <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
- }
1.3 注意事项
1.3.1 内置tomcat版本太高引发的问题
必须是1-9、a-z、A-Z、. 、- 这几个字符组成。 必须是数字或字母开头 (之前是以.creditease.corp 会报错tomcat cookie domain validation异常,最后改成了 creditease.corp)。 必须是数字或字母结尾。
sso.creditease.com对应的是后端的应用服务。 sso.creditease.com/web对应的是前端的静态资源应用服务。
2.2 方便联调效率,引入swagger
接口统计信息
请求参数和路径
返回值
2.3 跳转接口修改
改成restful接口的形式,前端控制跳转然后直接获取数据。 直接通过response.sendRedirect跳转页面。
注意:老代码跳转采用的是通过SpringMvc在return的页面路径前加redirect的形式,如:return "redirect:index",这样默认会在return的URL后加jessionID。
2.4 静态资源地址变更可能引发的问题
菜单权限校验的时候,之前人、角色和路径已经绑定了,修改菜单访问路径会导致没权限。 扫码登录的接口判断了refer来源,修改路径会导致请求失败。 之前的sso-dome工程引用了静态资源,修改路径会报404。
【本文是51CTO专栏机构宜信技术学院的原创文章,微信公众号“宜信技术学院( id: CE_TECH)”】