SSO is a project that has existed in the company for several years. The backend uses SpringMVC and MyBatis, the database uses MySQL, and the front-end display uses Freemark. This year, we made a revolutionary improvement to the project, transforming it into the SpringCloud architecture and separating the front and back ends with the Vue framework at the front end.

First, use the SpringCloud architecture for transformation

1.1 Why Use SpringCloud

The core of SpringCloud is SpringBoot. Compared with traditional Spring, SpringCloud has the following advantages:

  • Easy to deploy, SpringBoot has a built-in Tomcat container that can compile programs directly into a JAR and run them through java-JAR.
  • Easy to code, SpringBoot just needs to add a starter-Web dependency to the POM file to help developers quickly start a Web container, which is very convenient.
  • With simple configuration, SpringBoot can replace Spring’s very complex XML approach with simple annotations. If I wanted to leave a normal class to Spring to manage, I would simply add @Configuration and @Bean annotations.
  • Monitoring is simple. The dependence of spring-boot-start-actuator can be introduced, and the operating performance parameters of processes can be obtained by using REST mode to achieve monitoring purposes.

1.2 What parts need to be transformed in a conventional project

1.2.1 Configuration File

Before an SSO project is revamped, there are a lot of configuration files, including the following:

  • Static resource correlation
  • The data source
  • Mybatis configuration
  • Redis configuration
  • The transaction
  • Interceptors intercept content
  • Listeners, filters
  • Component scan path configuration

This paper focuses on the following parts:

1) Static resource processing

In SpringMVC, static resources are not intercepted if the MVC: Interceptors URL rule is as follows.

<mvc:mapping path="/*.do" />
Copy the code

But if the configuration is:

<mvc:mapping path="/ * *" />
Copy the code

Solution 1: Configure

default
in web. XML and use defaultServlet to process the request first.

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

Copy the code

Option 2: Declare static resource paths using the < MVC :resources /> tag

<mvc:resources mapping="/resources/js/**" location="/js/" /> 
<mvc:resources mapping="/resources/images/**" location="/images/" /> 
<mvc:resources mapping="/resources/css/**" location="/css/" />
Copy the code

Scenario 3: Use MVC :default-servlet-handler/ tag

SpringBoot solution: Inherits the WebMvcConfigurerAdapter to implement the addResourceHandlers method.

public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/ * *")
    .addResourceLocations("classpath:/resource/")Sso static resources
    .addResourceLocations("classpath:/META-INF/resources/")// Static resources
    .setCachePeriod(0);//0 indicates no cache
}
Copy the code

Sso static resource file path as shown below:

2) Interceptors

The SpringMVC configuration file contents:

Intercept any request and initialize parameters. Some requests do not need to be intercepted, and some requests are allowed without permission verification after login.

<mvc:interceptors>  
	<mvc:interceptor>  
		<mvc:mapping path="/ * *" />  
           <bean class="Custom PermissionInterceptor">  
		   <! -- Address accessible without login -->  
		  <property name="excludeUrls">
		  <list><value>Request the address<value></list>
		  </property>
		  <! -- As long as the login does not need to intercept resources -->
		  <property name="LogInExcludeUrls">
		  <list><value>Request the address<value></list>
		  </property>
		 </bean>
   </mvc:interceptor> 
 </mvc:interceptors>
Copy the code

To add an interceptor to SpringBoot, simply inherit the WebMvcConfigurerAdapter and override the addInterceptors method.

 /*** interceptor *@param registry
 */ 
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(permissionInterceptor).
            addPathPatterns("/ * *");
 	super.addInterceptors(registry);
 }
Copy the code

Custom interceptors need to be initialized with some parameters, so they need to be registered before the interceptor is registered, which we set to lazy loading. Paths that do not need to be blocked by login and paths that do not need to determine permissions after login are written in yML files and obtained from the system Environment variable Environment.

@Autowired
@Lazy 
private PermissionInterceptor permissionInterceptor;  
@Autowired 
private Environment environment;

/ * * * * /
@Bean 
public PermissionInterceptor permissionInterceptor(a) {
  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; 
}
Copy the code
3) Database and Mybatis configuration

A. Data source configuration

There are three cases of data source injection:

【 Situation 1 】

  • Druid-spring-boot-starter relies only on druid.jar and spring.datasource. Type is not specified.
  • Result: The injected data source is tomcat’s data source.
  • Resolution: The mybatis-spring-boot-starter project relies on tomcat data sources, Spring – the boot – autoconfigure – starter DataSourceAutoConfiguration automatic injection class will be in the case of does not specify the data source, to determine if there is a default in the path of 4 kinds of data sources (Hikari, Tomcat, Dbcp,Dbcp2), if any.

【 Situation 2 】

  • Druid-spring-boot-starter relies only on druid.jar and sets spring.datasource. Type to DruidDataSource.
  • Result: Druid datasource data source is injected, but the Druid configuration in the configuration file does not take effect.
  • Yml specifies the druid data source. Yml specifies the druid data source. Yml specifies the druid data source. The @ConfigurationProperties annotation’s DataSourceProperties does not handle the druID part’s performance parameter properties, only the data source part’s properties.

【 Situation 3 】

  • Druid-spring-boot-starter does not rely on druid.jar. Specify spring.datasource. Type as DruidDataSource.
  • Result: If the Druid datasource data source is injected, the Druid configuration in the configuration file will take effect.
  • Resolution: Druid – spring – the boot – the starter will automatically configure class in DataSourceAutoConfiguration before you create a data source, And the @ConfigurationProperties injected DataSourceProperties contains druid’s properties in the configuration file.

Pom. XML depends on:

	<! -- Dependencies introduced by case 1 and 2 tests -->
	<! --<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>
	
Copy the code

Yml configuration:

Spring: a datasource: type: com. Alibaba. Druid. Pool. DruidDataSource # current data source type driver operation - class - the name: Com.mysql.jdbc.driver # mysql Driver package url: JDBC :mysql://yourURL # database name username: yourusername password: yourpassword druid: Min-idle: 5 # max-active: 20 # max-wait: 60000 # connection timeout time-between-eviction-runs-millis: 60000 # Configure the interval for detecting idle connections to be closed, in milliseconds min-evictable-idle-time-millis: ValidationQuery: select 'x' test-while-idle: true # Test test-on-borrow when connection is idle: False # Whether to test the connection when borrowing the connection from the connection pool test-on-return: false # Whether to test the connection when returning the connection to the connection pool filters: config,wall,statCopy the code

B. MyBatis configuration

By introducing the Mybatis -spring-boot-starter dependency, you can easily configure MyBatis for use.

The following simple analysis of mybatis-starter source and how to configure myBatis

The “spring.factories” file of mybatis-spring-boot-autoconfigure is used

# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfigu rationCopy the code

Mybatis’ sqlSessionFactory will be loaded only after the data source is created.

@ EnableConfigurationProperties ({MybatisProperties. Class}) annotation specifies in the prefix = “mybatis” part of the configuration file attributes, These property values are injected into the created SqlSessionFactoryBean, and the SqlSessionFactory object is generated.

@Configuration
// Load the current Bean if SqlSessionFactory, SqlSessionFactoryBean exists
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// Specify only preferred data sources when specifying only one or more data sources in the container
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
// Load the current Bean when the data source is injected into the Spring container
@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);
	   // Set the mybatis configuration file path
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
	      factory.setConfigLocation(this.resourceLoader.getResource
          (this.properties.getConfigLocation())); }}// Set properties in other MyBatisProperties objects as....
       returnfactory.getObject(); }}Copy the code

MybatisProperties contains properties:

@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;
 privateClass<? > typeAliasesSuperType;private String typeHandlersPackage;
 private boolean checkConfigLocation = false;
 private ExecutorType executorType;
 private Properties configurationProperties;
 @NestedConfigurationProperty
 private Configuration configuration;
}
Copy the code

C. Use Mybatis

  • Configuration file:

application.yml

Mybatis: config-location: classpath:mybatis. XML # mybatis: config-location: classpath:mybatis Com. Creditease. Permission. All the Entity model # alias classes in package mapper - locations: the classpath: mybatis / * * / *. XMLCopy the code

As you can see from MybatisProperties above, MyBatis can specify some configuration, such as custom interceptor pageHelper.

mybatis.xml

<?xml version="1.0" encoding="UTF-8" ? >

      
<configuration>
	<plugins>
		<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
		<plugin interceptor="com.creditease.permission.manager.MybatisInterceptor"></plugin>
	</plugins>
</configuration>
Copy the code
  • Add the @mapperscan annotation to the startup class
@MapperScan("com.creditease.permission.dao")// Mapper class directory
public class SsoApplication {
    public static void main(String[] args) { SpringApplication.run(SsoApplication.class, args); }}Copy the code
4) transaction

Spring transactions are handled in two ways:

  • programmatic

Use TransactionTemplate or used directly at the bottom of the PlatformTransactionManager transaction code is written in the business code.

Advantages: Transactions can be handled in code blocks, which is flexible.

Disadvantages: Invasive to code.

  • declarative

Intercepts before and after methods using the @Transactional annotation or based on configuration files.

Advantages: Non-invasive and does not contaminate code.

Disadvantages: Transactions can only be controlled on methods and classes, with small granularity.

A) Use the @Transactional annotation

For non-Springboot projects, add the following configuration to the configuration file:

 <tx:annotation-driven/>
Copy the code

SpringBoot engineering can use @ EnableTransactionManagement notes instead of the configuration above content.

B. Use configuration files

The previous SSO approach was configuration based with the following configuration code:

<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>
Copy the code

The modified SpringBoot is based on Java code:

@Aspect
@Configuration
public class TransactionAdviceConfig {

 /** * specifies the pointcut */
 private static final String AOP_POINTCUT_EXPRESSION = "execution(public * com.creditease.permission.service.impl.*Impl.*(..) )";

 @Resource
 DruidDataSource dataSource;

 / * * * to specify transaction PlatformTransactionManager *@return* /
 @Bean
 public DataSourceTransactionManager transactionManager(a) {

     return new DataSourceTransactionManager(dataSource);

 }

 /** * specifies the pointcut processing logic to execute transactions *@return* /
 @Bean
 public TransactionInterceptor txAdvice(a) {

     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 assembles the configuration to inject the Advice code logic into the Pointcut location *@return* /
 @Bean
 public Advisor txAdviceAdvisor(a) {
     
     AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
     pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
     return new DefaultPointcutAdvisor(pointcut, txAdvice());
 }
Copy the code
5) Global exception handling

When you code an exception, you try catch it. Sometimes you catch more than one exception at a time to distinguish between different exceptions. A lot of try catch statements. It is also redundant to write the same exception handling code multiple times, so it is necessary to introduce global exception handling.

Exception handling configuration file before transformation:

<! Define the exception handling page -->
<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>
Copy the code

Using SimpleMappingExceptionResolver class to handle exceptions, set the custom exception type NopermissionException, and after the occurrence of abnormal request path/permission/noSecurity.

In SpringBoot, use @RestControllerAdvice or @ControllerAdvice to set the global exception class. This distinction is similar to the @Controller and @RestController annotations.

Three types of global Exception handling are defined in SSO: plain Exception handling; Custom NopermissionException and parameter verification exception.

The global exception handling code is as follows:

@Configuration
@Slf4j
@RestControllerAdvice
public class GlobalExceptionConfig {

	// No permission processing
    @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("Jump to unauthorized page address");
    }
    
   	 // Check the parameters
	@ExceptionHandler(value = {BindException.class})
    public ResultBody BindExceptionHandler(BindException bindException){
        List<ObjectError> errors = bindException.getBindingResult().getAllErrors();
        // This ResultBody is a return result object, which needs to return JSON, which contains the status code and prompts
        return  ResultBody.buildFailureResult(errors.get(0).getDefaultMessage());
    }

	// All uncaught exception handling logic
    @ExceptionHandler(value = {Exception.class})
    public ResultBody exceptionHandler(HttpServletRequest request,Exception ex){
        printLog(request,ex);
        return  ResultBody.buildExceptionResult();
    }

	// Print out the request parameters and exceptions, annotated with @slf4j
    public void printLog(HttpServletRequest request,Exception ex){
        String parameters = JsonHelper.toString(request.getParameterMap());
        log.error("url>>>:{},params>>>:{} ,printLog>>>:{}",request.getRequestURL(),parameters,ex); }}Copy the code

@RestControllerAdvice in conjunction with @Validation validates the Bean. Failure to validate throws a BindException. By using annotations, you can write less if-else code, determine whether the interface parameter of the request is empty, and improve the beauty of the code. Such as:

	// The general practice
	if(StringUtils.isEmpty(ssoSystem.getSysCode())
	
	/ / SSO
	// Add the @valid annotation to the Controller request method
	@RequestMapping(value = "/add", method = RequestMethod.POST)
    public ResultBody add(@Valid @RequestBody SsoSystem ssoSystem) {}// annotate @notnull on the properties of the SsoSystem Bean that you want to process
	@NotNull(message = "System number cannot be empty")
	private String sysCode;
Copy the code

When sysCode passes an empty parameter, it throws a BindException that is global and returns the json parameter:

{
    "resultCode":2."resultMsg":"System number cannot be empty"."resultData":null
}
Copy the code

1.3 Precautions

1.3.1 Problems caused by too high built-in Tomcat version

SpringBoot1.5 uses the embedded tomcat8.5 version by default, while the original SSO of SpringMVC is deployed on tomcat7. The most obvious influence of tomcat upgrade on this transformation is cookie. After tomcat8, the cookie verification protocol is Rfc6265CookieProcessor. The protocol requires that domains be named according to the following rules:

  • The value contains 1 to 9 characters, a-z, A-z, periods (.), and -.
  • The tomcat cookie Domain Validation must start with a letter or number. Creditease.
  • Must end in a number or letter.

Two, front and rear end separation

2.1 Solving cross-domain Problems

Since these are two different applications, there must be two different ports. Different ports will have cross-domain problems. SSO uses Nginx to distinguish requests from the front and back end, and reverse proxy requests correspond to different services.

  • Sso.creditease.com corresponds to the back-end application service.
  • sso.creditease.com/web corresponds to the front-end static resource application service.

2.2 Convenient joint adjustment efficiency and swagger introduction

Swagger is a plug-in for the back-end interface display. By modifying the interceptor code, the mock login object can directly access the interface for front-end and back-end debugging without logging in. On swagger plug-in, you can see the specific interface request path and parameters, whether parameters are required, return value, interface statistics, etc.

  • Interface Statistics

  • Request parameters and paths

  • The return value

2.3 Modifying the Forward Interface

The SpringMvc modeAndview jump is done in two ways:

  • Change to a restful interface where the front end controls the jump and gets the data directly.
  • Go to the page directly through Response. sendRedirect.

Return “redirect:index”, which adds a jessionID to the return URL by default.

2.4 Problems Caused by Static Resource ADDRESS Change

Pay special attention to the places in the code related to checkpath. For example, path modification in this transformation process will affect the following aspects.

  • During menu permission verification, the user, role, and path have been bound. If the menu access path is modified, permissions will be lost.
  • The interface scanning the code determines the source of the refer. Changing the path will cause a request failure.
  • The previous SSO-Dome project referenced a static resource and changed the path to 404.

Author: Huang Lingfeng

Source: Creditease Institute of Technology